Repository navigation
Conversation
Track initial acquisition alongside reconnect work and complete closed only once all owned cleanup has settled. Register tasks before callbacks can reenter, and keep explicit cancellation out of the fatal error path. Close Desktop candidates that fail admission before handoff, and prevent removed target generations from reactivating or publishing stale failures. Generated-by: Codex
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Full review of 51961a91 (one commit; +398/-35, 4 files). This PR hasn't been reviewed before, and it doesn't link an issue.
What changed.
RuntimeHostReconnectLifecyclenow owns the initial acquisition as#startTask, registered before any callback can re-enter.close()joins it, alongside the reconnect task and discard chain.close()also closes an unstartedinitialresource.#failPermanentlyroutes throughclose(), soclosedresolves only after the acquisition tasks settle.- In Desktop, the ready branch of
connectchecks abort and target ownership beforecompleteRegistration(), and closes a candidate rejected before handoff. - A new
#ownsTargetguard (manager not closed, and the generation is still the mapped one) gates#markUnavailable,onFatalError,#activate, and#publishState.
Correctness and races. I walked through the re-entrancy cases:
close()from a listener during#install. The task is already registered,#closeawaits#startTask, and#startreturns right after#install, so there is no cycle.- Permanent failure inside
#start.#failPermanently→void close()→#closeawaits#startTask, which settles when#startrethrows. The same holds inside#reconnect. No deadlock in either case. - Late resource after abort.
#installsees#closedand queues#discard, and#closeawaits#discardTaskonly after joining the start and reconnect tasks. The ordering is correct. - Errors after
close().waitForCurrentstill prefers#terminalErrorover the generic closed error, so callers keep the real failure. - Desktop.
#removeTargetdeletes the map entry beforelifecycle.close(). The late#markUnavailable/onFatalErrorfrom a cancelled start is therefore suppressed, while observations are still closed by#removeTarget. This matches the intent that explicit shutdown no longer publishes a fatal startup failure. The per-attempt IPC handlers belong to the candidate, socandidate.close()on the new throw path releases them.
Shutdown ordering and hangs. Manager close() now waits for an in-flight initial connect, where it previously returned without joining it. disposeRuntimeHostDesktop (runtime-host-boot.ts:2117) has no timeout around runtimeHostManager.close(). For local targets, connectOrSpawnRuntimeHost receives the signal plus connect and handshake timeouts, and the reconnect path already relied on abort-aware connects. Residual risk remains in createDesktopRuntimeHostCandidate's post-handshake RPCs, which take no signal. See the inline note (P3).
Findings:
- P3:
reconnect-lifecycle.ts:317.close()now has no upper bound on the initial-acquisition join, and Desktop quit has no outer timeout. - P3 (tests):
runtime-host-desktop-manager.ts:1532, and likewise:757and:1447. The new#ownsTargetguards are only exercised with the manager closed. Nothing covers a replaced generation while the manager is still open (retryLocalStart,enablereplacing an existing profile,disable): a stale fatal error, a stale#activate, or a stale state publication.#activatenow returns silently instead of activating or throwing. That is fine for the current callers, but a regression there would be invisible.
Local verification. I built at the head in a temporary worktree:
reconnecting-connection.test.jspasses 31/31, and Desktopruntime-host-desktop-manager.test.jspasses 63/63.- With
reconnect-lifecycle.tsswapped back to main, 5 of the new lifecycle tests fail: caller and re-entrant close, abort cleanup, unstarted initial, and repeated start. The permanent-failure case passes as a control. This is consistent with the PR body's "6 failures across lifecycle/manager".
Protocol: not touched, so no epoch change is needed.
Status:
- Mergeability: MERGEABLE (merge state BLOCKED, review required). The head is on current main
3597abe8;git merge-treeandgit diff --checkare clean. - CI: hosted
testwas still in progress at review time (labelpasses). This review does not reflect its result.
I found no blocking issues. This review is not merge approval.
The ownership guards were only exercised with the manager closed. The initial Local start is not a serialized target mutation, so once it marks its generation invalid, retryLocalStart can replace that generation while the old #markUnavailable still waits on observation teardown. Hold that teardown open from the startCandidate seam, let the retry reach ready, then release the old generation. Its epoch stays inactive and its late unavailable state is not published over the replacement. Without the #publishState ownership guard, the old generation publishes unavailable after the replacement is ready.
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Incremental review 51961a91 → e197faf5 (one commit: e197faf58, tests only). The merge-base is still 3597abe8 (current main).
Prior findings
- P3, unbounded initial-connection join in
close()(reconnect-lifecycle.ts:317): not changed, and the reason given is accepted. I checked the author's argument. Liveness probing starts when the connection is constructed after the handshake (connection.ts:475, timeouts of 8 s local and 45 s peer). So the post-handshake RPCs understartCandidatereject when the Host is wedged. A bounded join here would letclosedresolve before the start settles. A quit-time bound indisposeRuntimeHostDesktopis a reasonable follow-up. - P3 (tests), stale-generation guards only exercised with the manager closed: partly fixed.
- The new test (
runtime-host-desktop-manager.test.ts:1962) hasretryLocalStartreplace a failed Local generation while that generation's#markUnavailableis still pending and the manager stays open. - It asserts that the late unavailable state is not published, that the old epoch is inactive and not owned, and that the replacement stays ready. Per the author, it fails without the
#publishStateguard. - Still uncovered: a replaced generation's late ready result reaching
#activate(:1532), and replacement throughenableordisableof a remote profile. This doesn't block.
- The new test (
New code: test only, and I found no issues.
Protocol: not touched, so no epoch change is needed.
Status: MERGEABLE (merge state BLOCKED, review required). git merge-tree and git diff --check are clean. At review time, hosted CI test on e197faf5 was still in progress (run 37709625046), so this review does not reflect its result. I did not run the suites locally. This review is not merge approval.
|
On the remaining guard coverage: with the manager open, a replaced generation's late ready reaching 🤖 Addressed by Claude Code |
Summary
Closing Desktop during its first Host connection could finish before acquisition and cancellation cleanup. A late ready candidate could then fail IPC registration after the router closed, leaving the candidate unclosed and publishing stale failure state.
Make the shared reconnect lifecycle own and join initial acquisition, including late resource cleanup. Register tasks before callbacks can reenter, and complete
closedthrough one teardown path. Desktop closes candidates rejected before handoff and checks target generation ownership before activation or state publication. Explicit shutdown cancellation no longer reports a fatal startup failure.Verification
psspawning (EPERM). Those cases passed in a controlled rerun without the outer sandbox, followed by the passing full Runtime Host run above. No product changes were made to suppress them.AI use
Select exactly one:
Tool(s) and scope: Codex, including GPT-6 Astra design/review subagents, contributed investigation, implementation, behavior tests, validation, and this description. The commit includes
Generated-by: Codex. Final review and merge remain with human maintainers.Checklist
Does this PR entail a change in behavior?