fleetd #249: withhold agentSessionId for a non-provisioned opencode cwd #253

Closed
agent wants to merge 0 commits from worker/fleetd-249-7a7878-2 into main
Member

fleetd #249 — withhold agentSessionId for a non-provisioned opencode cwd

What changed and why

OpenCodeSessionDiscovery.sessionIdForDirectory keys on the worker's cwd. That is reliable only
when fleetd provisioned a fresh, unique git worktree for the member. fleet_spawn only provisions
one when the caller asks for it — the default spawn inherits the lead's own long-lived cwd, shared
with every other member and every past session ever run there. "Most recently updated row for this
directory" can then pick a stranger's session (measured 2026-09-03: a row three days old, belonging
to a different profile). fleet_list's own tool description told a lead this id was always safe to
pass as fleet_spawn{resumeSessionId} — a lead acting on that would silently load a stranger's
conversation into a working member.

Per the ticket's scope note, the directory heuristic itself is untouched (already established
unfixable at that layer by #234). Instead:

  1. Moved isProvisionedWorktree from ClaudeCodeLauncher (private) to the shared
    HerdrPeerLauncher base (package-private) — both adapters need it now, and both are in the
    same package.
  2. OpenCodeLauncher.SessionAwareHandle.agentSessionId() now withholds the id — returns null
    rather than guessing — for any member whose cwd is not a isProvisionedWorktree. Computed once
    at spawn time (cwd never changes for a handle) and checked after the existing
    memberHerdrSocket-unavailable gate (that OS-user mismatch makes discovery unusable regardless
    of worktree, so it keeps its own WARN either way) — criterion 1.
  3. OpenCodeLauncher.spawn() now refuses a resumeSessionId spawn outright when the target cwd
    is not a provisioned worktree, throwing IllegalArgumentException naming why, before anything
    spawns — criterion 3. opencode's -s <id> flag itself resumes precisely; the problem is that
    without a worktree fleetd can never again verify or re-report that identity, so accepting the
    resume request at all would let it silently land somewhere fleetd can't confirm.
  4. fleet_list/fleet_spawn tool descriptions (FleetMcp.java) corrected — agentSessionId
    is now documented as present only when fleetd can reliably re-identify the member (i.e. it was
    spawned into a fleetd-provisioned worktree), not "always safe to resume from" — criterion 4.
  5. Claude Code is unaffected: it mints its own UUID at spawn and never uses the directory
    heuristic, so its resume path (ClaudeCodeLauncher) is unchanged.

Scope note (not touched, per the ticket)

OpenCodeSessionDiscovery.sessionIdForDirectory's SQL and heuristic are unchanged, as instructed.

Tests — real path, not the discovery class alone (criterion 5)

All new/updated tests drive OpenCodeLauncher.spawn() → the real SessionAwareHandle, the same
object fleet_list/fleet_spawn actually call through — never OpenCodeSessionDiscovery directly.

  • theHandleNeverReportsAnIdForANonProvisionedCwdEvenAfterARowAppears (new): a member spawned into
    a non-worktree cwd stays null even after a matching DB row appears for that exact directory.
  • theHandleDiscoversTheSessionIdForTheWorkersCwdOnlyAfterItAppears (updated): the positive case —
    spawned into a provisioned worktree, the id still resolves once the row appears — proves criterion
    1's fix didn't regress criterion 2's working case.
  • aResumeSpawnWithoutAProvisionedWorktreeIsRefused (new): resumeSessionId + no worktree throws
    IllegalArgumentException naming "worktree", and no pane/process is started (agent.start never
    called).
  • aResumeSpawnIntoAProvisionedWorktreePassesTheSessionIdAsDashS (renamed/updated from
    aResumeSpawnPassesTheSessionIdAsDashS): resume + worktree still passes opencode's -s <id> flag
    through — the working case is preserved.
  • 13 existing fleetd #175/#234 model-mismatch tests updated to spawn into a provisioned worktree
    fixture (provisionedWorkDir helper) instead of the placeholder "/work/dir" string, which the
    new gate would otherwise (correctly) blank out — these tests are about the model-mismatch/
    late-resolve machinery, not the worktree gate itself, so the fixture change doesn't alter what
    they demonstrate.
  • discoveryIsUnavailableUnderMemberHerdrSocketEvenWhenARecordExists needed no change: the
    memberHerdrSocket-unavailable check runs before the new worktree gate, so it still exercises the
    WARN-once path on its unprovisioned fixture cwd.

Mutation testing (both gates)

Gate 1 — agentSessionId() withholding (OpenCodeLauncher.java:812, if (!worktreeProvisioned)):
mutated to if (false). theHandleNeverReportsAnIdForANonProvisionedCwdEvenAfterARowAppears went
RED — expected: <null> but was: <ses_someone_elses> at OpenCodeLauncherTest.java:448. 0 compile
errors. Reverted; diff -q confirmed byte-identical to the pre-mutation file.

Gate 2 — resume refusal (OpenCodeLauncher.java:683, the resumeSessionId/worktree check):
mutated to prepend false && to the condition. aResumeSpawnWithoutAProvisionedWorktreeIsRefused
went RED — "Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown." at
OpenCodeLauncherTest.java:379. 0 compile errors. Reverted; diff -q confirmed byte-identical.

Build

mvn clean install (unpiped), full output read: BUILD SUCCESS, Tests run: 1231, Failures: 0, Errors: 0, Skipped: 0.

Caveats for review

  • I read the full gitea issue #249 (not just the ticket summary) to resolve ambiguity around
    acceptance criterion 3 ("refused ... rather than silently resuming something"). I interpreted it
    as: refuse the spawn itself when resumeSessionId is combined with a non-provisioned cwd on an
    opencode profile, since fleetd can never verify or re-report that member's identity afterward
    either way. This is a judgment call on an underspecified criterion — worth a second look.
  • Out of scope, not touched: ClaudeCodeLauncher's own resume path never had this problem (it mints
    its own UUID), so no analogous gate was added there.
  • Not investigated further (per the brief's "note, don't fix" instruction): I did not go looking for
    other places in the codebase where a value reported to the lead outruns what the daemon can
    actually support — none surfaced incidentally while working this ticket, but I didn't do a
    separate sweep for the pattern.
## fleetd #249 — withhold agentSessionId for a non-provisioned opencode cwd ### What changed and why `OpenCodeSessionDiscovery.sessionIdForDirectory` keys on the worker's cwd. That is reliable only when fleetd provisioned a fresh, unique git worktree for the member. `fleet_spawn` only provisions one when the caller asks for it — the default spawn inherits the lead's own long-lived cwd, shared with every other member and every past session ever run there. "Most recently updated row for this directory" can then pick a stranger's session (measured 2026-09-03: a row three days old, belonging to a different profile). `fleet_list`'s own tool description told a lead this id was always safe to pass as `fleet_spawn{resumeSessionId}` — a lead acting on that would silently load a stranger's conversation into a working member. Per the ticket's scope note, the directory heuristic itself is untouched (already established unfixable at that layer by #234). Instead: 1. **Moved `isProvisionedWorktree`** from `ClaudeCodeLauncher` (private) to the shared `HerdrPeerLauncher` base (package-private) — both adapters need it now, and both are in the same package. 2. **`OpenCodeLauncher.SessionAwareHandle.agentSessionId()`** now withholds the id — returns `null` rather than guessing — for any member whose cwd is not a `isProvisionedWorktree`. Computed once at spawn time (cwd never changes for a handle) and checked *after* the existing `memberHerdrSocket`-unavailable gate (that OS-user mismatch makes discovery unusable regardless of worktree, so it keeps its own WARN either way) — criterion 1. 3. **`OpenCodeLauncher.spawn()`** now refuses a `resumeSessionId` spawn outright when the target cwd is not a provisioned worktree, throwing `IllegalArgumentException` naming why, before anything spawns — criterion 3. opencode's `-s <id>` flag itself resumes precisely; the problem is that without a worktree fleetd can never again verify or re-report that identity, so accepting the resume request at all would let it silently land somewhere fleetd can't confirm. 4. **`fleet_list`/`fleet_spawn` tool descriptions** (`FleetMcp.java`) corrected — `agentSessionId` is now documented as present only when fleetd can reliably re-identify the member (i.e. it was spawned into a fleetd-provisioned worktree), not "always safe to resume from" — criterion 4. 5. Claude Code is unaffected: it mints its own UUID at spawn and never uses the directory heuristic, so its resume path (`ClaudeCodeLauncher`) is unchanged. ### Scope note (not touched, per the ticket) `OpenCodeSessionDiscovery.sessionIdForDirectory`'s SQL and heuristic are unchanged, as instructed. ### Tests — real path, not the discovery class alone (criterion 5) All new/updated tests drive `OpenCodeLauncher.spawn()` → the real `SessionAwareHandle`, the same object `fleet_list`/`fleet_spawn` actually call through — never `OpenCodeSessionDiscovery` directly. - `theHandleNeverReportsAnIdForANonProvisionedCwdEvenAfterARowAppears` (new): a member spawned into a non-worktree cwd stays `null` even after a matching DB row appears for that exact directory. - `theHandleDiscoversTheSessionIdForTheWorkersCwdOnlyAfterItAppears` (updated): the positive case — spawned into a provisioned worktree, the id still resolves once the row appears — proves criterion 1's fix didn't regress criterion 2's working case. - `aResumeSpawnWithoutAProvisionedWorktreeIsRefused` (new): `resumeSessionId` + no worktree throws `IllegalArgumentException` naming "worktree", and no pane/process is started (`agent.start` never called). - `aResumeSpawnIntoAProvisionedWorktreePassesTheSessionIdAsDashS` (renamed/updated from `aResumeSpawnPassesTheSessionIdAsDashS`): resume + worktree still passes opencode's `-s <id>` flag through — the working case is preserved. - 13 existing fleetd #175/#234 model-mismatch tests updated to spawn into a provisioned worktree fixture (`provisionedWorkDir` helper) instead of the placeholder `"/work/dir"` string, which the new gate would otherwise (correctly) blank out — these tests are about the model-mismatch/ late-resolve machinery, not the worktree gate itself, so the fixture change doesn't alter what they demonstrate. - `discoveryIsUnavailableUnderMemberHerdrSocketEvenWhenARecordExists` needed no change: the `memberHerdrSocket`-unavailable check runs before the new worktree gate, so it still exercises the WARN-once path on its unprovisioned fixture cwd. ### Mutation testing (both gates) **Gate 1 — `agentSessionId()` withholding** (`OpenCodeLauncher.java:812`, `if (!worktreeProvisioned)`): mutated to `if (false)`. `theHandleNeverReportsAnIdForANonProvisionedCwdEvenAfterARowAppears` went RED — `expected: <null> but was: <ses_someone_elses>` at `OpenCodeLauncherTest.java:448`. 0 compile errors. Reverted; `diff -q` confirmed byte-identical to the pre-mutation file. **Gate 2 — resume refusal** (`OpenCodeLauncher.java:683`, the resumeSessionId/worktree check): mutated to prepend `false &&` to the condition. `aResumeSpawnWithoutAProvisionedWorktreeIsRefused` went RED — "Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown." at `OpenCodeLauncherTest.java:379`. 0 compile errors. Reverted; `diff -q` confirmed byte-identical. ### Build `mvn clean install` (unpiped), full output read: **BUILD SUCCESS**, `Tests run: 1231, Failures: 0, Errors: 0, Skipped: 0`. ### Caveats for review - I read the full gitea issue #249 (not just the ticket summary) to resolve ambiguity around acceptance criterion 3 ("refused ... rather than silently resuming something"). I interpreted it as: refuse the *spawn itself* when `resumeSessionId` is combined with a non-provisioned cwd on an opencode profile, since fleetd can never verify or re-report that member's identity afterward either way. This is a judgment call on an underspecified criterion — worth a second look. - Out of scope, not touched: `ClaudeCodeLauncher`'s own resume path never had this problem (it mints its own UUID), so no analogous gate was added there. - Not investigated further (per the brief's "note, don't fix" instruction): I did not go looking for other places in the codebase where a value reported to the lead outruns what the daemon can actually support — none surfaced incidentally while working this ticket, but I didn't do a separate sweep for the pattern.
agent added 1 commit 2026-09-03 07:52:40 +02:00
fleetd #249: withhold agentSessionId for a non-provisioned opencode cwd
CI / contract (pull_request) Successful in 54s
CI / build (pull_request) Successful in 1m52s
2afa3652bb
OpenCodeSessionDiscovery.sessionIdForDirectory keys on the worker's cwd, which
is reliable only when fleetd provisioned a unique git worktree for that
member. Without one (the default no-worktree spawn), the cwd is shared with
other sessions, and "most recently updated row for this directory" can pick a
stranger's session — fleet_list would then hand a lead an agentSessionId that
resumes someone else's conversation.

Move isProvisionedWorktree from ClaudeCodeLauncher to the shared
HerdrPeerLauncher base (both adapters need it now). OpenCodeLauncher.spawn now
refuses a resumeSessionId spawn outright when the target cwd is not a
provisioned worktree (fleetd can never verify or re-report that identity), and
SessionAwareHandle.agentSessionId() withholds the id — returns null rather
than guessing — for any member spawned without one, resumed or not. Corrected
fleet_list/fleet_spawn's tool descriptions, which previously implied
agentSessionId is always a safe resume handle.
ltms closed this pull request 2026-09-03 07:59:45 +02:00
Some checks are pending
CI / contract (pull_request) Successful in 54s
CI / build (pull_request) Successful in 1m52s

Pull request closed

Sign in to join this conversation.