diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index f45414da34..486d10321e 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -204,6 +204,11 @@ Run opencodex as a login-managed background service (macOS **launchd**, Linux ** Windows **Task Scheduler**) that auto-starts on login and auto-restarts on crash. Service runs set `OCX_SERVICE=1` so a restart does not churn the Codex config. +The Windows wrapper verifies its baked Bun runtime and CLI entry before every start attempt. If an +interrupted package update removed either file, it logs one `installation is incomplete` message and +stops instead of retrying the same missing executable every five seconds. Reinstall opencodex, then +run `ocx service repair` to refresh the task with the restored package paths. + | Subcommand | Action | | --- | --- | | none | Create/update and start the service. | diff --git a/src/service.ts b/src/service.ts index 253ff00e71..2a5de28478 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1545,6 +1545,14 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ '>>"%OCX_SERVICE_LOG%" echo opencodex_home="%OPENCODEX_HOME%"', '>>"%OCX_SERVICE_LOG%" echo codex_home="%CODEX_HOME%"', '>>"%OCX_SERVICE_LOG%" echo token_file="%OCX_API_TOKEN_FILE%"', + 'if not exist "%OCX_BUN%" (', + ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: bundled Bun is missing; reinstall opencodex, then run ocx service repair', + " exit /b 3", + ")", + 'if not exist "%OCX_CLI%" (', + ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: CLI entry is missing; reinstall opencodex, then run ocx service repair', + " exit /b 3", + ")", `"%OCX_BUN%" "%OCX_CLI%" start --port ${port} >>"%OCX_SERVICE_LOG%" 2>&1`, "if %ERRORLEVEL% NEQ 0 (", ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] child exited with code %ERRORLEVEL%; restarting in 5s', diff --git a/structure/06_docs-and-release.md b/structure/06_docs-and-release.md index ec599f8318..71c86df6f9 100644 --- a/structure/06_docs-and-release.md +++ b/structure/06_docs-and-release.md @@ -35,6 +35,16 @@ bun install --frozen-lockfile bun run build ``` +## Windows service wrapper and incomplete updates + +[Decision Log] +- 목적과 의도: Prevent a failed npm replacement from making the Task Scheduler wrapper retry missing package files forever. +- 기존 구현 및 제약 조건: The wrapper deliberately restarts a proxy after runtime crashes, but an absent baked Bun or CLI path cannot recover inside that process. Current updater preflight and stop-first behavior reduce replacement risk but do not provide a transactional restore of npm's package tree and global launchers. +- 검토한 주요 대안: Keep unconditional five-second retries, add a generic crash ceiling, restore npm directories in-place, or classify only proven missing executable paths as terminal. +- 선택한 방식: Check the baked Bun and CLI paths before every spawn; log one actionable incomplete-install message and exit with code 3 when either is absent. Preserve the existing retry loop for a child that actually launched and then failed. +- 다른 대안 대신 이 방식을 선택한 이유: A generic retry ceiling can stop a service after unrelated intermittent crashes, while copying a package directory without matching npm shims, ownership, and lock guarantees is not a safe rollback. +- 장점, 단점 및 영향: File-less package skeletons no longer produce unbounded service logs or restart churn. The wrapper still recovers ordinary proxy crashes, but repairing an incomplete npm install remains an explicit reinstall plus `ocx service repair` operation until a verified staged-update design exists. + ## GitHub workflow map | Workflow | Trigger | Purpose | diff --git a/tests/service.test.ts b/tests/service.test.ts index 174953a9bb..96018f8c12 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -523,6 +523,30 @@ describe("Windows service task", () => { expect(script).not.toContain("timeout /t"); }); + test("stops instead of restart-looping when an update removed the baked runtime or CLI (#1849)", () => { + const script = buildWindowsServiceScript({ + bun: "C:\\OpenCodex\\bun.exe", + bunRuntimeSource: "bundled", + cli: "C:\\OpenCodex\\cli.ts", + }); + const loopAt = script.indexOf(":loop"); + const bunCheckAt = script.indexOf('if not exist "%OCX_BUN%"'); + const cliCheckAt = script.indexOf('if not exist "%OCX_CLI%"'); + const launchAt = script.indexOf('"%OCX_BUN%" "%OCX_CLI%" start --port'); + const retryAt = script.indexOf("goto loop"); + + expect(loopAt).toBeGreaterThanOrEqual(0); + expect(bunCheckAt).toBeGreaterThan(loopAt); + expect(cliCheckAt).toBeGreaterThan(bunCheckAt); + expect(launchAt).toBeGreaterThan(cliCheckAt); + expect(retryAt).toBeGreaterThan(launchAt); + expect(script).toContain("installation is incomplete: bundled Bun is missing"); + expect(script).toContain("installation is incomplete: CLI entry is missing"); + expect(script.match(/exit \/b 3/g)).toHaveLength(2); + // `goto loop` re-enters both checks before another child spawn. + expect(script.slice(loopAt, launchAt).match(/if not exist/g)).toHaveLength(2); + }); + test("rewrites profile-relative paths to env indirection so non-ASCII usernames survive OEM-codepage batch parsing", () => { const oldUserProfile = process.env.USERPROFILE; const oldAppData = process.env.APPDATA;