Skip to content

fix(service): preserve supervision across restart - #1160

Closed
jonathanli12 wants to merge 2 commits into
lidge-jun:devfrom
jonathanli12:codex/preserve-service-on-restart
Closed

fix(service): preserve supervision across restart#1160
jonathanli12 wants to merge 2 commits into
lidge-jun:devfrom
jonathanli12:codex/preserve-service-on-restart

Conversation

@jonathanli12

@jonathanli12 jonathanli12 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • stop registered managers and tracked standalone listeners before service install
  • preserve installed service supervision across ocx restart
  • keep standalone restart behavior unchanged when no service is installed
  • add lifecycle regression coverage and update canonical documentation

Validation

  • focused lifecycle suite: 129 passed
  • wider service, restart, and update suite: 279 passed
  • typecheck: passed
  • privacy scan: passed
  • git diff check: passed

No live service restart was used for validation.

Summary by CodeRabbit

  • New Features

    • Improved ocx restart to preserve the current supervision mode.
    • Installed services now restart through the service manager; standalone proxies continue using background startup.
    • Active ports are synchronized with Codex after restarting.
  • Bug Fixes

    • Service installation now stops existing services and standalone proxies first, preventing port conflicts and restart loops.
    • Failed stop operations correctly prevent restart.

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.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: da688fe6-c4c6-461d-a64a-27704f0c26d9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ocx restart now preserves service or standalone supervision. Service installation stops existing service managers and tracked standalone proxies before installing new assets. Tests and lifecycle documentation cover both flows.

Changes

Service lifecycle

Layer / File(s) Summary
Restart supervision selection
src/cli/index.ts, tests/grok-lifecycle.test.ts, docs-site/src/content/docs/reference/cli/lifecycle.md
restart detects service installation, stops the current process, then starts the service or uses handleEnsure(). The live port remains synchronized in both paths. Tests verify ordering and failed-stop behavior.
Installation cleanup ordering
src/service.ts, tests/service.test.ts, docs-site/src/content/docs/reference/cli/lifecycle.md
Service installation stops existing service managers and tracked standalone proxies before ops.install() writes and loads new assets. A regression test verifies this order.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: lidge-jun, wibias, ingwannu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 the main change: preserving service supervision during restart.
✨ Finishing Touches
🧪 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.

@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: 3

🤖 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 `@src/service.ts`:
- Around line 2654-2657: Update the manager shutdown logic around ops.status(),
isServiceInstalled(), and stopTrackedProxyForServiceCommand() to catch only a
confirmed not-installed condition; report other status or stop failures and
return before invoking ops.install() or reporting installation success. Add a
regression test proving installation is skipped when manager shutdown fails.
- Line 2655: Update the cleanup around the ops.stop() call to use
diagnoseService().backend, rather than only isServiceInstalled(), when selecting
the service manager to stop; ensure the previously recorded backend is stopped
before switching to the newly requested parsed.backend, including stopping both
Windows backends when they differ.

In `@tests/grok-lifecycle.test.ts`:
- Around line 83-99: Strengthen the restart assertions in the test named
“handleStop returns its outcome and restart preserves its supervision mode” to
verify branch association, not just source order: assert that
serviceCommand("start") is inside the serviceWasInstalled=true path and
handleEnsure() is the false/standalone path. Match the conditional block
explicitly while preserving the existing ordering checks.
🪄 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: f0364115-3ac4-4c8c-83f0-9f9747f1a9f6

📥 Commits

Reviewing files that changed from the base of the PR and between b39eecf and ea78087.

📒 Files selected for processing (5)
  • docs-site/src/content/docs/reference/cli/lifecycle.md
  • src/cli/index.ts
  • src/service.ts
  • tests/grok-lifecycle.test.ts
  • tests/service.test.ts

Comment thread src/service.ts Outdated
Comment on lines +2654 to +2657
try {
if (ops.status() !== null || isServiceInstalled()) ops.stop();
} catch { /* absent or already stopped */ }
await stopTrackedProxyForServiceCommand();

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail closed when manager shutdown fails.

The empty catch swallows permission errors, ownership errors, command failures, and real stop failures. The code then calls ops.install() while the old manager may still be running. This can leave a respawning proxy on the configured port and make reportServiceServing("installed") report a false success. Catch only a confirmed not-installed condition; otherwise report the error and return before installing assets. Add a regression test that verifies installation does not run after a stop failure.

🤖 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/service.ts` around lines 2654 - 2657, Update the manager shutdown logic
around ops.status(), isServiceInstalled(), and
stopTrackedProxyForServiceCommand() to catch only a confirmed not-installed
condition; report other status or stop failures and return before invoking
ops.install() or reporting installation success. Add a regression test proving
installation is skipped when manager shutdown fails.

Comment thread src/service.ts Outdated
// restart-loops on EADDRINUSE, and the old standalone process makes the install
// verification report a false success.
try {
if (ops.status() !== null || isServiceInstalled()) ops.stop();

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Stop the recorded service backend before switching backends.

ops is built from parsed.backend, which is the backend requested for the new install. isServiceInstalled() calls diagnoseService().installed but discards diagnoseService().backend. If a Windows service was installed with the other backend, this condition can be true while ops.stop() targets only the new backend. The old manager can remain registered and respawn a proxy while the new backend is installed. Use the recorded backend from diagnoseService().backend for cleanup, or stop both Windows backends when the requested backend differs.

🤖 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/service.ts` at line 2655, Update the cleanup around the ops.stop() call
to use diagnoseService().backend, rather than only isServiceInstalled(), when
selecting the service manager to stop; ensure the previously recorded backend is
stopped before switching to the newly requested parsed.backend, including
stopping both Windows backends when they differ.

Comment thread tests/grok-lifecycle.test.ts Outdated
Comment on lines +83 to +99
test("handleStop returns its outcome and restart preserves its supervision mode", () => {
const stopFn = sliceFn(CLI_SOURCE, "async function handleStop(", "async function handleUninstall(");
// process.exit() inside handleStop would strand runTrayProxyRestart's start() half.
expect(stopFn).toContain("process.exitCode = 1");
expect(stopFn).toContain("return !stopFailed");
expect(stopFn).not.toContain("process.exit(1)");

const restartCase = sliceFn(CLI_SOURCE, 'case "restart"', 'case "health"');
expect(restartCase).toContain("if (await handleStop()) await handleEnsure()");
const diagnoseAt = restartCase.indexOf("diagnoseService().installed");
const stopAt = restartCase.indexOf("await handleStop()");
const serviceStartAt = restartCase.indexOf('await serviceCommand("start")');
const standaloneStartAt = restartCase.indexOf("await handleEnsure()");
expect(diagnoseAt).toBeGreaterThan(-1);
expect(diagnoseAt).toBeLessThan(stopAt);
expect(serviceStartAt).toBeGreaterThan(stopAt);
expect(standaloneStartAt).toBeGreaterThan(serviceStartAt);
expect(restartCase).toContain("if (serviceWasInstalled)");

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 | 🔵 Trivial | ⚡ Quick win

Assert branch association, not only source order.

The assertions prove that both startup calls occur after handleStop(), but they do not prove that serviceCommand("start") runs only when serviceWasInstalled is true or that handleEnsure() is the false branch. An unconditional service start followed by handleEnsure() would still pass these checks and would break standalone restart. Match the conditional block explicitly or execute both supervision paths.

🤖 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/grok-lifecycle.test.ts` around lines 83 - 99, Strengthen the restart
assertions in the test named “handleStop returns its outcome and restart
preserves its supervision mode” to verify branch association, not just source
order: assert that serviceCommand("start") is inside the
serviceWasInstalled=true path and handleEnsure() is the false/standalone path.
Match the conditional block explicitly while preserving the existing ordering
checks.

@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: ea78087a3f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cli/index.ts Outdated
break;
}
case "restart": {
const serviceWasInstalled = diagnoseService().installed;

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 Fall back when the installed service is not startable

When a stale, conflicting, or otherwise non-startable service registration exists, diagnoseService().installed is still true even though src/service.ts explicitly separates that from startable/viable; in that case ocx restart now stops the currently working proxy and then attempts ocx service start, which can fail or time out and leave no proxy running. This regresses the previous standalone restart behavior for broken service registrations, so the service path should be gated on a startable/viable diagnostic or fall back to handleEnsure() if service start does not actually recover.

Useful? React with 👍 / 👎.

Comment on lines +39 to +42
Stop and restart the proxy while preserving its supervision mode. If a background service was
installed, `restart` starts that service again so login and crash protection remain active. Without
an installed service, it starts the standalone background proxy through `ensure`. Both paths sync
the live port back into Codex.

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 Update the translated lifecycle docs

This changes the canonical ocx restart behavior to preserve service supervision, but the Korean and Simplified Chinese lifecycle pages still describe it as stop followed by ensure, so the localized docs now contradict the English source for the same user workflow. Please update those directly affected pages along with this English change.

AGENTS.md reference: docs-site/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment thread src/service.ts Outdated
try {
if (ops.status() !== null || isServiceInstalled()) ops.stop();
} catch { /* absent or already stopped */ }
await stopTrackedProxyForServiceCommand();

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 Abort install when proxy cleanup fails

If the tracked standalone proxy cannot be stopped here (for example the management stop times out or is refused), stopTrackedProxyForServiceCommand() only logs and returns "none", but install still proceeds and reportServiceServing() can accept the old standalone /healthz listener as proof that the newly registered service is serving. That leaves the same false-success/service-restart-loop scenario this install cleanup is meant to prevent, so failed cleanup should block the install or the verification must prove the listener is the new supervised child.

Useful? React with 👍 / 👎.

Comment thread src/cli/index.ts Outdated
// Preserve the installed supervision boundary. Starting via handleEnsure here
// creates a detached standalone proxy after handleStop unloads the service,
// so every routine restart silently loses login/crash protection.
if (serviceWasInstalled) await serviceCommand("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 Wait for Codex sync before returning from service restart

For a healthy installed service, this path now returns after serviceCommand("start") only verifies /healthz; that endpoint is available immediately after the service child binds, before handleStart completes its startup syncCodexOnStartIfEnabled injection. Previously restart used handleEnsure(), whose parent process synced the live port before returning, so a user who runs codex immediately after ocx restart can still be on the restored native/stale config until the service child finishes its async sync. Please wait for readiness or explicitly sync after service start.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⏳ 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.

@github-actions
github-actions Bot marked this pull request as draft August 6, 2026 23:09
@github-actions github-actions Bot added the bug Something isn't working label Aug 6, 2026

Wibias commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Thank you @jonathanli12 — I’ve carried this PR’s unique service-install cleanup and its regression coverage into #1206. The alternate stop/start ocx restart path is intentionally not carried because #1131’s identity-bound in-place restart is the canonical restart architecture in the consolidation.

Closing this source PR as superseded by #1206. Your contribution is explicitly credited in the consolidation PR. 🙏

@Wibias Wibias closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants