Skip to content

fix(lifecycle): bind in-place proxy restart to runtime identity - #1131

Draft
luvs01 wants to merge 1 commit into
lidge-jun:devfrom
luvs01:agent/fix-bound-in-place-restart
Draft

fix(lifecycle): bind in-place proxy restart to runtime identity#1131
luvs01 wants to merge 1 commit into
lidge-jun:devfrom
luvs01:agent/fix-bound-in-place-restart

Conversation

@luvs01

@luvs01 luvs01 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Make both ocx restart and the Windows tray use the running proxy's existing POST /api/system/restart lifecycle instead of a separate stop-then-start transaction.
  • Attest the exact live runtime PID and port, derive a one-use process-scoped HMAC capability bound to the method, route, PID, and port, and never send the reusable management token.
  • Reject invalid or stale target identities before scheduling restart, share one absolute deadline across attestation/request/replacement observation, and report success only after a different runtime PID is healthy on the same port.
  • Treat a lost restart response as uncertain: observe the original identity for replacement and never replay it as a destructive fallback stop/start.
  • Preserve service supervision, managed Codex/Grok routing, and tray ownership throughout the restart; extend the tray watchdog to cover the documented drain and handoff budget.
  • Update lifecycle, Grok, and Windows memory documentation in all maintained locales, plus the management architecture invariant.

This builds on the restart surface introduced by #580 / #594. It is not a duplicate of #720 / #737 (readiness boundary) or #733 / #752 (tray socket inheritance).

Verification

  • Final head, Bun 1.4.0-canary.1: 111 focused restart/auth/tray/lifecycle tests passed, 0 failed.
  • Final head, Bun 1.3.14: the same 111 focused tests passed, 0 failed.
  • An additional 71-test launcher/provenance/tray regression set passed on both Bun runtimes.
  • A broader 208-test affected-surface run passed 207 tests; its one existing timing-sensitive test passed when rerun alone.
  • bun run typecheck passed on both Bun runtimes.
  • bun run privacy:scan, PowerShell AST parsing, and git diff --check passed.
  • cd docs-site && bun run build passed, 221 pages generated.
  • Independent review found no remaining P0/P1 issue after addressing the current Codex and CodeRabbit findings.
  • Live Windows validation on two OpenCodex 2.10.2 hosts confirmed exact PID replacement on port 10100, old PID exit, Canary runtime preservation, service recovery, and intact main/pool account state.
  • A repository-wide Windows run is not claimed green: the pre-termination failure was an existing api-usage cache test and reproduced unchanged on the latest dev worktree. The affected restart surface remains green in the focused runs above.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

This changes the management-auth boundary. Explicit maintainer security review and sponsorship are still required before merge.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • New Features
    • ocx restart now performs an in-place proxy restart while preserving managed routing and service supervision.
    • Restart success requires a healthy, identity-verified replacement process on the same port.
    • The dashboard’s Drain & restart flow preserves Codex injection during replacement.
    • If no proxy is running, restart falls back to the standard startup flow.
  • Documentation
    • Updated lifecycle, service-management, and troubleshooting guidance across supported languages.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces stop-then-start proxy restarts with attested in-place replacement. It adds PID-bound HMAC authorization, bounded restart observation, same-port replacement checks, CLI and tray integration, tests, and multilingual documentation updates.

Changes

Proxy restart lifecycle

Layer / File(s) Summary
Restart authorization and endpoint validation
src/lib/system-restart-contract.ts, src/server/management-auth.ts, src/server/management/system-routes.ts, src/server/index.ts, tests/server-management-auth.test.ts, tests/system-restart.test.ts, structure/05_gui-and-management-api.md
Defines PID-bound HMAC capabilities, authenticates local restart requests, validates expected PIDs, and covers accepted, malformed, stale, tampered, and misplaced requests.
Bound restart request and replacement coordination
src/cli/system-restart-client.ts, src/cli/tray-proxy.ts, tests/system-restart-client.test.ts, tests/tray-proxy.test.ts
Attests the live proxy, submits bounded restart requests, distinguishes definite and uncertain failures, supports fallback startup, and verifies a healthy replacement PID on the same port.
CLI, tray, and shared-state integration
src/cli/index.ts, src/tray/windows-tray.ps1, tests/grok-lifecycle.test.ts, tests/windows-tray.test.ts
Routes CLI and tray restarts through the shared coordinator, returns lifecycle status through booleans and exit codes, restores shared state after stop, and extends tray observation timeouts.
Restart behavior documentation
docs-site/src/content/docs/**/guides/grok-build.md, docs-site/src/content/docs/**/reference/cli/lifecycle.md, docs-site/src/content/docs/**/troubleshooting/windows-memory.md
Updates localized documentation for in-place restart, service supervision retention, same-port PID replacement, and ensure fallback.
Estimated code review effort: 4 (Complex) ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ProxyRestartCoordinator
  participant ManagementAPI
  participant ReplacementProxy
  CLI->>ProxyRestartCoordinator: restart live proxy
  ProxyRestartCoordinator->>ManagementAPI: attest and submit PID-bound capability
  ManagementAPI->>ReplacementProxy: validate target and schedule drain
  ProxyRestartCoordinator->>ReplacementProxy: observe healthy new PID on same port
  ReplacementProxy-->>CLI: replacement readiness
Loading

Possibly related PRs

Suggested reviewers: lidge-jun, wibias, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes binding in-place proxy restart to the running runtime identity, which is the main change.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

⚠️ Deterministic hygiene checks failed.

  • empty_catch — An empty catch block was added. Handle, report, or deliberately propagate the error. Paths: src/tray/windows-tray.ps1.
  • unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/server/management-auth.ts.

@github-actions github-actions Bot added the bug Something isn't working label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@luvs01

luvs01 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@luvs01

