Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ src/agentmemory-launcher.ts # Plugin entry (V2 module)

Flow: on plugin load (`setup()`) → `GET /agentmemory/livez` (2s timeout) → spawns `npx @agentmemory/agentmemory` if down → 60s supervision loop → cleanup function stops the loop on unload.

Windows launch path (two-mode): a launch triggered by the startup health check opens a visible Windows Terminal tab (`wt -w 0 nt --title agentmemory --suppressApplicationTitle node <cli>` — WT launches `node` itself, no cmd/pwsh shell hop) — focus is taken once at startup, which is acceptable. Every launch from the 60s supervision loop is silent: plain `windowsHide` is not enough there (it hides only the direct child, and every cmd.exe hop — `shell: true`, npx's `.cmd` shim — lets a grandchild allocate a NEW console, which Windows Terminal shows as a focus-stealing tab), so recovery resolves `dist/cli.mjs` from the **newest** agentmemory it can find — the npm/npx cache (cache dir discovered via `npm_config_cache` → `~/.npmrc` `cache=` → `%LOCALAPPDATA%/npm-cache`, plus a `../cache/_npx` probe beside each PATH entry for portable installs like scoop) or a global install on `PATH` (`<dir>/node_modules/@agentmemory/agentmemory` and `<prefix>/lib/node_modules/@agentmemory/agentmemory`), highest version wins — and spawns `node` directly on it: the tree (node → cli.mjs → iii.exe) contains no cmd.exe and never allocates a console (the CLI spawns iii-engine with `windowsHide` itself). Falls back to the legacy npx spawn when no candidate is found or the direct spawn errors. Relaunches are gated by a 90s boot grace window plus a cross-instance self-expiring lock (`%TEMP%/agentmemory-launcher.lock`) because every OpenCode server runs its own plugin instance against the same backend.
Windows launch path (always silent): every launch — startup and 60s-supervision recovery alike — bypasses npx/cmd and spawns `node` directly on `dist/cli.mjs` resolved from the **newest** agentmemory it can find (the npm/npx cache discovered via `npm_config_cache` → `~/.npmrc` `cache=` → `%LOCALAPPDATA%/npm-cache`, plus a `../cache/_npx` probe beside each PATH entry for portable installs like scoop, or a global install on `PATH` at `<dir>/node_modules/@agentmemory/agentmemory` / `<prefix>/lib/node_modules/@agentmemory/agentmemory`, highest version wins). The tree (node → cli.mjs → iii.exe) contains no cmd.exe and never allocates a console (the CLI spawns iii-engine with `windowsHide` itself; live process-tree audits show no cmd.exe/conhost under the spawned backend). Falls back to the legacy npx spawn only when no candidate is found or the direct spawn errors. Plain `windowsHide` is not enough for the npx path: it hides only the direct child, and every cmd.exe hop — `shell: true`, npx's `.cmd` shim — lets a grandchild allocate a NEW console, which Windows Terminal shows as a focus-stealing window. Relaunches are gated by a 90s boot grace window plus a cross-instance self-expiring lock (`%TEMP%/agentmemory-launcher.lock`) because every OpenCode server runs its own plugin instance against the same backend.

## Agentmemory Backend (Critical Knowledge)

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to opencode-agentmemory-launcher will be documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **Windows launch is now always silent — the startup tab is gone.** A launch triggered by the startup health check (backend down when the plugin loads) now takes the exact path recovery launches have used since 4.1.0: resolve the newest `dist/cli.mjs` and spawn `node` directly on it, so no window, terminal tab, or focus steal ever appears at startup either. The earlier rollback to a visible startup tab (4.2.0) was triggered by console flashes at OpenCode startup that are actually caused by OpenCode's own local-MCP spawns (`cmd.exe /d /s /c npx …` allocates a console per server, and each OpenCode server instance spawns the whole local fleet) — not by this plugin, whose spawned tree (node → cli.mjs → iii.exe) carries no cmd.exe/conhost at all (verified by live process-tree audits). Diagnostics for a failed launch remain in the plugin log (`warn`) plus `agentmemory doctor`.

## [4.2.1] - 2026-10-07

### Changed
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,10 @@ Restart OpenCode to relaunch agentmemory with the updated version.

1. **On load** (`setup()`): the plugin starts a health-check interval (60s)
2. **Health check**: Pings `GET /agentmemory/livez` on the backend (always public, no auth — even with agentmemory 0.9.30's auth-by-default)
3. **Auto-restart (two-mode)**: If the health check fails, the backend is launched.
- **Startup** (backend down when the plugin loads): opens a visible Windows Terminal tab titled `agentmemory` (`wt -w 0 nt --title agentmemory --suppressApplicationTitle node <cli>`) — Windows Terminal launches `node` itself, so no cmd/pwsh shell is involved. The backend coming up is visible; focus is taken once here — acceptable at startup, and never on the recovery path. Falls back to the silent path when `wt.exe` is unavailable.
- **Recovery** (every later (re)launch from the 60s loop): fully silent. On Windows the plugin bypasses npx/cmd entirely — it resolves the **newest** `dist/cli.mjs` it can find in the npm/npx cache or a global install on `PATH`, and spawns `node` directly on it, so the process tree (node → cli.mjs → iii.exe) never touches cmd.exe and never allocates a console: no window/tab and no focus steal, mid-session or while idle (plain `windowsHide` is insufficient because every cmd.exe hop lets a grandchild allocate a new console). Falls back to an npx spawn when no local copy is found. Relaunches are throttled by a 90s boot grace window plus a cross-instance launch lock (multiple OpenCode servers share one backend)
3. **Auto-restart**: If the health check fails, the backend is launched.
- **Always silent** (startup and every later (re)launch from the 60s loop): on Windows the plugin bypasses npx/cmd entirely — it resolves the **newest** `dist/cli.mjs` it can find in the npm/npx cache or a global install on `PATH`, and spawns `node` directly on it, so the process tree (node → cli.mjs → iii.exe) never touches cmd.exe and never allocates a console: no window/tab and no focus steal, at startup, mid-session or while idle (plain `windowsHide` is insufficient because every cmd.exe hop lets a grandchild allocate a new console). Falls back to an npx spawn when no local copy is found. Relaunches are throttled by a 90s boot grace window plus a cross-instance launch lock (multiple OpenCode servers share one backend); a startup launch does not count toward the failure backoff, so a failed startup is retried ~90s later
4. **Debug mode**: Set `OPENCODE_AGENTMEMORY_DEBUG=1` for verbose logging
5. **Failure diagnostics**: A backend launch that exits with a non-zero code is logged at `warn` with the likely causes — a daemon already answering on the port (agentmemory 0.9.30 refuses to start a second instance) or the enforced iii-engine pin mismatch. A visible startup tab whose process exits with a non-zero code stays open (Windows Terminal's `closeOnExit: graceful`) so the error text remains readable
5. **Failure diagnostics**: A backend launch that exits with a non-zero code is logged at `warn` with the likely causes — a daemon already answering on the port (agentmemory 0.9.30 refuses to start a second instance) or the enforced iii-engine pin mismatch. The backend runs with `stdio: "ignore"`, so for the actual error text run `npx @agentmemory/agentmemory doctor`

## Environment Variables

Expand All @@ -126,7 +125,7 @@ Two common causes: agentmemory 0.9.30 enforces its iii-engine pin (v0.22.1), so
npx @agentmemory/agentmemory doctor
```

**A visible startup tab shows `agentmemory worker did not become ready within 15s` and exits with code 1.**
**The launcher log shows `agentmemory worker did not become ready within 15s` and the launch exits with code 1.**
A stale instance from an earlier session is still holding the REST port, so the fresh CLI starts its engine but its worker registration never becomes ready — a healthy fresh start completes in ~2s. Diagnose and restart the whole chain (never kill a single worker process: it may be the active route handler, and killing it makes `/agentmemory/livez` return 404):

```powershell
Expand Down
61 changes: 18 additions & 43 deletions src/agentmemory-launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import { delimiter, join } from "node:path";
// Uses /agentmemory/livez (always public, no auth) for health checks.
// Health-checks every 60s and restarts the backend if it is down.
//
// Windows launch is two-mode:
// • startup — the backend is down when the plugin loads, so open a visible
// Windows Terminal tab titled "agentmemory" for it. Taking focus once at
// startup is acceptable: the user sees the backend come up.
// • recovery — every later (re)launch from the 60s supervision loop must be
// invisible. `windowsHide` alone is NOT enough there: it only suppresses
// the direct child, and every hop through cmd.exe (npx's shim,
// `shell: true`) lets a grandchild allocate a NEW console, which Windows
// Terminal surfaces as a focus-stealing tab. So recovery bypasses npx/cmd
// and spawns `node` directly on the resolved CLI entry; the tree
// (node → cli.mjs → iii.exe) never touches cmd.exe and never owns a
// console (the CLI spawns iii-engine with `windowsHide` itself).
// Windows launch is always silent — no window, no tab, no focus steal, at
// startup and on recovery alike. `windowsHide` alone is NOT enough: it only
// suppresses the direct child, and every hop through cmd.exe (npx's shim,
// `shell: true`) lets a grandchild allocate a NEW console, which Windows
// Terminal surfaces as a focus-stealing window. So every launch bypasses
// npx/cmd and spawns `node` directly on the resolved CLI entry; the tree
// (node → cli.mjs → iii.exe) never touches cmd.exe and never owns a console
// (the CLI spawns iii-engine with `windowsHide` itself). Live process-tree
// audits confirm the spawned backend carries no cmd.exe/conhost at all,
// while OpenCode's own local-MCP spawns (`cmd.exe /d /s /c npx …`) each
// allocate a console — console flashes at OpenCode startup come from those
// MCP spawns, not from this plugin.
//
// OpenCode V2 plugin: the host loads the default export `{ id, setup }`.
// Supervision starts in `setup()` (V2 has no `config` hook) and the returned
Expand Down Expand Up @@ -54,12 +54,13 @@ let lastLaunchAt = 0;
let launchAttempts = 0;
let timer: ReturnType<typeof setInterval> | null = null;

/** Startup launches open a visible tab; recovery launches are silent. */
/**
* Why a launch is happening. Startup launches (backend down when the plugin
* loads) do not count toward the failure backoff, so a failed startup is
* retried ~90s later rather than 180s. Both modes spawn identically silent.
*/
type LaunchReason = "startup" | "recovery";

/** Title of the visible startup tab (kept stable via --suppressApplicationTitle). */
const TAB_TITLE = "agentmemory";

type Level = "info" | "warn" | "error" | "debug";

// V2 has no `client.app.log` equivalent — warn/error go to stderr
Expand Down Expand Up @@ -271,27 +272,6 @@ function spawnSilent(cli: string, env: NodeJS.ProcessEnv, label: string): void {
void log("info", `launching backend silently via ${label}`);
}

/**
* Startup launch: open a visible Windows Terminal tab running the backend.
* No shell is involved — Windows Terminal launches `node` itself, so the
* visible path is as shell-free as the silent one (no cmd/pwsh hop).
* Focus is taken once — acceptable at startup, never used on the recovery path.
* Degrades to the silent launch when wt.exe is unavailable.
*/
function spawnVisibleTab(cli: string, env: NodeJS.ProcessEnv, label: string): void {
const child = spawn(
"wt.exe",
["-w", "0", "nt", "--title", TAB_TITLE, "--suppressApplicationTitle", "node", cli],
{ detached: true, stdio: "ignore", windowsHide: true, env },
);
child.on("error", (err) => {
void log("warn", `visible tab spawn failed (${err.message}); falling back to a silent launch`);
spawnSilent(cli, env, label);
});
watch(child, `visible tab → ${label}`);
void log("info", `launching backend in a visible tab via ${label}`);
}

function launch(reason: LaunchReason): void {
const now = Date.now();
// Exponential backoff across consecutive FAILED attempts (90s, 180s, …,
Expand All @@ -310,12 +290,7 @@ function launch(reason: LaunchReason): void {
const resolved = process.platform === "win32" ? resolveCli() : null;

if (resolved) {
const label = `${resolved.version} (${resolved.path})`;
if (process.platform === "win32" && reason === "startup") {
spawnVisibleTab(resolved.path, env, label);
} else {
spawnSilent(resolved.path, env, label);
}
spawnSilent(resolved.path, env, `${resolved.version} (${resolved.path})`);
return;
}

Expand Down
Loading