Skip to content
Open
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
50 changes: 45 additions & 5 deletions bin/ocx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { checkNpmCacheOwnership, formatNpmCacheOwnershipFailure } from "../src/update/npm-cache-preflight.mjs";
import {
INSTALLER_TREE_CLEANUP_FAILED_EXIT_CODE,
runProcessTreeCommand,
} from "../src/update/install-process.mjs";
import { isRealBunBinary } from "../src/lib/bun-binary-validator.mjs";
import { npmInvocation } from "../src/update/npm-invocation.mjs";
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "../src/update/tray-update-plan.mjs";

const PKG = "@bitkyc08/opencodex";
const NPM_INSTALL_TIMEOUT_MS = 180_000;
const require = createRequire(import.meta.url);
const here = dirname(fileURLToPath(import.meta.url));
const cliPath = join(here, "..", "src", "cli", "index.ts");
Expand Down Expand Up @@ -110,7 +116,7 @@ function runTrayLifecycle(launcher, action) {
});
}

function runNpmSelfUpdate() {
async function runNpmSelfUpdate() {
const current = currentPackageVersion();
const tag = updateTag(current);
const latestInvocation = npmInvocation(["view", `${PKG}@${tag}`, "version"]);
Expand All @@ -133,6 +139,15 @@ function runNpmSelfUpdate() {
process.exit(0);
}

const cacheOwnership = checkNpmCacheOwnership();
if (cacheOwnership.ok === false) {
console.error(`opencodex: ${formatNpmCacheOwnershipFailure(cacheOwnership)}`);
process.exit(1);
}
if (cacheOwnership.ok === "skipped") {
console.warn(`opencodex: npm cache ownership pre-flight skipped: ${cacheOwnership.reason}. Proceeding best-effort.`);
}

// Remember whether a background service manages the proxy BEFORE stopping — `ocx stop`
// unloads it permanently, so a successful update must reinstall it afterwards.
const serviceStatePath = join(configDir(), "service-state.json");
Expand Down Expand Up @@ -222,12 +237,32 @@ function runNpmSelfUpdate() {
}

console.log(`Updating${latest ? ` to v${latest}` : ""}...\n$ npm install -g ${PKG}@${tag}`);
const res = spawnSync(installInvocation.file, installInvocation.args, {
const res = await runProcessTreeCommand(installInvocation.file, installInvocation.args, {
stdio: "inherit",
timeout: 180000,
timeoutMs: NPM_INSTALL_TIMEOUT_MS,
windowsHide: true,
...installInvocation.options,
});
if (!res.treeExited) {
console.error("\nopencodex: installer process-tree cleanup could not be confirmed; automatic recovery is unsafe until those processes exit.");
console.error(` The proxy is stopped. Once no 'npm' installer processes remain, run 'ocx start' or re-run 'ocx update'.${trayBeforeUpdate.restoreOnFailure ? " The Windows tray also remains stopped; run 'ocx tray start' after the installer processes exit." : ""}`);
process.exit(INSTALLER_TREE_CLEANUP_FAILED_EXIT_CODE);
}
if (res.interruptedSignal) {
if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
console.error(`\nopencodex: update interrupted (${res.interruptedSignal}); the proxy is still stopped. Run 'ocx start' or re-run 'ocx update'.`);
const exitCode = res.interruptedSignal === "SIGINT"
? 130
: res.interruptedSignal === "SIGHUP"
? 129
: 143;
if (process.platform === "win32") {
process.exit(exitCode);
}
process.kill(process.pid, res.interruptedSignal);
// Do not return to the update call site while fatal signal delivery is pending.
process.exit(exitCode);
}
Comment on lines +246 to +265

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Both update paths check !treeExited before interruptedSignal, and runProcessTreeCommand forces treeExited === false on POSIX once cleanup starts — so the interruption and timeout branches are dead code on Linux and macOS, and the tests that cover them only match source text.

The root cause is in src/update/install-process.mjs lines 203-351. Any forwarded signal or the timeout timer calls startCleanup(), which assigns cleanupPromise while the child is alive. The final computation is then treeExited = process.platform === "win32" && knownGroupExited, which is unconditionally false off Windows. Both callers branch on !treeExited first, so on POSIX a Ctrl+C and a 180s timeout both land in the unconfirmed-cleanup branch and exit 75. The fail-closed behavior is correct; the problem is that both callers present three distinct outcomes and deliver one, and both test files assert the unreachable branches by grepping source text.

  • bin/ocx.mjs#L246-L265: report the interruption cause inside the !res.treeExited branch at line 247, using res.interruptedSignal and res.timedOut. The POSIX process.kill(process.pid, res.interruptedSignal) re-raise at line 262 is unreachable; either delete it or reorder the two checks and remove the tray restart from the interruption branch.
  • src/update/index.ts#L289-L304: apply the same message change at line 290 so the operator learns whether they interrupted the update or it timed out; note that "timed out after 180s" at line 419 currently renders only on Windows.
  • tests/ocx-launcher-source.test.ts#L26-L41: replace the text assertions at lines 38-39 with a behavioral assertion on the outcome classification, or add a comment recording that the interruption block is Windows-only in practice.
  • tests/update-stop-first.test.ts#L113-L151: add a behavioral test that maps a { treeExited, interruptedSignal, status } triple to the selected branch on each platform; keep the timeout-margin check at lines 136-142, which already asserts a real invariant.
📍 Affects 4 files
  • bin/ocx.mjs#L246-L265 (this comment)
  • src/update/index.ts#L289-L304
  • tests/ocx-launcher-source.test.ts#L26-L41
  • tests/update-stop-first.test.ts#L113-L151
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/ocx.mjs` around lines 246 - 265, Update the outcome handling so POSIX
interruptions and timeouts are reported distinctly even when tree cleanup is
unconfirmed: in bin/ocx.mjs lines 246-265, use res.interruptedSignal and
res.timedOut within the !res.treeExited branch, and remove the unreachable POSIX
signal re-raise or reorder branches while avoiding duplicate tray restoration.
Apply the same reporting change in src/update/index.ts lines 289-304, preserving
the 180-second timeout message. Replace source-text assertions with behavioral
outcome classification coverage in tests/ocx-launcher-source.test.ts lines 26-41
and tests/update-stop-first.test.ts lines 113-151; retain the existing
timeout-margin invariant.

if (res.status === 0) {
console.log(`\nUpdated${latest ? ` to v${latest}` : ""}.`);
repairCodexShimIfNeeded();
Expand Down Expand Up @@ -306,7 +341,12 @@ function runNpmSelfUpdate() {
process.exit(0);
}
if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
console.error(`\nUpdate failed (npm exit ${res.status ?? "?"}). Try manually: npm install -g ${PKG}@${tag}`);
const failure = res.timedOut
? `timed out after ${Math.trunc(NPM_INSTALL_TIMEOUT_MS / 1000)}s`
: res.error
? `could not run: ${res.error.message}`
: `exit ${res.status ?? "?"}`;
console.error(`\nUpdate failed (npm ${failure}). Try manually: npm install -g ${PKG}@${tag}`);
process.exit(1);
}

Expand Down Expand Up @@ -386,7 +426,7 @@ if (updateHelpRequested) {
}

if (process.argv[2] === "update" && isNodeModulesInstall() && !isBunGlobalInstall()) {
runNpmSelfUpdate();
await runNpmSelfUpdate();
}

const bun = resolveBun();
Expand Down
43 changes: 43 additions & 0 deletions devlog/_plan/260802_pr557_finalize/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# PR #557 finalize — research (2026-08-02)

Branch `codex/pr533-update-recovery-hardening`, worktree `260727-pr533-current`.
Policy decision (maintainer, 2026-08-02): **안 A fail-closed** — Wibias recommendation.

## Facts established at P

- Branch is 16 ahead / 768 behind origin/dev; merge-base `800ebc931`.
- dev carries NONE of this work: `sanitizeUpdateJobText` / `npm-cache-preflight`
count on origin/dev:src/update/job.ts = 0. The branch is the sole carrier.
- Merge surface: 14 files changed in both (37 conflict markers): `src/update/job.ts`,
`src/update/index.ts`, `src/config.ts`, tests ×5 (`update-job`, `update-stop-first`,
`ocx-launcher-source`, `config`, `windows-deploy-close-regressions`),
docs-site cli reference (zh-cn, ru), others. 599 paths merge clean; 166 removed
in remote (dev deletions, branch untouched).
- Open Wibias blocker (sanitizer): `src/update/job.ts:324-330` regexes use
`[^\s"'<>]`, so `C:\Users\Alice Smith\.npm\_cacache\tmp\entry` leaks whole and
`/Users/Alice Smith/.npm/_cacache/tmp` half-redacts. Wibias ran the exact
regexes against those inputs — reproduction is authoritative.
- Closed blocker (preflight): `src/update/npm-cache-preflight.mjs:98-113`
`accessSync` R/W/X fail-closed — DO NOT TOUCH.
- Auto-recovery path to remove (policy A): `recoverFailedGuiUpdate`
(`src/update/job.ts:908`) step 4 — `findNpmRecoveryLaunchers` + restart-candidate
loop + unhealthy-candidate kill. Detection/reporting steps 1-3 (healthy probe,
oldPid identity, replacement-became-healthy) stay: they only observe and refuse,
never start a process. The success-path `restartAfterUpdate` stays untouched.
- Attribution obligation (Wibias): PR description and final merge commit must
credit #533 and @WZBbiao (imported commits keep the `wzb` author identity but
GitHub does not link the email).

## Claim ledger

| # | Claim | Source | Status |
|---|-------|--------|--------|
| 1 | Sanitizer leaks space-containing profile paths | Wibias review on #557 (ran the regexes) | verified by reviewer; re-prove red in B |
| 2 | Preflight blocker closed | Wibias review | verified; non-goal |
| 3 | Auto-recovery races a live installer (policy rationale) | Wibias recommendation; maintainer decision A | decision recorded |
| 4 | dev has no part of this branch's feature code | `git show origin/dev:src/update/job.ts` grep = 0 | verified |

## Out of scope

- Force-push / rebase. Merge origin/dev in (regular push afterwards).
- Any change to the fail-closed preflight (blocker 1).
87 changes: 87 additions & 0 deletions devlog/_plan/260802_pr557_finalize/010_implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# PR #557 finalize — implementation (one cycle)

## Step order (dependency-ordered)

1. **Merge origin/dev into the branch** and resolve the 14 both-changed files
semantically: branch feature semantics on top of dev's evolution. Never drop
either side's behavior silently; every conflict hunk gets a named decision in
the commit message or this doc's amendment.
2. **Sanitizer fix (red-green first)** in `src/update/job.ts`
`sanitizeUpdateJobText`.
3. **Fail-closed policy A** in `recoverFailedGuiUpdate` + type/test updates.
4. **Docs**: manual recovery path (docs-site troubleshooting, EN; locale files
must not contradict).
5. **Gates + push + PR update** (body, attribution, undraft).

## Step 2 — sanitizer (anchor-based, space-tolerant)

Rewrite rules in order:

- npm/cache paths: `(?:[A-Za-z]:[\\/]|\/)[^"'<>\n]*?(?:\.npm|_cacache|_npx|\.opencodex-)`
with prefix allowing spaces (non-greedy up to first anchor), then consume
space-free trailing segments `([\\/][^\s"'<>]+)*` → `[redacted npm path]`.
npm cache internals have no spaces; the space risk lives in the profile prefix.
- ocx.mjs launcher paths: prefix allows spaces → `ocx.mjs` (keep `\b`, do not
swallow trailing args).
- user-path fallback (no anchor): redact the username component including ONE
optional space segment: `(\/(?:Users|home)\/)[^/\s"'<>]+(?:\s+[^/\s"'<>]+)?`
→ `$1[redacted]`. Anchor rule runs first, so `.npm`-carrying cases never reach
this rule half-matched.
- uid/gid rules unchanged.

Red-green tests (port Wibias's exact inputs):

- `C:\Users\Alice Smith\.npm\_cacache\tmp\entry` → `[redacted npm path]`
- `C:\Users\AliceSmith\.npm\_cacache\tmp\entry` → `[redacted npm path]`
- `/Users/Alice Smith/.npm/_cacache/tmp` → `[redacted npm path]` (whole)
- `/home/alice/.npm/_cacache/tmp` → `[redacted npm path]`
- `/Users/Alice Smith/some/other.log` → `/Users/[redacted]/some/other.log`
- uid/gid lines unchanged behavior.
Comment on lines +26 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
file='devlog/_plan/260802_pr557_finalize/010_implementation.md'
printf '%s\n' '--- target file ---'
cat -n "$file" | sed -n '1,90p'
printf '%s\n' '--- related path-redaction references ---'
rg -n -C 3 'Users|home|redact|redaction|npm|recovery|automatic' --glob '!node_modules' .

Repository: lidge-jun/opencodex

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -eu
file='devlog/_plan/260802_pr557_finalize/010_implementation.md'
printf '%s\n' '--- target lines 1-90 ---'
cat -n "$file" | sed -n '1,90p'
printf '%s\n' '--- implementation and test files containing redactUserPath ---'
rg -l 'redactUserPath|redactUser|redactSecretString' --glob '!devlog/**' --glob '!node_modules/**' | sort

Repository: lidge-jun/opencodex

Length of output: 5300


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- sanitizer definitions and call sites ---'
rg -n -C 5 'sanitizeUpdateJobText|redact|npm|ocx\.mjs' src/update tests --glob '*.ts' --glob '*.js' --glob '*.mjs' 2>/dev/null | head -n 300
printf '%s\n' '--- recovery symbols ---'
rg -n -C 3 'recoverFailedGuiUpdate|findNpmRecoveryLaunchers|recoveryLaunchersFn|FailedUpdateRecovery|recoverFailedGuiUpdateForTests' src tests --glob '*.ts' --glob '*.js' --glob '*.mjs' 2>/dev/null | head -n 300

Repository: lidge-jun/opencodex

Length of output: 26009


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- sanitizer tests ---'
rg -n -C 8 'redacted npm path|Alice Smith|AliceSmith|sanitizeUpdateJobText|some/other\.log|Users/' tests src --glob '*.ts' --glob '*.js' --glob '*.mjs' | head -n 400
printf '%s\n' '--- sanitizer implementation ---'
cat -n src/update/job.ts | sed -n '260,290p'
printf '%s\n' '--- standalone current-regex probe ---'
node - <<'JS'
const sanitize = (value) => value
  .replace(/(?:[A-Za-z]:[\\/]|\/)[^"'<>\n]*?ocx\.mjs\b/g, "ocx.mjs")
  .replace(
    /(?:(?:[A-Za-z]:[\\/]|\/)(?:Users|home)[\\/][^\\/]+[\\/]|(?:[A-Za-z]:[\\/]|\/))[^\s"'<>]*?(?:\.npm|_cacache|_npx|\.opencodex-)(?:[\\/][^\s"'<>]+)*/g,
    "[redacted npm path]",
  )
  .replace(/(\/(?:Users|home)\/)[^/\s"'<>]+(?:\s+[^/\s"'<>]+)?/g, "$1[redacted]")
  .replace(/\b(uid|gid)\s*[:=]\s*\d+\b/gi, "$1=[redacted]")
  .replace(/\b(UID|GID)\s+\d+\b/g, "$1 [redacted]");

const proposed = /((?:[A-Za-z]:[\\/]|\/)(?:Users|home)[\\/])[^\/\\\s"'<>]+(?:\s+[^\/\\\s"'<>]+)*/g;
const inputs = [
  String.raw`C:\Users\Alice Smith\Desktop\error.log`,
  String.raw`C:\Users\Alice Smith\.npm\_cacache\tmp\entry`,
  `/Users/Alice Mary Jane/some/other.log`,
  `/Users/Alice Smith/some/other.log`,
  `/home/alice/.npm/_cacache/tmp`,
];
for (const input of inputs) {
  console.log(JSON.stringify({input, current: sanitize(input), proposed: input.replace(proposed, "$1[redacted]")}));
}
JS

Repository: lidge-jun/opencodex

Length of output: 33625


🏁 Script executed:

#!/bin/bash
set -eu
node - <<'JS'
const sanitize = (value) => value
  .replace(/(?:[A-Za-z]:[\\/]|\/)[^"'<>\n]*?ocx\.mjs\b/g, "ocx.mjs")
  .replace(
    /(?:(?:[A-Za-z]:[\\/]|\/)(?:Users|home)[\\/][^\\/]+[\\/]|(?:[A-Za-z]:[\\/]|\/))[^\s"'<>]*?(?:\.npm|_cacache|_npx|\.opencodex-)(?:[\\/][^\s"'<>]+)*/g,
    "[redacted npm path]",
  )
  .replace(
    /((?:[A-Za-z]:[\\/]|\/)(?:Users|home)[\\/])[^\/\\\s"'<>]+(?:\s+[^\/\\\s"'<>]+)*/g,
    "$1[redacted]",
  )
  .replace(/\b(uid|gid)\s*[:=]\s*\d+\b/gi, "$1=[redacted]")
  .replace(/\b(UID|GID)\s+\d+\b/g, "$1 [redacted]");

const cases = new Map([
  [String.raw`C:\Users\Alice Smith\Desktop\error.log`, String.raw`C:\Users\[redacted]\Desktop\error.log`],
  [String.raw`C:\Users\Alice Smith\.npm\_cacache\tmp\entry`, "[redacted npm path]"],
  [String.raw`C:\Users\AliceSmith\.npm\_cacache\tmp\entry`, "[redacted npm path]"],
  ["/Users/Alice Smith/.npm/_cacache/tmp", "[redacted npm path]"],
  ["/home/alice/.npm/_cacache/tmp", "[redacted npm path]"],
  ["/Users/Alice Mary Jane/some/other.log", "/Users/[redacted]/some/other.log"],
  ["/Users/Alice Smith/some/other.log", "/Users/[redacted]/some/other.log"],
]);
for (const [input, expected] of cases) {
  const actual = sanitize(input);
  if (actual !== expected) throw new Error(`${input}\nexpected: ${expected}\nactual:   ${actual}`);
  console.log(`ok: ${input} -> ${actual}`);
}
JS

Repository: lidge-jun/opencodex

Length of output: 657


Make fallback path redaction platform-complete and space-tolerant. In devlog/_plan/260802_pr557_finalize/010_implementation.md:26-29, use both separators and consume all username segments up to the next separator: ((?:[A-Za-z]:[\\/]|\/)(?:Users|home)[\\/])[^\/\\\s"'<>]+(?:\s+[^\/\\\s"'<>]+)*. Add regressions for C:\Users\Alice Smith\Desktop\error.log and /Users/Alice Mary Jane/some/other.log.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devlog/_plan/260802_pr557_finalize/010_implementation.md` around lines 26 -
39, Update the user-path fallback redaction rule in the implementation plan to
support both slash types, optional drive prefixes, and all space-separated
username segments until the next separator; retain the existing replacement
behavior. Add regression cases for `C:\Users\Alice Smith\Desktop\error.log` and
`/Users/Alice Mary Jane/some/other.log`, while preserving the existing anchor,
npm-path, and uid/gid behavior.


## Step 3 — fail-closed (policy A)

In `recoverFailedGuiUpdate` (`src/update/job.ts:908`):

- KEEP steps 1-3 (probe-healthy, oldPid-identity, replacement-healthy) — pure
observation/refusal, no process start.
- REPLACE step 4 (launcher discovery + restart loop + candidate kill) with:
log `Update command failed after the proxy stopped. Automatic recovery is
disabled: the failed installer may still be mutating the global package tree.
Restore the package, then run 'ocx service install' or 'ocx start --port N'.`
and return `"failed"`.
- REMOVE the now-dead recovery machinery ONLY where nothing else references it
(map callers first): `findNpmRecoveryLaunchers`, validated-launcher helpers,
`recoveryLaunchersFn` io hook, `FailedUpdateRecovery = "restarted"` variant,
`recoverFailedGuiUpdateForTests` recovery-candidate cases. KEEP
`packageLauncherPath` / `restartAfterUpdate` (success path) and
`npm-cache-preflight.mjs` / `recovery-tree-scan.mjs` (preflight path).
- Update branch tests to fail-closed expectations: recovery-restart tests become
"job failed, no start attempted, manual instruction logged" tests; the
step 1-3 observation tests stay.
Comment on lines +47 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 \
  'recoverFailedGuiUpdate|installerFailureAllowsRecovery|findNpmRecoveryLaunchers' \
  src/update/job.ts tests

rg -n -C 5 \
  'no automatic recovery|автоматическое восстановление|自動復旧|자동 복구' \
  docs-site/src/content/docs

Repository: lidge-jun/opencodex

Length of output: 31216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runtime recovery symbols and call sites ---'
rg -n -C 8 \
  'type FailedUpdateRecovery|FailedUpdateRecovery|findNpmRecovery|recoveryLaunch|recoverFailedGuiUpdate|installerFailureAllowsRecovery|restartAfterUpdate' \
  src/update/job.ts src tests

printf '%s\n' '--- plan and ADR ---'
sed -n '15,35p' devlog/_plan/260802_pr557_finalize/000_plan.md
sed -n '35,65p' devlog/_plan/260802_pr557_finalize/010_implementation.md
sed -n '35,60p' docs/adr/0001-gui-update-worker.md

printf '%s\n' '--- lifecycle wording ---'
sed -n '308,325p' docs-site/src/content/docs/reference/cli/lifecycle.md
sed -n '195,210p' docs-site/src/content/docs/ja/reference/cli/lifecycle.md
sed -n '260,275p' docs-site/src/content/docs/ko/reference/cli/lifecycle.md
sed -n '282,296p' docs-site/src/content/docs/ru/reference/cli/lifecycle.md

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact obsolete recovery references ---'
rg -n \
  'findNpmRecoveryLauncher|findNpmRecoveryLaunchers|recoveryLaunchersFn|recoveryLauncher|installerFailureAllowsRecovery|mayRecover|recoverFailedGuiUpdateForTests|FailedUpdateRecovery' \
  src/update/job.ts tests/update-job.test.ts tests/update-stop-first.test.ts

printf '%s\n' '--- failed-update implementation ---'
sed -n '1328,1372p' src/update/job.ts
sed -n '1638,1665p' src/update/job.ts

printf '%s\n' '--- plan and implementation instructions ---'
sed -n '20,30p' devlog/_plan/260802_pr557_finalize/000_plan.md
sed -n '45,62p' devlog/_plan/260802_pr557_finalize/010_implementation.md

printf '%s\n' '--- ADR recovery policy ---'
sed -n '40,58p' docs/adr/0001-gui-update-worker.md

printf '%s\n' '--- focused source/test symbol counts ---'
python3 - <<'PY'
from pathlib import Path
terms = [
    "findNpmRecoveryLauncher",
    "findNpmRecoveryLaunchers",
    "recoveryLaunchersFn",
    "recoveryLauncher",
    "installerFailureAllowsRecovery",
    "mayRecover",
    "recoverFailedGuiUpdateForTests",
]
for path in [
    Path("src/update/job.ts"),
    Path("tests/update-job.test.ts"),
    Path("tests/update-stop-first.test.ts"),
]:
    text = path.read_text()
    print(path)
    for term in terms:
        count = text.count(term)
        if count:
            print(f"  {term}: {count}")
PY

Repository: lidge-jun/opencodex

Length of output: 10420


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/update/job.ts").read_text()
test = Path("tests/update-job.test.ts").read_text()

name = "findNpmRecoveryLauncher"
source_defs = re.findall(
    rf'^\s*(?:export\s+)?(?:async\s+)?function\s+{name}\b|^\s*export\s+(?:const|let|var)\s+{name}\b',
    source,
    re.MULTILINE,
)
imports = re.findall(rf'\b{name}\b', test)

print(f"src/update/job.ts definitions of {name}: {len(source_defs)}")
print(f"tests/update-job.test.ts references to {name}: {len(imports)}")
print("import block:")
for i, line in enumerate(test.splitlines()[:25], 1):
    print(f"{i}: {line}")
PY

printf '%s\n' '--- all repository references ---'
rg -n 'findNpmRecoveryLauncher|findNpmRecoveryLaunchers|recoveryLaunchersFn|recoveryLauncher' .

Repository: lidge-jun/opencodex

Length of output: 2031


Remove the stale recovery import and update the ADR.

src/update/job.ts:1335-1361 already performs observation only; it does not launch a recovery candidate. However, tests/update-job.test.ts:9 imports findNpmRecoveryLauncher, which has no definition or export, so the test module cannot load. Remove that import and retain only fail-closed observation tests.

docs/adr/0001-gui-update-worker.md:42-53 still documents candidate inspection, restart, and direct launcher execution. Replace those claims with the fail-closed policy. The plans and lifecycle pages already state the correct behavior.

📍 Affects 7 files
  • devlog/_plan/260802_pr557_finalize/010_implementation.md#L47-L60 (this comment)
  • devlog/_plan/260802_pr557_finalize/000_plan.md#L22-L26
  • docs/adr/0001-gui-update-worker.md#L42-L53
  • docs-site/src/content/docs/reference/cli/lifecycle.md#L315-L320
  • docs-site/src/content/docs/ja/reference/cli/lifecycle.md#L202-L205
  • docs-site/src/content/docs/ko/reference/cli/lifecycle.md#L266-L269
  • docs-site/src/content/docs/ru/reference/cli/lifecycle.md#L287-L291
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devlog/_plan/260802_pr557_finalize/010_implementation.md` around lines 47 -
60, Remove the undefined findNpmRecoveryLauncher import from
tests/update-job.test.ts and retain only fail-closed observation tests. Update
docs/adr/0001-gui-update-worker.md:42-53 to document observation-only,
fail-closed behavior with manual recovery instructions, removing candidate
inspection, restart, and direct launcher execution claims.
devlog/_plan/260802_pr557_finalize/010_implementation.md:47-60 and
000_plan.md:22-26 already describe the intended policy and require no direct
change; the lifecycle documentation at
docs-site/src/content/docs/reference/cli/lifecycle.md:315-320,
docs-site/src/content/docs/ja/reference/cli/lifecycle.md:202-205,
docs-site/src/content/docs/ko/reference/cli/lifecycle.md:266-269, and
docs-site/src/content/docs/ru/reference/cli/lifecycle.md:287-291 likewise
requires no direct change.

Source: Path instructions


## Step 4 — docs

- docs-site troubleshooting (EN): "update failed, proxy is down" → the fail-closed
rationale (one line) + manual path: verify/reinstall package, then
`ocx service install` or `ocx start --port N`; job log names the exact command.
- Check zh-cn/ja/ru/ko troubleshooting pages for auto-recovery claims; amend any
contradiction (the branch already touches docs-site cli reference in 2 locales —
keep consistent).

## Activation scenarios (C)

1. Red: Wibias's four sanitizer inputs leak before the fix (assert exact current
bad outputs), pass after.
2. Fail-closed: a GUI npm update whose installer exits nonzero after a proxy stop
results in job.status failed, zero calls to the restart io hooks, and the
manual instruction in the sanitized job log. Activation: io-spy test.
3. Observation preserved: replacement-becomes-healthy still yields "still-running"
with no restart. Activation: existing step 1-3 tests green.
4. Full `bun run test`, `bun run typecheck`, `bun run privacy:scan` on the merged
branch.

## Push plan (user-approved for this PR)

Regular `git push` of the merge+fix commits (no force). Then `gh pr ready 557`,
PR body update (policy decision + evidence + `Refs #533` + @WZBbiao credit), and
a summary comment. If the remote rejects non-FF → STOP and ask (UNSAFE).
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/ja/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ Windows ステータス トレイ アイコンをインストールして制御

npm から opencodex を自己更新します。安定したインストールでは `@latest` を使用します。 `--tag latest|preview` を渡さない限り、プレビュー インストールは `@preview` に残ります。ソース チェックアウトを検出し、代わりに `git pull && bun install` を使用するように指示しますが、そのタグの最新バージョンをすでに使用している場合は何もしません。実行中のプロキシは、ファイルが置き換えられる前に停止されます。インストールされたサービスは再構築されて自動的に開始されますが、フォアグラウンド インストールでは次のステップとして `ocx start` が出力されます。

Unix では、更新前に設定済み npm キャッシュが現在のユーザー所有か確認します。所有者の異なる
エントリが見つかった場合やキャッシュを検査できない場合は、実行中のプロキシを停止する前に更新を中止します。

プロキシ停止後に更新コマンド自体が失敗した場合、自動復旧は行いません。失敗したインストーラがグローバルパッケージツリーをまだ変更中である可能性があるためです。更新ジョブは失敗のままとなり、ログに手動の復旧手順(パッケージを復元してから `ocx service install` または `ocx start --port <port>`)が記録されます。

```bash
ocx update
ocx update --tag preview
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/ko/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ npm에서 opencodex를 자체 업데이트합니다. 안정판 설치는 `@lates
않습니다. 실행 중인 프록시가 있으면 파일을 교체하기 전에 중지합니다. 설치된 서비스는 자동으로 다시
빌드해 시작하며, 포그라운드 설치에서는 다음 단계로 `ocx start`를 출력합니다.

Unix에서는 업데이트 전에 설정된 npm 캐시가 현재 사용자 소유인지 확인합니다. 다른 소유자의 항목이
있거나 캐시를 검사할 수 없으면 실행 중인 프록시를 중지하기 전에 업데이트를 중단합니다.

프록시가 중지된 뒤 업데이트 명령 자체가 실패하면 자동 복구를 시도하지 않습니다. 실패한 설치 프로그램이 전역 패키지 트리를 아직 바꾸는 중일 수 있기 때문입니다. 업데이트 작업은 실패로 남고, 로그에 수동 복구 경로(패키지를 복원한 뒤 `ocx service install` 또는 `ocx start --port <port>`)가 안납니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the Korean recovery-log sentence

For Korean readers, 로그에 ... 경로 ... 가 안납니다 says that the manual recovery path does not appear in the log, which is the opposite of the English source and the implemented behavior. This is likely a typo for 안내합니다 or 기록됩니다; correct it so users whose proxy is down know where to find the recovery command.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Korean manual-recovery sentence.

경로가 안납니다 does not correctly state that the log records or provides the manual recovery path. Use wording such as 로그에 수동 복구 경로가 기록됩니다.

As per path instructions, translated docs-site/** pages must remain clear and consistent with the English source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/ko/reference/cli/lifecycle.md` at line 269, Update
the Korean manual-recovery sentence in the lifecycle documentation so it clearly
states that the manual recovery path is recorded in the log, replacing the
incorrect “경로가 안납니다” wording while preserving the listed recovery commands and
consistency with the English source.

Source: Path instructions


```bash
ocx update
ocx update --tag preview
Expand Down
7 changes: 6 additions & 1 deletion docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,12 @@ Self-update opencodex from npm. Stable installs use `@latest`; preview installs
unless you pass `--tag latest|preview`. It detects a source checkout and tells you to
`git pull && bun install` instead, and is a no-op if you are already on the newest version for that
tag. A running proxy is stopped before files are replaced; an installed service is rebuilt and
started automatically, while a foreground installation prints `ocx start` as the next step.
started automatically, while a foreground installation prints `ocx start` as the next step. On
Unix, the updater first checks that the configured npm cache is owned by the current user. It aborts
before stopping the proxy when it finds a foreign-owned cache entry or cannot inspect the cache, so
you can correct the cache ownership or configure a user-owned cache and retry without losing the
running service.
If the update command itself fails after the proxy was stopped, no automatic recovery is attempted — the failed installer may still be mutating the global package tree. The update job stays failed and its log names the manual path: restore the package, then run `ocx service install` or `ocx start --port <port>`.

```bash
ocx update
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/ru/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ source checkout и предлагает вместо этого `git pull && bun
останавливается; установленная служба автоматически пересобирается и запускается заново, а для
foreground-установки печатается подсказка `ocx start`.

В Unix перед обновлением проверяется, что настроенный кеш npm принадлежит текущему пользователю.
Если найден элемент с другим владельцем или кеш невозможно проверить, обновление прерывается до остановки прокси.

Если после остановки прокси сама команда обновления завершилась с ошибкой, автоматическое восстановление не выполняется — завершившийся сбоем установщик может ещё изменять глобальное дерево пакетов. Задание обновления остаётся failed, а его лог указывает ручной путь: восстановите пакет, затем выполните `ocx service install` или `ocx start --port <port>`.

```bash
ocx update
ocx update --tag preview
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ ocx codex-shim uninstall

从 npm 自更新 opencodex。稳定版安装使用 `@latest`;预览版安装保持在 `@preview`,除非你传入 `--tag latest|preview`。它会检测源码检出,并提示你改为运行 `git pull && bun install`;如果你已经是该标签的最新版本,则不会执行任何操作。在替换文件之前会先停止正在运行的代理;已安装的服务会自动重建并启动,而前台安装则会打印 `ocx start` 作为下一步。

在 Unix 上,更新器会先检查配置的 npm 缓存是否全部归当前用户所有。如果发现其他用户拥有的条目,
或无法检查缓存,更新会在停止代理前中止。

如果代理停止后更新命令本身失败,则不会尝试自动恢复——失败的安装程序可能仍在修改全局包树。更新任务保持失败状态,其日志会指明手动恢复路径:恢复软件包,然后运行 `ocx service install` 或 `ocx start --port <port>`。

```bash
ocx update
ocx update --tag preview
Expand Down
Loading
Loading