luvs01 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e45eb7f2c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +124 to +130
const response = await fetchImpl(`${baseUrl}${SYSTEM_RESTART_PATH}`, {
method: SYSTEM_RESTART_METHOD,
headers: {
[SYSTEM_RESTART_EXPECTED_PID_HEADER]: String(target.pid),
[SYSTEM_RESTART_NONCE_HEADER]: challenge,
[SYSTEM_RESTART_CAPABILITY_HEADER]: capability,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve restart compatibility with a pre-update proxy

When the new CLI is installed while a proxy from the previous release is still running, this POST sends only the newly introduced capability headers. The previous server's requireManagementAuth does not recognize those headers and requires the management credential for every /api/* request, so it returns 401; line 133 treats that as a definite rejection, and both ocx restart and the tray refuse to observe or fall back, leaving users unable to restart the old process into the new version. Negotiate capability support or provide a safe compatibility path for an already-attested older runtime.

Useful? React with 👍 / 👎.

Comment thread src/cli/index.ts Outdated
else console.error("↩️ Restart aborted: the proxy was not stopped cleanly.");
// The running proxy owns its drain and replacement through /api/system/restart.
// If nothing is live, restart degrades to the documented `ensure` start behavior.
await handleProxyRestart(handleEnsure);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid treating ensure-spawned proxies as supervised

On Windows with the Task Scheduler backend, ocx stop ends the task but leaves its enabled registration installed, and this stopped-proxy fallback invokes handleEnsure, which starts a detached proxy with OCX_SERVICE=1. On the next ocx restart, isSupervisedServiceChild sees that marker and isServiceViable() reports an enabled scheduler registration as viable even though the direct process is not owned by a running task, so the restart path exits with code 1 instead of spawning a replacement; no supervisor observes that exit, leaving the proxy down and managed routing pointed at it. Start the installed service here or persist enough runtime ownership information to distinguish an actual supervised child from an ensure-spawned process.

Useful? React with 👍 / 👎.

Comment thread src/tray/windows-tray.ps1 Outdated
# handing off to an identity-verified replacement. The tray observes health/PID
# rather than the detached CLI exit, so keep a watchdog margin around that shared
# lifecycle budget. The CLI remains the lifecycle owner; the tray never kills.
Set-PendingAction "Restart Proxy" 160

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 Report detached tray restart failures before the full timeout

When __tray-restart fails immediately—for example because runtime attestation is missing, the target changed, or the POST is rejected—Start-OcxCommand has already disposed the child process and only reports whether it launched, while the old proxy remains online with the same PID. Consequently the pending predicate can never complete and this new 160-second deadline leaves the tray displaying “Restarting...” for nearly three minutes before surfacing the already-known failure. Retain and observe the detached command's exit status, or add an explicit completion/failure signal, while keeping the long watchdog only for commands that are still running.

Useful? React with 👍 / 👎.

Comment thread src/cli/index.ts Outdated
Comment on lines +654 to +655
if (!ownershipBlocked) {
const r = await restoreNativeCodexAsync();
if (r.success) console.log(`↩️ ${r.message}`);
else {
stopFailed = true;
console.error(`⚠️ ${r.message}`);
}
}
// revertSystemEnv is NOT gated: it carries its own ownership check and concerns launchctl
// user env, not CODEX_HOME. Safety net for when the daemon's syncCleanup didn't run (SIGKILL).
try { revertSystemEnv(); } catch { /* best-effort */ }
if (!ownershipBlocked) {
// Same safety net for the Grok Build managed block (marker-owned, idempotent).
try {
const g = stripGrokConfig();
if (g.changed) console.log(`↩️ ${g.message}`);
// A refused strip (e.g. orphaned marker) leaves the fence pointing at a dead proxy —
// reporting success there hides a broken end state.
else if (!g.ok) { stopFailed = true; console.error(`⚠️ ${g.message}`); }
} catch { /* best-effort */ }
if (!await restoreSharedStateAfterStop()) stopFailed = true;

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 Keep environment rollback outside the service-ownership gate

When stopServiceIfInstalled() reports an ownership mismatch, this gate now skips restoreSharedStateAfterStop() entirely, including revertSystemEnv(). Before this refactor, environment rollback deliberately ran outside the ownership gate because it has its own marker/ownership check and cleans the current home's launchctl variables and shell environment file independently of the foreign service's Codex/Grok state. On macOS, stopping the current proxy in this scenario can therefore leave ANTHROPIC_BASE_URL and related variables pointing at its dead port. Move revertSystemEnv() back outside this gate while continuing to gate native Codex and Grok teardown.

Useful? React with 👍 / 👎.

Comment thread src/cli/tray-proxy.ts Outdated
Comment on lines +101 to +105
if (!previous) {
try {
return await io.startWhenStopped()
? { ok: true, mode: "started" }
: { ok: false, phase: "start" };

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 Confirm absence before taking the restart start fallback

A single findLiveProxy() transport timeout returns null here and is treated as proof that no proxy exists. Both fallback implementations immediately probe again, so if the same running proxy answers that second probe, handleEnsure or runTrayProxyStart returns true and the coordinator reports a successful “started” restart even though the PID never changed and no restart occurred; if the second probe also lands during a restart handoff gap, it can instead spawn a competing start. Require stable absence or retain the runtime/PID candidate and verify that it is gone before selecting the start-only path.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs-site/src/content/docs/reference/cli/lifecycle.md`:
- Around line 39-42: Update the lifecycle descriptions to document that a live
proxy lacking an attested runtime PID or having a non-runtime source fails with
phase "identity" and does not fall back to ensure or perform stop/start: add
this behavior to docs-site/src/content/docs/reference/cli/lifecycle.md lines
39-42, docs-site/src/content/docs/ko/reference/cli/lifecycle.md lines 39-42 in
Korean, and docs-site/src/content/docs/ru/reference/cli/lifecycle.md lines 43-46
in Russian.

In `@src/cli/index.ts`:
- Around line 407-412: `handleEnsure` currently returns `false` when
`codexAutoStartEnabled(config)` is disabled, which makes `runProxyRestart` and
`reportRestartFailure` treat a deliberate skip as a failed start. Update the
restart flow so the `startWhenStopped` path can distinguish “autostart disabled”
from an actual startup failure, using `handleEnsure` and the
`runProxyRestart`/`reportRestartFailure` wiring. Preserve the existing
disabled-autostart message, but make `ocx restart` return success and keep
`process.exitCode` at 0 when the proxy was intentionally not started.
- Around line 575-579: Update the catch block surrounding stripGrokConfig in the
stop flow so any thrown failure sets restored = false before continuing.
Preserve the existing best-effort behavior and error handling for returned
!grok.ok results, ensuring both failure paths prevent ocx stop from reporting
successful restoration.
- Around line 490-503: Update the tray start action’s pending timeout in
Set-PendingAction for “Start Proxy” within the tray start flow to cover the
CLI’s 20-second probe plus its 40-second follow-up wait, using a budget
consistent with the existing extended restart timeout. Preserve the existing
polling and completion behavior.
- Line 54: Remove the top-level MEMORY_DRAIN_RESTART_MS and
REPLACEMENT_READY_TIMEOUT_MS import from src/cli/index.ts, and make
PROXY_RESTART_OBSERVE_MS obtain these values only within the handleProxyRestart
restart path. Prefer a dependency-free shared constants module if reuse is
required; otherwise derive the observation window locally while preserving
existing timing behavior.

In `@tests/server-management-auth.test.ts`:
- Around line 165-178: Extend the existing system-restart capability tests with
two HTTP-level negative cases: send a DELETE request to SYSTEM_RESTART_PATH
using otherwise valid restart headers and assert it is rejected, then create
valid capability headers bound to a different port and assert a request against
the live server is rejected. Keep these cases adjacent to the
tampered-capability assertion and preserve the existing valid-request
expectations.

In `@tests/system-restart-client.test.ts`:
- Around line 85-94: Add focused tests beside “rejects missing runtime proof
state before fetching” that provide runtime records with valid attestationSecret
but mismatched pid and mismatched port relative to target. Assert
requestBoundSystemRestart returns accepted false and fetchImpl is never called
for each case, covering both runtime identity guard comparisons.
- Around line 26-70: Consolidate the duplicated healthz response fixtures by
removing the inline response construction from successfulDeps and reusing
successfulDepsResponse instead. Update the single successfulDepsResponse builder
to include target.port, preserving the response shape served by
src/server/index.ts and keeping existing request behavior unchanged.

In `@tests/tray-proxy.test.ts`:
- Around line 124-190: Add focused regression tests near the existing
runProxyRestart tests for both thrown-error branches: make findLive reject and
assert { ok: false, phase: "request", error } with no request or fallback start,
then make waitForReplacement reject and assert { ok: false, phase:
"replacement", error }. Use call tracking where needed to verify fail-closed
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a0234fdd-4d17-40de-afde-0e03c7c9e1c2

📥 Commits

Reviewing files that changed from the base of the PR and between 43a1fdc and e45eb7f.

📒 Files selected for processing (30)
  • docs-site/src/content/docs/guides/grok-build.md
  • docs-site/src/content/docs/ja/guides/grok-build.md
  • docs-site/src/content/docs/ja/reference/cli/lifecycle.md
  • docs-site/src/content/docs/ja/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/ko/guides/grok-build.md
  • docs-site/src/content/docs/ko/reference/cli/lifecycle.md
  • docs-site/src/content/docs/ko/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/reference/cli/lifecycle.md
  • docs-site/src/content/docs/ru/guides/grok-build.md
  • docs-site/src/content/docs/ru/reference/cli/lifecycle.md
  • docs-site/src/content/docs/ru/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/zh-cn/guides/grok-build.md
  • docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
  • docs-site/src/content/docs/zh-cn/troubleshooting/windows-memory.md
  • src/cli/index.ts
  • src/cli/system-restart-client.ts
  • src/cli/tray-proxy.ts
  • src/lib/system-restart-contract.ts
  • src/server/index.ts
  • src/server/management-auth.ts
  • src/server/management/system-routes.ts
  • src/tray/windows-tray.ps1
  • structure/05_gui-and-management-api.md
  • tests/grok-lifecycle.test.ts
  • tests/server-management-auth.test.ts
  • tests/system-restart-client.test.ts
  • tests/system-restart.test.ts
  • tests/tray-proxy.test.ts
  • tests/windows-tray.test.ts

Comment on lines +39 to +42
When a proxy is running, ask that exact attested PID and port to restart in place, wait for its
normal drain, and verify a different runtime PID on the same port. Managed routing and service
supervision stay installed throughout; an uncertain request is observed rather than replayed as a
separate stop/start. If no proxy is running, the command falls back to the normal `ensure` start.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the rejected live-identity path in every lifecycle page.

A live proxy without a runtime PID or with a non-runtime source fails with phase: "identity"; it does not fall back to ensure. Add this behavior to all localized lifecycle descriptions.

  • docs-site/src/content/docs/reference/cli/lifecycle.md#L39-L42: state that restart fails without stop/start when the live proxy cannot be attested.
  • docs-site/src/content/docs/ko/reference/cli/lifecycle.md#L39-L42: add the same identity-failure behavior in Korean.
  • docs-site/src/content/docs/ru/reference/cli/lifecycle.md#L43-L46: add the same identity-failure behavior in Russian.
📍 Affects 3 files
  • docs-site/src/content/docs/reference/cli/lifecycle.md#L39-L42 (this comment)
  • docs-site/src/content/docs/ko/reference/cli/lifecycle.md#L39-L42
  • docs-site/src/content/docs/ru/reference/cli/lifecycle.md#L43-L46
🤖 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/reference/cli/lifecycle.md` around lines 39 - 42,
Update the lifecycle descriptions to document that a live proxy lacking an
attested runtime PID or having a non-runtime source fails with phase "identity"
and does not fall back to ensure or perform stop/start: add this behavior to
docs-site/src/content/docs/reference/cli/lifecycle.md lines 39-42,
docs-site/src/content/docs/ko/reference/cli/lifecycle.md lines 39-42 in Korean,
and docs-site/src/content/docs/ru/reference/cli/lifecycle.md lines 43-46 in
Russian.

Source: Path instructions

Comment thread src/cli/index.ts Outdated
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
import { initializeNodeLauncherContext } from "./launcher-context";
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../server/management/system-restart";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Measure the import cost and side effects pulled in by the new static CLI import.
set -euo pipefail

fd -t f 'system-restart.ts' src/server/management -x cat -n | head -80

echo "--- top-level imports of the restart module ---"
fd -t f 'system-restart.ts' src/server/management -x rg -n '^import|^export const|^const '

echo "--- top-level statements that are not declarations (possible side effects) ---"
fd -t f 'system-restart.ts' src/server/management -x rg -n '^(?!import|export|const|let|function|class|type|interface|//|/\*|\s|$)' -P

echo "--- how src/cli/index.ts imports other server modules ---"
rg -n 'await import\("\.\./server' src/cli/index.ts

Repository: lidge-jun/opencodex

Length of output: 5671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- src/cli/index.ts outline/import context ---"
sed -n '1,70p;330,480p;490,530p' src/cli/index.ts

echo "--- find usages of imported constants in cli ---"
rg -n 'MEMORY_DRAIN_RESTART_MS|REPLACEMENT_READY_TIMEOUT_MS|PROXY_RESTART_OBSERVE_MS' src/cli/index.ts src/server src/lib src/config src/service | head -120

echo "--- lifecycle imports at src/server/management/system-restart.ts top imports ---"
fd -t f 'lifecycle.ts' src -x sh -c 'echo "--- $1"; sed -n "1,80p" "$1"' sh {}

echo "--- service imports/top at src/service.ts ---"
fd -t f 'service.ts' src -x sh -c 'echo "--- $1"; sed -n "1,80p" "$1"' sh {}

echo "--- config imports/top at src/config.ts ---"
fd -t f 'config.ts' src -x sh -c 'echo "--- $1"; sed -n "1,80p" "$1"' sh {}

Repository: lidge-jun/opencodex

Length of output: 14970


🏁 Script executed:

#!/bin/bash
set -u

echo "--- constants and handleProxyRestart usage ---"
rg -n 'MEMORY_DRAIN_RESTART_MS|REPLACEMENT_READY_TIMEOUT_MS|PROXY_RESTART_OBSERVE_MS|function handleProxyRestart|async function handleProxyRestart' src || true

echo "--- handleProxyRestart context ---"
rg -n 'PROXY_RESTART_OBSERVE_MS|waitForProxyReplacement|reportRestartFailure' src/cli/index.ts || true
sed -n '500,565p' src/cli/index.ts

echo "--- restart module non-declaration top-level statements probe ---"
python3 - <<'PY'
from pathlib import Path
import re
root = Path('src/server/management/system-restart.ts')
text = root.read_text()
top = ''.join(text.splitlines(True)[:120])
print("length:", len(top), "lines:", top.count("\n")+1)
declarations = re.findall(r'(?m)^(/\*.*?\*/|//.*|import .*|export .*|const .*|let .*|function .*|class .*|interface .*|type .*|async function .*|export interface .*)$', top, re.S)
print("top declarations:", len(declarations))
for d in declarations[:80]:
    s=' '.join(d.split())
    if '<120' not in s: print(s[:140])
non_decl=[]
for i,line in enumerate(top.splitlines(),1):
    s=line.strip()
    if s.startswith(('import ','export ','const ','let ','function ','class ','type ','interface ','async function ','//','/*','')):
        continue
    non_decl.append((i,line))
print("non-declaration first 40 lines:")
for i,line in non_decl[:40]:
    print(f"{i}: {line}")
PY

Repository: lidge-jun/opencodex

Length of output: 4663


Avoid the top-level CLI import for handleProxyRestart’s constants.

src/cli/index.ts imports MEMORY_DRAIN_RESTART_MS and REPLACEMENT_READY_TIMEOUT_MS at the module top, but only PROXY_RESTART_OBSERVE_MS needs them, inside the restart handling path. This top-level import forces the whole src/server/management/system-restart graph to load before any CLI command dispatch. Move these constants to a small, dependency-free shared module, or derive the observed restart window inside the handleProxyRestart code path.

🤖 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 `@src/cli/index.ts` at line 54, Remove the top-level MEMORY_DRAIN_RESTART_MS
and REPLACEMENT_READY_TIMEOUT_MS import from src/cli/index.ts, and make
PROXY_RESTART_OBSERVE_MS obtain these values only within the handleProxyRestart
restart path. Prefer a dependency-free shared constants module if reuse is
required; otherwise derive the observation window locally while preserving
existing timing behavior.

Source: Path instructions

Comment thread src/cli/index.ts Outdated
Comment on lines +407 to +412
async function handleEnsure(): Promise<boolean> {
if (!currentExternalCodexModelProvider()) reconcileJournal();
const config = loadConfig();
if (!codexAutoStartEnabled(config)) {
console.log("Codex autostart is disabled.");
return;
return false;

Copy link
Copy Markdown
Contributor

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

ocx restart now fails with exit code 1 when Codex autostart is disabled.

Line 412 returns false when codexAutoStartEnabled(config) is false. Line 1162 passes handleEnsure as the startWhenStopped callback. Trace the disabled-autostart path with no live proxy:

  1. runProxyRestart finds no live proxy and calls startWhenStopped.
  2. handleEnsure prints "Codex autostart is disabled." and returns false.
  3. runProxyRestart returns { ok: false, phase: "start" }.
  4. reportRestartFailure prints "❌ Proxy was not running and the fallback start did not become healthy." (line 533).
  5. Line 550 sets process.exitCode = 1.

Nothing failed. The operator deliberately disabled autostart. The command now emits a false error and a nonzero exit code, which breaks scripts and CI wrappers that treat ocx restart exit status as health.

Distinguish "deliberately not started" from "start attempt failed". One option is a tri-state return from the fallback so the coordinator can report a skip.

🐛 Proposed fix sketch
-async function handleEnsure(): Promise<boolean> {
+/** Returns false only for a real failure; "skipped" means autostart is off by choice. */
+async function handleEnsure(): Promise<boolean | "skipped"> {
   if (!currentExternalCodexModelProvider()) reconcileJournal();
   const config = loadConfig();
   if (!codexAutoStartEnabled(config)) {
     console.log("Codex autostart is disabled.");
-    return false;
+    return "skipped";
   }

Then have the restart wiring treat "skipped" as success without printing a start failure, and keep process.exitCode at 0.

🤖 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 `@src/cli/index.ts` around lines 407 - 412, `handleEnsure` currently returns
`false` when `codexAutoStartEnabled(config)` is disabled, which makes
`runProxyRestart` and `reportRestartFailure` treat a deliberate skip as a failed
start. Update the restart flow so the `startWhenStopped` path can distinguish
“autostart disabled” from an actual startup failure, using `handleEnsure` and
the `runProxyRestart`/`reportRestartFailure` wiring. Preserve the existing
disabled-autostart message, but make `ocx restart` return success and keep
`process.exitCode` at 0 when the proxy was intentionally not started.

Comment thread src/cli/index.ts
Comment on lines +490 to 503
// serviceCommand("start") already spends up to 20s confirming the supervised
// child. Slow Windows hosts can still be publishing native-main state after that
// first window, so keep one shared follow-up budget instead of returning a false
// failure while Task Scheduler is still starting the approved child.
waitForProxy: () => waitForProxy(40_000),
info: message => console.log(message),
error: message => console.error(message),
});
if (!ok) process.exitCode = 1;
// serviceCommand("start") can set exitCode=1 after its own 20s probe, while
// the coordinator's bounded follow-up observes the same service become live.
// The final observed state, not the earlier probe, owns this command result.
process.exitCode = ok ? 0 : 1;
return ok;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The tray start watchdog is now shorter than the CLI start wait, so a slow start reports a false failure.

Line 494 raises the CLI follow-up budget to waitForProxy(40_000). The comment explains that serviceCommand("start") already spent up to 20 seconds, so the CLI can now observe a start for roughly 60 seconds total.

The tray side was not extended to match. src/tray/windows-tray.ps1 line 248 still uses Set-PendingAction "Start Proxy" 15. The tray polls health every 3 seconds and calls Complete-PendingAction $false once pendingDeadline passes (line 241).

Failure mode on a slow Windows host:

  1. The operator clicks "Start Proxy". The tray dispatches __tray-start and arms a 15-second watchdog.
  2. At 15 seconds the proxy is not yet healthy. The tray logs "Start Proxy failed to reach the expected state" and shows an error balloon.
  3. The CLI keeps waiting and the proxy becomes healthy at, for example, 35 seconds. process.exitCode is 0.

The operator sees a failure notification for a start that succeeded. This PR extended the restart budget to 160 seconds (line 262) for exactly this reason but left the start budget at the old value.

Raise the tray start budget to cover the CLI's own start observation window.

🐛 Proposed fix in `src/tray/windows-tray.ps1`
 $startItem.add_Click({
   $statusItem.Text = "Proxy: Starting..."
-  Set-PendingAction "Start Proxy" 15
+  # serviceCommand("start") spends up to 20s, then handleTrayProxyStart waits a further
+  # 40s (src/cli/index.ts waitForProxy(40_000)). Keep a margin around that budget so the
+  # tray never reports a failure for a start the CLI is still confirming.
+  Set-PendingAction "Start Proxy" 75
   if (-not (Start-OcxCommand @("__tray-start"))) { $script:pendingAction = $null }
 })
🤖 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 `@src/cli/index.ts` around lines 490 - 503, Update the tray start action’s
pending timeout in Set-PendingAction for “Start Proxy” within the tray start
flow to cover the CLI’s 20-second probe plus its 40-second follow-up wait, using
a budget consistent with the existing extended restart timeout. Preserve the
existing polling and completion behavior.

Comment thread src/cli/index.ts Outdated
Comment on lines +575 to +579
try {
const grok = stripGrokConfig();
if (grok.changed) console.log(`↩️ ${grok.message}`);
else if (!grok.ok) { restored = false; console.error(`⚠️ ${grok.message}`); }
} catch { /* best-effort */ }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A thrown stripGrokConfig failure leaves restored true and contradicts the stated invariant.

Line 578 sets restored = false when stripGrokConfig() returns !grok.ok. The bare catch { } at line 579 swallows a thrown failure and leaves restored at true.

The comment at lines 572-573 states the rule: "a refused Grok strip is actionable because it would point Grok at a dead proxy." A thrown failure has the identical consequence. The fence in ~/.grok/config.toml survives, the proxy is gone, and ocx stop reports success at line 660.

src/grok/inject.ts lines 460-507 currently wraps its body and returns errorResult("strip", error) instead of throwing, so this is latent today. The guard still contradicts its own stated intent, and a future refactor of stripGrokConfig would silently downgrade the stop result.

🛡️ Proposed fix
   try {
     const grok = stripGrokConfig();
     if (grok.changed) console.log(`↩️  ${grok.message}`);
     else if (!grok.ok) { restored = false; console.error(`⚠️  ${grok.message}`); }
-  } catch { /* best-effort */ }
+  } catch (error) {
+    // Same consequence as a refused strip: the fence survives and points Grok at a
+    // dead proxy, so the stop must not report success.
+    restored = false;
+    console.error(`⚠️  Grok config cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
   return restored;
🤖 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 `@src/cli/index.ts` around lines 575 - 579, Update the catch block surrounding
stripGrokConfig in the stop flow so any thrown failure sets restored = false
before continuing. Preserve the existing best-effort behavior and error handling
for returned !grok.ok results, ensuring both failure paths prevent ocx stop from
reporting successful restoration.

Comment on lines +165 to +178
const tampered = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
method: SYSTEM_RESTART_METHOD,
headers: { ...headers, [SYSTEM_RESTART_CAPABILITY_HEADER]: "C".repeat(43) },
});
expect(tampered.status).toBe(503);

const request = new Request(new URL(SYSTEM_RESTART_PATH, server.url), {
method: SYSTEM_RESTART_METHOD,
headers,
});
const local = { attestationSecret: secret, pid: process.pid, port: server.port };
expect(requireManagementAuth(request, unavailable, remoteConfig(), local)).toBeNull();
expect(managementPrincipal(request, unavailable, remoteConfig(), local))
.toBe("system-restart-capability");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add two negative cases: wrong method and wrong bound port.

The tampered-capability case at Lines 165-169 only corrupts the capability value. Two bindings enforced by restartCapabilityPayload in src/lib/system-restart-contract.ts (Lines 32-34) have no HTTP-level regression test:

  1. Method binding. hasSystemRestartCapability rejects a non-POST request at src/server/management-auth.ts Line 271. Nothing asserts that a DELETE to SYSTEM_RESTART_PATH with valid restart headers is refused. A future refactor that widens the method guard would pass the whole suite.
  2. Port binding. A capability minted for a different port must fail verification, because local.port comes from the live bound port at src/server/index.ts Line 680. This is the binding that stops a capability captured from one proxy instance from authorizing a different instance on another port.

Both cases are cheap to add next to the existing assertions and they pin the security properties the PR is built on.

💚 Proposed additional negative cases
       const tampered = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
         method: SYSTEM_RESTART_METHOD,
         headers: { ...headers, [SYSTEM_RESTART_CAPABILITY_HEADER]: "C".repeat(43) },
       });
       expect(tampered.status).toBe(503);
+
+      // The capability is bound to POST; another method on the same route must not pass.
+      const wrongMethod = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
+        method: "DELETE",
+        headers,
+      });
+      expect(wrongMethod.status).not.toBe(202);
+
+      // A capability minted for a different port must not authorize this instance.
+      const foreignPortCapability = createSystemRestartCapability(
+        secret,
+        nonce,
+        SYSTEM_RESTART_METHOD,
+        SYSTEM_RESTART_PATH,
+        process.pid,
+        server.port === 65535 ? 65534 : server.port + 1,
+      );
+      const wrongPort = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
+        method: SYSTEM_RESTART_METHOD,
+        headers: { ...headers, [SYSTEM_RESTART_CAPABILITY_HEADER]: foreignPortCapability! },
+      });
+      expect(wrongPort.status).toBe(503);
+      expect(scheduled).toBe(1);

As per path instructions: "Tests are flat Bun tests under tests/. A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tampered = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
method: SYSTEM_RESTART_METHOD,
headers: { ...headers, [SYSTEM_RESTART_CAPABILITY_HEADER]: "C".repeat(43) },
});
expect(tampered.status).toBe(503);
const request = new Request(new URL(SYSTEM_RESTART_PATH, server.url), {
method: SYSTEM_RESTART_METHOD,
headers,
});
const local = { attestationSecret: secret, pid: process.pid, port: server.port };
expect(requireManagementAuth(request, unavailable, remoteConfig(), local)).toBeNull();
expect(managementPrincipal(request, unavailable, remoteConfig(), local))
.toBe("system-restart-capability");
const tampered = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
method: SYSTEM_RESTART_METHOD,
headers: { ...headers, [SYSTEM_RESTART_CAPABILITY_HEADER]: "C".repeat(43) },
});
expect(tampered.status).toBe(503);
// The capability is bound to POST; another method on the same route must not pass.
const wrongMethod = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
method: "DELETE",
headers,
});
expect(wrongMethod.status).not.toBe(202);
// A capability minted for a different port must not authorize this instance.
const foreignPortCapability = createSystemRestartCapability(
secret,
nonce,
SYSTEM_RESTART_METHOD,
SYSTEM_RESTART_PATH,
process.pid,
server.port === 65535 ? 65534 : server.port + 1,
);
const wrongPort = await fetch(new URL(SYSTEM_RESTART_PATH, server.url), {
method: SYSTEM_RESTART_METHOD,
headers: { ...headers, [SYSTEM_RESTART_CAPABILITY_HEADER]: foreignPortCapability! },
});
expect(wrongPort.status).toBe(503);
expect(scheduled).toBe(1);
const request = new Request(new URL(SYSTEM_RESTART_PATH, server.url), {
method: SYSTEM_RESTART_METHOD,
headers,
});
const local = { attestationSecret: secret, pid: process.pid, port: server.port };
expect(requireManagementAuth(request, unavailable, remoteConfig(), local)).toBeNull();
expect(managementPrincipal(request, unavailable, remoteConfig(), local))
.toBe("system-restart-capability");
🤖 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 `@tests/server-management-auth.test.ts` around lines 165 - 178, Extend the
existing system-restart capability tests with two HTTP-level negative cases:
send a DELETE request to SYSTEM_RESTART_PATH using otherwise valid restart
headers and assert it is rejected, then create valid capability headers bound to
a different port and assert a request against the live server is rejected. Keep
these cases adjacent to the tampered-capability assertion and preserve the
existing valid-request expectations.

Source: Path instructions

Comment on lines +26 to +70
function successfulDeps() {
const secret = createLocalAttestationSecret();
const challenge = "A".repeat(43);
const requests: Array<{ url: string; init?: RequestInit }> = [];
const fetchImpl = (async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input);
requests.push({ url, init });
if (url.endsWith("/healthz")) {
const proof = createLocalAttestationProof(secret, challenge, target.pid!, target.port);
return new Response(JSON.stringify({
status: "ok",
service: "opencodex",
version: "test",
uptime: 1,
pid: target.pid,
port: target.port,
}), {
status: 200,
headers: {
"content-type": "application/json",
[LOCAL_ATTESTATION_PROOF_HEADER]: proof!,
},
});
}
return new Response(JSON.stringify({ success: true }), { status: 202 });
}) as typeof fetch;

return {
secret,
challenge,
requests,
deps: {
fetchImpl,
readRuntime: () => ({
pid: target.pid!,
port: target.port,
hostname: target.hostname,
attestationSecret: secret,
}),
findLive: async () => target,
createChallenge: () => challenge,
now: () => 1_000,
},
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two healthz fixtures now exist and they have diverged. Build the inline one from the helper.

The inline healthz response at Lines 35-48 includes port: target.port. The successfulDepsResponse helper at Lines 197-209 builds the same response but omits port. Both currently pass, because requestBoundSystemRestart binds the port through verifyLocalAttestationProof(runtime.attestationSecret, challenge, target.pid, target.port, proof) and never reads body.port. The divergence is therefore latent, not broken.

The failure mode: if isOpencodexHealthz is later tightened to require port, the tests at Lines 163-184 (which use the helper) would start failing while the tests using successfulDeps keep passing. A reader would then have to diff two near-identical fixtures to find out why. Collapse them onto one builder.

♻️ Proposed dedup of the healthz fixture
     if (url.endsWith("/healthz")) {
-      const proof = createLocalAttestationProof(secret, challenge, target.pid!, target.port);
-      return new Response(JSON.stringify({
-        status: "ok",
-        service: "opencodex",
-        version: "test",
-        uptime: 1,
-        pid: target.pid,
-        port: target.port,
-      }), {
-        status: 200,
-        headers: {
-          "content-type": "application/json",
-          [LOCAL_ATTESTATION_PROOF_HEADER]: proof!,
-        },
-      });
+      return successfulDepsResponse(secret, challenge);
     }

Then add the port field to the single remaining builder so the fixture matches what src/server/index.ts Line 631 actually serves:

 function successfulDepsResponse(secret: string, challenge: string): Response {
   const proof = createLocalAttestationProof(secret, challenge, target.pid!, target.port);
   return new Response(JSON.stringify({
     status: "ok",
     service: "opencodex",
     version: "test",
     uptime: 1,
     pid: target.pid,
+    port: target.port,
   }), {

successfulDepsResponse is a hoisted function declaration, so calling it from successfulDeps above its definition is safe.

🤖 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 `@tests/system-restart-client.test.ts` around lines 26 - 70, Consolidate the
duplicated healthz response fixtures by removing the inline response
construction from successfulDeps and reusing successfulDepsResponse instead.
Update the single successfulDepsResponse builder to include target.port,
preserving the response shape served by src/server/index.ts and keeping existing
request behavior unchanged.

Comment on lines +85 to +94
test("rejects missing runtime proof state before fetching", async () => {
let fetches = 0;
const fetchImpl = (async () => { fetches += 1; return new Response(); }) as typeof fetch;
const noRuntime = await requestBoundSystemRestart(target, 10_000, {
fetchImpl,
readRuntime: () => null,
});
expect(noRuntime.accepted).toBe(false);
expect(fetches).toBe(0);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the runtime identity mismatch branch, not only the missing-runtime branch.

requestBoundSystemRestart rejects on a compound guard: !runtime?.attestationSecret || runtime.pid !== target.pid || runtime.port !== target.port (src/cli/system-restart-client.ts, Lines 72-75). This test only exercises readRuntime: () => null. The pid and port mismatch clauses are untested.

That matters because those two clauses are the check that stops the CLI from signing a capability for a stale runtime record after a PID or port change — the exact failure this PR exists to prevent. A refactor that dropped either comparison would leave the suite green.

💚 Proposed additional mismatch cases
     expect(noRuntime.accepted).toBe(false);
     expect(fetches).toBe(0);
   });
+
+  test("rejects a stale runtime record before fetching", async () => {
+    const base = successfulDeps();
+    const stalePid = await requestBoundSystemRestart(target, 10_000, {
+      ...base.deps,
+      readRuntime: () => ({
+        pid: target.pid! + 1,
+        port: target.port,
+        hostname: target.hostname,
+        attestationSecret: base.secret,
+      }),
+    });
+    expect(stalePid.accepted).toBe(false);
+
+    const stalePort = successfulDeps();
+    const outcome = await requestBoundSystemRestart(target, 10_000, {
+      ...stalePort.deps,
+      readRuntime: () => ({
+        pid: target.pid!,
+        port: target.port + 1,
+        hostname: target.hostname,
+        attestationSecret: stalePort.secret,
+      }),
+    });
+    expect(outcome.accepted).toBe(false);
+    expect(stalePort.requests).toHaveLength(0);
+  });

As per path instructions: "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("rejects missing runtime proof state before fetching", async () => {
let fetches = 0;
const fetchImpl = (async () => { fetches += 1; return new Response(); }) as typeof fetch;
const noRuntime = await requestBoundSystemRestart(target, 10_000, {
fetchImpl,
readRuntime: () => null,
});
expect(noRuntime.accepted).toBe(false);
expect(fetches).toBe(0);
});
test("rejects missing runtime proof state before fetching", async () => {
let fetches = 0;
const fetchImpl = (async () => { fetches += 1; return new Response(); }) as typeof fetch;
const noRuntime = await requestBoundSystemRestart(target, 10_000, {
fetchImpl,
readRuntime: () => null,
});
expect(noRuntime.accepted).toBe(false);
expect(fetches).toBe(0);
});
test("rejects a stale runtime record before fetching", async () => {
const base = successfulDeps();
const stalePid = await requestBoundSystemRestart(target, 10_000, {
...base.deps,
readRuntime: () => ({
pid: target.pid! + 1,
port: target.port,
hostname: target.hostname,
attestationSecret: base.secret,
}),
});
expect(stalePid.accepted).toBe(false);
const stalePort = successfulDeps();
const outcome = await requestBoundSystemRestart(target, 10_000, {
...stalePort.deps,
readRuntime: () => ({
pid: target.pid!,
port: target.port + 1,
hostname: target.hostname,
attestationSecret: stalePort.secret,
}),
});
expect(outcome.accepted).toBe(false);
expect(stalePort.requests).toHaveLength(0);
});
🤖 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 `@tests/system-restart-client.test.ts` around lines 85 - 94, Add focused tests
beside “rejects missing runtime proof state before fetching” that provide
runtime records with valid attestationSecret but mismatched pid and mismatched
port relative to target. Assert requestBoundSystemRestart returns accepted false
and fetchImpl is never called for each case, covering both runtime identity
guard comparisons.

Source: Path instructions

Comment thread tests/tray-proxy.test.ts
Comment on lines +124 to +190
test("request uncertainty observes for a replacement and never falls back to stop/start", async () => {
const calls: string[] = [];
const error = new Error("response connection closed");
const result = await runProxyRestart({
findLive: async () => ({ pid: 10, port: 10100, source: "runtime" }),
startWhenStopped: async () => { calls.push("fallback-start"); return true; },
requestInPlaceRestart: async () => {
calls.push("request");
return { accepted: false, uncertain: true, error };
},
waitForReplacement: async () => { calls.push("wait"); return null; },
});
expect(result).toEqual({ ok: false, phase: "request", error });
expect(calls).toEqual(["request", "wait"]);
});

test("a replacement proves success even when the request response was lost", async () => {
const error = new Error("response connection closed");
const replacement: ProxyRestartLive = { pid: 20, port: 10100, source: "runtime" };
const result = await runProxyRestart({
findLive: async () => ({ pid: 10, port: 10100, source: "runtime" }),
startWhenStopped: async () => true,
requestInPlaceRestart: async () => ({ accepted: false, uncertain: true, error }),
waitForReplacement: async () => replacement,
});
expect(result).toEqual({ ok: true, mode: "restarted", live: replacement });
});

test("a definite request rejection does not wait or start another proxy", async () => {
const calls: string[] = [];
const error = new Error("target changed");
const result = await runProxyRestart({
findLive: async () => ({ pid: 10, port: 10100, source: "runtime" }),
startWhenStopped: async () => { calls.push("fallback-start"); return true; },
requestInPlaceRestart: async () => {
calls.push("request");
return { accepted: false, uncertain: false, error };
},
waitForReplacement: async () => { calls.push("wait"); return null; },
});
expect(result).toEqual({ ok: false, phase: "request", error });
expect(calls).toEqual(["request"]);
});

test("an accepted restart that never publishes a replacement fails closed", async () => {
const calls: string[] = [];
const result = await runProxyRestart({
findLive: async () => ({ pid: 10, port: 10100, source: "runtime" }),
startWhenStopped: async () => { calls.push("fallback-start"); return true; },
requestInPlaceRestart: async () => { calls.push("request"); return { accepted: true }; },
waitForReplacement: async () => { calls.push("wait"); return null; },
});
expect(result).toEqual({ ok: false, phase: "replacement" });
expect(calls).toEqual(["request", "wait"]);
});

test("an unverified live target fails closed before the restart request", async () => {
const calls: string[] = [];
const result = await runProxyRestart({
findLive: async () => ({ pid: null, port: 10100, source: "config" }),
startWhenStopped: async () => { calls.push("fallback-start"); return true; },
requestInPlaceRestart: async () => { calls.push("request"); return { accepted: true }; },
waitForReplacement: async () => { calls.push("wait"); return null; },
});
expect(result).toEqual({ ok: false, phase: "identity" });
expect(calls).toEqual([]);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression tests for the two thrown-error branches of runProxyRestart.

The suite covers returned outcomes well. Two thrown-error branches in src/cli/tray-proxy.ts have no coverage, and both are reachable in production:

  • io.findLive rejects → src/cli/tray-proxy.ts line 98 maps it to { ok: false, phase: "request" }. src/cli/index.ts line 542 throws restart_deadline_expired from findLive on purpose, so this is a wired production path.
  • io.waitForReplacement rejects → src/cli/tray-proxy.ts line 135 maps it to { ok: false, phase: "replacement" }.

Both phase values drive the operator message in reportRestartFailure at src/cli/index.ts lines 525-535. Without tests, a future change to the phase mapping would silently change what the operator is told.

💚 Proposed additional test cases
  test("a discovery failure fails closed without requesting or starting", async () => {
    const calls: string[] = [];
    const error = new Error("restart_deadline_expired");
    const result = await runProxyRestart({
      findLive: async () => { throw error; },
      startWhenStopped: async () => { calls.push("fallback-start"); return true; },
      requestInPlaceRestart: async () => { calls.push("request"); return { accepted: true }; },
      waitForReplacement: async () => { calls.push("wait"); return null; },
    });
    expect(result).toEqual({ ok: false, phase: "request", error });
    expect(calls).toEqual([]);
  });

  test("a failed replacement observation reports the replacement phase", async () => {
    const error = new Error("probe failed");
    const result = await runProxyRestart({
      findLive: async () => ({ pid: 10, port: 10100, source: "runtime" }),
      startWhenStopped: async () => true,
      requestInPlaceRestart: async () => ({ accepted: true }),
      waitForReplacement: async () => { throw error; },
    });
    expect(result).toEqual({ ok: false, phase: "replacement", error });
  });

Based on path instructions: "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem."

🤖 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 `@tests/tray-proxy.test.ts` around lines 124 - 190, Add focused regression
tests near the existing runProxyRestart tests for both thrown-error branches:
make findLive reject and assert { ok: false, phase: "request", error } with no
request or fallback start, then make waitForReplacement reject and assert { ok:
false, phase: "replacement", error }. Use call tracking where needed to verify
fail-closed behavior.

Source: Path instructions

@luvs01
luvs01 force-pushed the agent/fix-bound-in-place-restart branch from e45eb7f to 74c2a9c Compare August 6, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant