Repository navigation
[Bug] Post-merge #7851: completed-drain restart refusal and Grok bind-scope regression #7871
Description
Client or integration
OpenCodex dashboard / Codex CLI (startup recovery and Grok integration)
Area
Service lifecycle
Summary
Track the two functional findings left open after #7851 merged as 7a1565bb968b343f68eddd6ea266222fcfd95ff1. The original recovery-ordering defect in #7827 was closed by that merge; these are its remaining follow-up findings.
Reproduction
The post-merge validation reports four product-module reproduction/control cases, 23 assertions, pinned Bun 1.4.4, repository preload and isolated temporary homes. Those passing cases assert the defects and controls; they do not establish a fix. This issue's read-only check did not rerun those tests. On 2026-10-28, the current dev blobs for proxy-liveness.ts, system-restart.ts, start-owner-fence.ts, cli/index.ts, grok/sync.ts and grok/inject.ts still matched the merge. Both original review threads remain unresolved.
Findings and completion criteria
- P1 — completed-drain dashboard restart handoff: original finding. In the unsupervised completed-drain path, the parent stops listening but waits for replacement health before exiting. Its PID record remains live; the replacement's recovery observation returns
unknownand refuses startup. Reported composition reproduction ends with parent exit 1 and no serving replacement. Add a safe known-parent handoff without treating arbitrary live/unknown owners as absent; regress completed drain, still-live parent and refused endpoints, with the exited-parent control. Waiting for parent exit while the parent waits for replacement health is not a solution. Real process-replacement/dashboard E2E was not established by the reported module test and should be verified or explicitly recorded as untested. - P2 — bind scope versus discovery address: original finding. After the early ensure probe misses a live wildcard-bound proxy, recovery returns the responding loopback address instead of the bind scope. The reported real Grok-writer case then writes a configuration without the admission token required by that listener; retaining the original bind hostname gives the expected non-loopback refusal and no file. Preserve separate bind-scope and probe-address values and use bind scope for integration policy. Add the missed-early-probe/wildcard regression plus a normal loopback control. This is incorrect client configuration, not a server authentication bypass.
Close when each item has a linked fix and focused regression evidence, or an explicit evidence-backed maintainer disposition. Separate fix PRs are fine. #7866 changes bearer-matrix test setup, not these product paths.
Version
Reviewed merge 7a1565bb968b343f68eddd6ea266222fcfd95ff1 (#7851); follow-up base dev@f9e7d939d125c810aa3f3d700dcb8b0cb4129a0f.
Operating system
Linux verification with pinned Bun 1.4.4 and isolated test homes. The product-module evidence above is not a native dashboard/service restart acceptance run.
Provider and model
Not provider-specific. No provider request is needed for these regressions.
Logs or error output
The linked original review threads and post-merge verification contain the evidence and its limits. No new product log or native platform reproduction is claimed by this issue-format correction.
Screenshots and supporting files
See the source, review and validation links above.
Redacted configuration
Not applicable; no account, credential, installed application or live service configuration was changed for this report.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
coderabbitai commentedon Oct 28, 2026
🔗 Related PRs
#7747 - fix(update): keep Codex routing for the restarted proxy during an update (
#7423) [open]📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
luvs01 commentedon Oct 28, 2026
Verified candidate published as Draft PR #7874: #7874
Head
889dc0f980d934af199b1f23b6493ee2297def39, based ondev@f9e7d939. Its remote tree exactly matches the locally validated index. Both issue items are implemented: completed-drain retirement of only the parent's own ownership records, with the ordinary child live/unknown-owner refusal retained; and separate bind hostname versus responding probe address for Grok policy.The actual parent/child and loopback-listener fixtures passed, along with the real Grok sync/inject matrix. Broader regression selection: 287 passed / 0 failed; layout/tooling/ratchet: 38 passed / 0 failed. Typecheck, structure and privacy checks passed. The full server's native lease shutdown order was source-reviewed, not exercised as a live provider/dashboard E2E.
Current-head hosted CI and explicit maintainer/security acceptance remain pending. Native Windows/macOS replacement is untested. The issue remains open until integration and the stated acceptance/disposition; no security-only completion claim is made. The candidate also merges cleanly in a read-only integration check with the newer
dev@492c1a15after #7870.Review follow-up published at head
0fe66b2b16bce2e93eee5dc47104a584b7cfd075(tree945277083196bccc8494d60ffaf70788a8dc347d) by a normal fast-forward update. Missing/blank default binds are now explicit loopback in both discovery paths. The new actual reconciler/Grok matrix reproduced 18 old failures and passes all 48 cases after the correction; 10-file regression: 285 passed / 0 failed. The preceding889dc0fCI completed successfully, but it is not current-head evidence after this correction. New CI/re-review and maintainer/native conditions remain.coderabbitai commentedon Oct 28, 2026
Coding Plan
Summary
restart-parent-stoppedobservation kind only inhandleStart. Arbitrary live or unknown owners stay refused. The child never waits for parent exit.LiveProxy.hostnameas bind scope and addsprobeAddressfor the address that answered. Wildcard scope therefore reaches Grok and produces the expected non-loopback refusal.Design Choices
Design Choice 1: Fix revision baseline
Options Considered:
88aaa754.dev, which contains7a1565bb, and implement there.Chosen Option: 2
Rationale: HEAD does not contain either defect, so a fix there cannot address the reported composition.
Design Choice 2: Parent signal for a stopped listener while waiting for child health
Options Considered:
OCX_RESTART_PARENT_PIDas permission to recover past a live recorded owner.Chosen Option: 3
Rationale: Option 1 would also authorize deadline, failed and rejected handoffs where the parent can still serve. Option 2 adds persistent state that needs owner-bound publication and stale-record cleanup. Option 3 needs no persistent state and reuses the existing actual-parent validation in
takeRestartParentMarker.Design Choice 3: Observation representation for the handoff
Options Considered:
kind: "absent"when the handoff conditions hold.{ kind: "restart-parent-stopped", live: null, parentPid }. Only the start recovery gate accepts this kind, and only when the caller passed the validated marker.Chosen Option: 2
Rationale: "Absent" would be false while the parent process is alive. A distinct kind keeps
handleEnsureand other callers fail-closed and makes the exception auditable.Design Choice 4: Representation of bind scope and probe address
Options Considered:
hostnameto mean "probe address" everywhere and addbindHostname.LiveProxy.hostnameas bind scope, which is its documented meaning. AddprobeAddressfor the address that answered. Fix every producer that writes the responding address intohostname.Chosen Option: 2
Rationale: Consumers already use
hostnameas bind scope and applyprobeHostnamewhen they dial:runtime-api.ts:95,ready.ts:209,status.ts:939–942,index.ts:1300–1304and Grok. Option 2 limits churn to the faulty producers.Design Choice 5: Scope of spawned-ensure metadata loss at `src/cli/index.ts:879–890`
Options Considered:
Chosen Option: 2
Rationale: That branch is not the reported substitution. Changing it would widen scope beyond the ticket.
💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: P1 — Known-parent completed-drain handoff
Goal. The replacement must start when all of these conditions hold:
The child must distinguish the known waiting parent from arbitrary live or unknown owners.
Failure mode at
7a1565bb.findLiveProxyreturnsnull.probeOwnerPastRestartParentreturns at once onlive: null(src/cli/restart-handoff.ts:79–110). This step is correct.observeProxyLivenesssees an alive recorded PID with an OpenCodex command line. It returnsunknown.7a1565bb:tests/server/proxy-liveness-recovery.test.ts:35–40asserts this result, even withECONNREFUSED.7a1565bb:src/cli/start-owner-fence.ts:6–21throws.spawnReplacementStartexhausts early-exit retries. The parent exits 1 with no serving replacement.Fix. Add an explicit, parent-verified, refusal-gated exception. The exception must not wait for parent exit. It must not convert a generic
unknownresult into permission to start.Task 1: Establish the revision baseline and port the reported reproductions
Confirm that the fix base contains
7a1565bb. Convert the post-merge reproduction cases into failing tests before any product change.dev. Confirm thatgit merge-base --is-ancestor 7a1565bb968b343f68eddd6ea266222fcfd95ff1 HEADexits 0. The local checkout is shallow. Fetch enough history first.88aaa754lacks theunknownrecovery gate. The ticket reports thatdevequals7a1565bbfor all affected files.observeProxyLivenessand theProxyLivenessObservationtype insrc/server/proxy-liveness.ts.recoverStartJournalAfterOwnerRecheckandrecoverStartStateUnderOwnershipLeaseinsrc/cli/start-owner-fence.ts.handleStartandhandleEnsurecall sites insrc/cli/index.ts.#7851post-merge validation comment. That comment reports 23 assertions with Bun 1.4.4, the repository preload, and isolated temporary homes.tests/cli/cli-start-owner-absence.test.ts. Port the P2 cases into the files named in Phase 2.Task 2: Define the stopped-parent handoff marker contract
Add a consumed, parent-bound marker next to
OCX_RESTART_PARENT_PID.src/lib/system-restart-contract.ts(lines 48–75, next towithRestartParentMarkerandtakeRestartParentMarker):OCX_RESTART_PARENT_STOPPED_PID.with…helper that writes the parent PID.take…helper.take…helper:takeRestartParentMarker.^[1-9]\d{0,9}$syntax.takeRestartParentMarkeragainstprocess.ppid.null.takeRestartHandoffMarkersinsrc/cli/restart-handoff.tsto return the validated stopped-parent PID next to the existingrestartParentvalue. Keep existing call sites source-compatible.Task 3: Emit the marker only on the unsupervised completed-drain, listener-stopped path
The parent must assert "I no longer serve and I wait for your health" only when that statement is true.
src/server/management/system-restart.ts, apply the stopped-parent marker whenwaitForHealthis true. Do this inspawnDetachedStart(lines 265–276) or inreplacementStartEnvironment(lines 250–263).acceptSystemRestartpassestrueonly from the completed-drain branch, afterawaitof listener stop (lines 442–468).false. If any branch passestrueafter an unsuccessful listener stop, require a successful stop before passingtrue.tests/clients/desktop-supervised-restart.test.ts:51–95.src/server/restart-replacement.ts(lines 347–385) forwards the marker unchanged into every early-exit retry attempt.withRestartParentMarkerat line 363 already applies torequest.env. The new marker uses the sameenvobject.waitForReplacementReadymust keep rejecting the parent PID as replacement health.Task 4: Add a refusal-gated handoff observation in proxy-liveness
Let
observeProxyLivenessrecognize only the known stopped parent.src/server/proxy-liveness.ts, extend theProxyLivenessObservationunion (7a1565bb:474–494) with a distinct handoff kind. For example:{ kind: "restart-parent-stopped"; live: null; parentPid: number }.stoppedRestartParentPid?: number. Keep behavior unchanged for callers that omit it.kind: "live". Never bypass a different owner that appears.stoppedRestartParentPid.isConnectionRefused).unknownfor them:probeEndpointLivenesscounts that response asdead, but it does not prove that the parent's endpoint is closed.classifyHealthzandloopbackProbeHostsprimitives. Do not reuse the aggregatedeadstate.absentlogic. The exited-parent case must not depend on the marker.Task 5: Accept the handoff kind only in the start recovery gate
Pass the exception narrowly through
handleStart.src/cli/start-owner-fence.ts, letrecoverStartJournalAfterOwnerRecheckaccept an optionalstoppedRestartParentPid. Pass it toobserveProxyLiveness.kind === "absent".kind === "restart-parent-stopped"withparentPidequal to the passed value.src/cli/index.ts:608–617. The parent's exit cleanup atsrc/cli/index.ts:664–665removes only its own records.reconcileStartupJournalunchanged on the handoff kind. Before#7851, this path reached recovery throughlive: null. This change restores the pre-fix(cli): recheck live ownership before journal recovery #7851 behavior only for the verified case.src/cli/index.ts, inhandleStartaround lines 441–483, pass the validated stopped-parent PID fromtakeRestartHandoffMarkersintorecoverStartStateUnderOwnershipLease.handleEnsure. Ensure is never a restart replacement and must stay fail-closed.probeOwnerPastRestartParentalready returns at once onlive: null. Keep that behavior.7a1565bbnow takes for ordinary starts. If the parent holds the lease, the child would block until the replacement deadline. If such a hold exists, document it and release the lease after listener stop on the completed-drain path.src/cli/index.ts:527–556unchanged. It must still refuse any live owner.Task 6: Add P1 regressions and controls
Cover the composition and every negative case.
tests/lib/system-restart-contract-security.test.ts, test the stopped-parent marker:tests/server/proxy-liveness-recovery.test.ts, cover:ECONNREFUSED→restart-parent-stopped.unknown. Keep the existing test at lines 35–40 unchanged.unknown.ETIMEDOUTor reset on any candidate →unknown.unknown.live.tests/cli/cli-start-owner-absence.test.ts, test the completed-drain composition. Use the live test-parent PID, retained PID and runtime records, refused endpoints, and both markers.probeOwnerPastRestartParent. The existing restart-parent case at lines 90–91 replaces it withprobe => probe(). Do not copy that replacement.OCX_RESTART_PARENT_PIDmust still refuse. It must preserve the PID and journal.absentrecovery.tests/server/system-restart.test.ts(near lines 716–776) andtests/server/restart-replacement.test.ts, verify:waitForHealthis true, for a completed drain with a successful listener stop.🤖 Prompt for AI agents
Phase 2: P2 — Separate bind scope from probe address
Goal. Integration policy must use the scope the listener bound. It must not use the loopback address that answered the health probe.
Failure mode.
handleEnsure's earlyfindProxyOwnerBeforeJournalRecoverymisses a live0.0.0.0proxy (src/cli/index.ts:816–826).127.0.0.1. In7a1565bb,recoverStartJournalAfterOwnerRechecksetsowner.live = observation.live. The observation returns the responding address ashostname.handleEnsuretakes the live branch and forwardslive.hostname(src/cli/index.ts:853–860). The value passes throughensureGrokFenceMatchesDesired(src/cli/ensure-desired-integrations.ts:128–134) andsyncGrokConfig(src/grok/sync.ts:51–60).isLoopbackHostname("127.0.0.1")passes insrc/grok/inject.ts:1142–1165. The injector writesapi_key = "opencodex-loopback"(lines 1032–1040). That listener requires an admission token.This defect causes incorrect client configuration. It is not a server authentication bypass.
Scope exclusion. The spawned-ensure branch at
src/cli/index.ts:879–890keeps only.portand reconciles with confighostname. It never receives a probe address. Record this metadata loss separately. Do not change that branch in this ticket.Task 1: Add `probeAddress` and keep `hostname` as bind scope
Represent both values without changing the meaning of
hostnamefor existing consumers.src/server/proxy-liveness.ts, add optionalprobeAddress?: stringtoLiveProxy(lines 152–158). It holds the bracketed or normalized address that answered.hostnameis bind scope. It comes from either the runtime record or config. State that configured-port fallback is configured scope, not listener-attested scope.hostnameundefined. Never fill it from the responding address. Downstream code (opts.hostname ?? config.hostname) keeps its current fallback.src/cli/index.ts:853–856. It currently claims that the value is what the proxy "ACTUALLY bound". State the real provenance instead.Task 2: Fix every producer that writes the responding address into `hostname`
Correct the observation path. Correct
probePortOwnerfor consistency.observeProxyLivenessand its live branch at7a1565bb:src/server/proxy-liveness.ts, locate the code that returns the successful loopback candidate ashostname. This is the confirmed P2 site. Task 1.1 records its exact line.hostnamefrom the runtime record or config. Use the same provenance as the threefindLiveProxybranches at lines 491–553.probeAddress.findLiveProxybranches already preserveruntime.hostname,record.hostname, andconfig.hostname. AddprobeAddressthere only if the dial candidate is available without restructuring.probePortOwner(lines 591–601), changereturn { ...identity, hostname }. Return the candidate asprobeAddress. Keephostnameas the input bind scope (opts.hostname).chooseListenPortatsrc/cli/index.ts:318–339, does not readholder.hostname. Its behavior does not change.tests/server/proxy-liveness.test.ts:301–313. Change the expectation fromhostname: "[::1]"toprobeAddress: "[::1]".loopbackProbeHosts, alternate-family probing, wildcard normalization inprobeHostname, or cross-home discovery insrc/cli/cross-home-owner.ts:217–245.Task 3: Add P2 regressions and controls
Prove that the missed-early-probe composition preserves wildcard scope through the real Grok writer.
tests/server/proxy-liveness-recovery.test.ts, cover:hostname: "0.0.0.0"that answers on127.0.0.1→ observationlivewithhostname: "0.0.0.0"andprobeAddress: "127.0.0.1".127.0.0.1or::1keeps thathostname.tests/cli/cli-start-owner-absence.test.tsortests/clients/sync-client-integrations.test.ts:930–975. If a new file is created undertests/cli/, update the layout map as required byAGENTS.md:14–28.reconcileEnsureDesiredIntegrations, the realsyncGrokConfig, and the real injector against an isolated temporarygrokHome.syncGrokConfig.tests/cli/ensure-desired-integrations-race.test.tsstubs it. Do not copy that setup.skippedReason: "non-loopback". Assert that noconfig.tomlexists. Reuse the assertion shape fromtests/providers/xai/grok-sync.test.ts:226–249.base_url.tests/providers/xai/grok-sync.test.ts:226–249: wildcard skip and IPv6http://[::1]:10100/v1.tests/providers/xai/grok-sync.test.ts:251–303: the explicit unauthenticated loopback-listener exception, which writeshttp://127.0.0.1:10102/v1, and the companion-listener case.🤖 Prompt for AI agents
Phase 3: Verification and evidence disposition
Goal. Produce the closure evidence required by the ticket. Link the fix and focused regression evidence for each item. Provide an explicit E2E record.
Task 1: Run focused validation under the pinned toolchain
tests/cli/cli-restart-handoff.test.tstests/cli/cli-start-owner-absence.test.tstests/cli/cli-start-journal-order.test.tstests/server/proxy-liveness*.test.tstests/server/system-restart*.test.tstests/server/restart-replacement.test.tstests/lib/system-restart-contract-security.test.tstests/cli/ensure-desired-integrations-race.test.tstests/providers/xai/grok-sync.test.tssrc/AGENTS.md:22–29.Task 2: Verify or record real process-replacement E2E
/healthz.tests/ci-workflows/package-tree-*restart*.test.tsharness can run this composition, extend it. Otherwise, state in the PR that real process-replacement and dashboard E2E is untested. List the module tests that cover each step.src/cli/index.ts:879–890) as a separate observation. State that this ticket does not change it.🤖 Prompt for AI agents
Research
Ticket Summary
This plan fixes two follow-up defects from
#7851. That change merged as7a1565bb968b343f68eddd6ea266222fcfd95ff1.unknownbecause the waiting parent's PID record is still live. The replacement refuses startup. The parent then exits 1.hostname. Grok then writes a tokenless loopback configuration instead of refusing non-loopback scope.The fix requires:
Codebase Summary
OpenCodex is a Bun-native TypeScript CLI and proxy.
Restart path
src/server/management/system-restart.tsruns dashboard restarts.acceptSystemRestart(lines 420–483) drains, stops the listener, and callsspawnStart(restartPort, drainOutcome === "completed").src/server/restart-replacement.tsspawns the child.spawnReplacementStart(lines 347–374) addsOCX_RESTART_PARENT_PIDfor every replacement at line 363. It then waits for replacement readiness. Readiness rejects the parent's own health (lines 64–110).ocx.pidandruntime-port.jsonuntil exit cleanup (src/cli/index.ts:664–665).Replacement start path
handleStart(src/cli/index.ts:437–483) consumes the markers throughtakeRestartHandoffMarkers.probeOwnerPastRestartParent(src/cli/restart-handoff.ts:79–110) anddecideStartWithLiveOwner(src/cli/dispatch.ts:1110–1125).recoverStartStateUnderOwnershipLease(src/cli/start-owner-fence.ts).Revision caveat
88aaa754does not contain commit7a1565bbin its available shallow ancestry.devblobs match7a1565bb.7a1565bb, the following code exists:ProxyLivenessObservation(live | absent | unknown) andobserveProxyLiveness, at7a1565bb:src/server/proxy-liveness.ts:474–494.7a1565bb:src/cli/start-owner-fence.ts:6–21.7a1565bb:src/cli/start-owner-fence.ts:23–49.Hostname and Grok path
LiveProxy.hostnameis documented as the raw bind hostname (src/server/proxy-liveness.ts:152–158).probeHostnamenormalizes wildcard binds to127.0.0.1only for dialing (lines 218–223).probePortOwneralready overloadshostnameas the responding address (lines 591–601).live.hostname(src/cli/index.ts:853–860).reconcileEnsureDesiredIntegrations, then tosyncGrokConfig, then to the Grok injector.src/grok/inject.ts:1142–1165).Tests
Tests live in subsystem directories, as defined in
AGENTS.md:14–28. New test files need layout-map coverage.Relevant suites:
tests/cli/cli-restart-handoff.test.tstests/cli/cli-start-owner-absence.test.tstests/server/proxy-liveness-recovery.test.tstests/server/system-restart.test.tstests/server/restart-replacement.test.tstests/lib/system-restart-contract-security.test.tstests/providers/xai/grok-sync.test.tsFile-Level Change Summary and Follow-Up Agent Instructions
Product files
src/lib/system-restart-contract.tswith/takehelpers. The marker must equal the validated parent PID and is always consumed.src/cli/restart-handoff.tstakeRestartHandoffMarkersalso returns the validated stopped-parent PID. Do not changeprobeOwnerPastRestartParent.src/server/management/system-restart.tswaitForHealthis true, for a completed drain after a successful listener stop. Verify that all fallback branches passfalse.src/server/restart-replacement.tssrc/server/proxy-liveness.tsrestart-parent-stoppedobservation kind and refusal-only gating. AddLiveProxy.probeAddress. Return bind scope inhostnamefromobserveProxyLivenessandprobePortOwner.src/cli/start-owner-fence.tssrc/cli/index.tshandleStart, but not fromhandleEnsure. Verify that exit cleanup is owner-conditional. Correct the provenance comment at lines 853–856.Test files
tests/lib/system-restart-contract-security.test.tstests/server/proxy-liveness-recovery.test.tstests/server/proxy-liveness.test.ts(lines 301–313, the expectation update)tests/cli/cli-start-owner-absence.test.tstests/server/system-restart.test.tstests/server/restart-replacement.test.tstests/cli/ortests/clients/Instructions for follow-up agents
devwith7a1565bbin the ancestry. Do not implement on88aaa754.unknowntoabsent. The only new start permission isrestart-parent-stopped. Require the validated actual parent, the matching marker, matching records, and recognized connection refusal on every candidate.LiveProxy.hostnameas bind scope. Put the responding address only inprobeAddress.syncGrokConfigand the real injector in the P2 composition test.🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
@coderabbitai <feedback>Start Coding →
fix(restart): retire drained parent ownership and preserve bind scope (…
lidge-jun commentedon Oct 28, 2026
Fixed on
devby #7874 (mergef29091a5).hostname, admission policy) and the responding address (probeHostname, destination only) separately through ensure, reconciliation and Grok sync; regression intests/server/proxy-liveness-bind-scope.test.ts.Recorded as untested: a full dashboard HTTP/auth/provider-boot process-replacement E2E, and native POSIX/macOS execution of the handoff. The hosted macOS/Windows matrix runs in the next
lane=alldev batch.github-actions commentedon Oct 28, 2026
Automated translation bookkeeping — detected language: English.