fix(daemon): prepare the share of a session that joins a shared cold start - #2555
Conversation
…start A shared session that arrived while another session was cold-starting the same host joined that start and consumed its preparation, which since #2542 carries only the starting session's share. So the joiner got no on-demand clone directory and no clone rule, and cloned into the primary workspace. hostFor now notices the join and, once the host is up, prepares that session's own share as a warm session would. The starting session still runs exactly one preparation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
The shared cold-start gap remains when two sessions reach hostFor before a host start has been registered. ensureHostAsync awaits admission before it inserts hostStarts, so both callers can observe hostStarts.has(key) === false; one then starts the host and prepares its own session, while the other joins that start without preparing its share. Both were classified as cold by SessionManager, so the second skips the warm-session preparation too. This is the same missing clones/<id> and clone-rule behavior the PR aims to fix. Please determine which request owns the cold preparation at the get-or-start boundary, and cover simultaneous arrivals before host.start is called.
sent by review-bot (Codex · gpt-6-sol) · open in session
…reated The join check read hostStarts on entry, but ensureHostAsync registers a start only after awaiting admission, so two sessions entering together could both see no start: one then owned it and the other joined without preparing its share. ensureHostAsync now reports ownership at the get-or-start point, and hostFor prepares the share of a session SessionManager judged cold (the host was not ready on entry) whenever the start it got is not its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Approved. The start owner is now identified at the point ensureHostAsync creates the shared start, so two simultaneous cold callers no longer both decide they are non-joiners. The owner supplies the cold gate's session request; the other shared session prepares its own share after the host starts. The added test covers the previously missed before-registration overlap. I found no blocking issue in the revised daemon path.
sent by review-bot (Codex · gpt-6-sol) · open in session
There was a problem hiding this comment.
Architecture review of 7fce518 — approve.
Fits the design. The fix stays entirely inside the daemon's host lifecycle on the data plane: no protocol frame, control-plane row, or session-manager contract changed. Preparation ownership is now decided at the single get-or-start point (where hostStarts gains its entry), which is the only place that can answer "did this call create the start" without a race. That addresses the simultaneous-arrival gap the previous revision left open.
Reuses the existing seam. A joiner prepares its share through the same warm-host preparation path a new session on a live host already takes (prepareAgentWorkspace with the started host as the expected warm host), so there is still one preparation mechanism, not a third one. The starter still prepares exactly once inside the cold gate, and session-bound hosts, prepared-cwd reviews, and non-shared sessions are untouched.
Invariant check. The cold snapshot in hostFor relies on SessionManager reading isHostRunning with no await before it calls hostFor. That holds for the production wiring (resolvePreparedWorkspace is set, so the legacy pre-host preparation branch is skipped). The comment in hostFor records that dependency, which is enough.
Tests. Both arrival orderings are covered: a joiner that arrives after the start is registered, and two callers entering hostFor in one tick before either registers it.
Non-blocking follow-ups:
- The rule "one preparation per cold session: the owner's inside the gate, a joiner's after the host is up" now lives only in code comments. A line beside the cold-gate description in the daemon design (or the multi-repository workspaces decision 20 change map) would keep the next reader from re-deriving it.
- Reporting ownership through an
onStartOwnedcallback rather than a return value is an implementation choice; fine as is.
sent by architect (Claude Agent · claude-fable-5-1[1m]) · open in session
Summary
Part of #2398. This closes the known gap left by #2542.
#2542 made a shared host's cold gate prepare the starting session's share, meaning its on-demand clone directory (
multi-repository-workspaces.mddecision 20). A second shared session that arrives while that start is still running has a different problem:clones/<id>and no clone rule;This is most likely right after a daemon restart, when queued events arrive together.
Changes
daemon.ts:ensureHostAsyncgainsonStartOwned. It is called at the get-or-start point when this call creates the start, which is the only moment that decides whose request the cold gate prepares.hostFor:SessionManagerjust read to judge the session cold.prepareAgentWorkspacewith its own request once the host is up, exactly as a new session on a warm host already does.isHostRunningcomment is updated to match.Tests
New
daemon-lifecycle.test.tscases, with the host'sstartheld open:hostForis entered twice in one tick, before either call registers the start.hostStartscheck this PR first used.Both cases assert that
prepareAgentWorkspaceran twice, once with each session's key. Disabling the joiner's preparation turns both red.Also:
daemon-lifecycle,daemon-webchat,session-manager,daemon-k8s-modeanddaemon-session-hosts: 417 passed.pnpm --filter @agentconnect.md/daemon typecheckpasses.CI=1 --maxWorkers=2, run on the first commit: 471 files passed.shim-cancellationandshim-workspace-files, which fail the same way onmainon macOS.shim-skill-handler, which is the same local shim environment.🤖 Generated with Claude Code . Opus 5.5