Skip to content

fix(daemon): prepare the share of a session that joins a shared cold start - #2555

Merged
zfy0701 merged 2 commits into
mainfrom
claude/cold-start-joiner-clones
Sep 26, 2026
Merged

zfy0701 merged 2 commits into
mainfrom
claude/cold-start-joiner-clones

Conversation

@zfy0701

@zfy0701 zfy0701 commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

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.md decision 20). A second shared session that arrives while that start is still running has a different problem:

  • it joins the same start promise and consumes its preparation;
  • that preparation is not its own, so it gets no clones/<id> and no clone rule;
  • an agent with only an installation grant then clones into its primary workspace.

This is most likely right after a daemon restart, when queued events arrive together.

Changes

daemon.ts:

  • ensureHostAsync gains onStartOwned. 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:
    • It records whether the host was ready on entry. That is the same state SessionManager just read to judge the session cold.
    • A cold shared session whose start it did not create runs prepareAgentWorkspace with its own request once the host is up, exactly as a new session on a warm host already does.
  • Unaffected. The owner still prepares exactly once, inside the gate (feat(web): unify IM and GitHub trigger bars #118). Session-bound hosts, requests with a prepared cwd (reviews) and non-shared sessions are unchanged.
  • Comment. The isHostRunning comment is updated to match.

Tests

New daemon-lifecycle.test.ts cases, with the host's start held open:

  • "gives a session that joins a shared cold start its own preparation once the host is up": a second session is dispatched while the start waits.
  • "gives each of two sessions arriving together at a cold shared host its own preparation": hostFor is entered twice in one tick, before either call registers the start.
    • It fails with the entry-time hostStarts check this PR first used.

Both cases assert that prepareAgentWorkspace ran twice, once with each session's key. Disabling the joiner's preparation turns both red.

Also:

  • daemon-lifecycle, daemon-webchat, session-manager, daemon-k8s-mode and daemon-session-hosts: 417 passed.
  • pnpm --filter @agentconnect.md/daemon typecheck passes.
  • Full daemon suite, CI=1 --maxWorkers=2, run on the first commit: 471 files passed.
    • The 12 failures are in shim-cancellation and shim-workspace-files, which fail the same way on main on macOS.
    • The one unhandled rejection is a shim exec timeout in shim-skill-handler, which is the same local shim environment.

🤖 Generated with Claude Code . Opus 5.5

…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>
@zfy0701
zfy0701 enabled auto-merge (squash) September 26, 2026 20:31

@agentconnect-md-test agentconnect-md-test 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.

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

Comment thread packages/daemon/src/daemon.ts Outdated
…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>

@agentconnect-md-test agentconnect-md-test 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.

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

@agentconnect-md-test agentconnect-md-test 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.

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 onStartOwned callback 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

@zfy0701
zfy0701 merged commit c40fcc5 into main Sep 26, 2026
16 checks passed
@zfy0701
zfy0701 deleted the claude/cold-start-joiner-clones branch September 26, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant