Skip to content

fix(dispatch): name target worktrees by project and allow follow-up turns - #1955

Merged
bobleer merged 2 commits into
GCWing:mainfrom
bobleer:fix/dispatch-worktree-naming-and-followup-turns
Aug 1, 2026
Merged

fix(dispatch): name target worktrees by project and allow follow-up turns#1955
bobleer merged 2 commits into
GCWing:mainfrom
bobleer:fix/dispatch-worktree-naming-and-followup-turns

Conversation

@bobleer

@bobleer bobleer commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Two problems that only showed up once dispatch was used for real work.

Target worktrees were named after the job, not the project

A dispatch from BitFun produced a checkout at
~/.bitfun/dispatch/worktrees/dispatch-3d82ff46-bbf9-44c3-b4c3-110ddb9a4fd4
a directory whose name says nothing about the project it came from.

It now follows the same convention a local managed worktree already uses:

~/.bitfun/dispatch/worktrees/<repoKey>/<project>-<short job id>

repoKey groups every checkout of one source repository under its shared clone,
and the leaf is recognizable at a glance.

The project name is advisory input from the controller, so the target does not
trust it for path construction: it is sanitized, falls back to the remote URL's
basename and then to a constant, and the store independently rejects anything
that is not a single safe path component. ../../etc, .git, and an empty
label all fail closed rather than shaping the path.

The resolved path is recorded in the provision record and read back from there.
That is also what lets retention still find a checkout now that its name no
longer contains the job id — the record is the only link back to it, so it is
removed before the record that points at it.

A dispatch session refused every message after the first

Sending a second message produced "This detached dispatch job has already been
submitted"
, because a job was modelled as a single exchange.

A dispatch session is a conversation:

  • while a turn is running, a message is an append that steers it;
  • once it has finished, a message is a continue that starts the next turn.

continue rewinds only the job's run state to queued and clears the runtime
turn id, so a fresh detached worker picks it up. Everything that makes the
session a session survives: the target session, its worktree, and its
append-only event log. The worker now restores the target session instead of
creating it, which is what gives a follow-up turn the earlier turns as context —
creating unconditionally also failed outright on the second turn, because the
persisted session id already existed.

Because the event log stays per job rather than per turn, the controller's
cursor, transcript cache, and projection are unchanged: the observer keeps
reading one growing transcript.

Follow-ups carry a caller-generated turn id and are claimed under the job lock
together with the runtime turn id, so an ambiguous response resolves to the same
turn instead of starting a second one, and a crash settles the job as failed
rather than replaying a prompt whose side effects may already have happened.
A running job rejects a follow-up outright — steering a live turn is what
append is for.

Verification

  • cargo check --workspace, cargo check -p bitfun-desktop
  • cargo test -p bitfun-cli --bin bitfun dispatch:: — 64 passed
  • cargo test -p bitfun-core --lib service::dispatch — 37 passed
  • pnpm run type-check:web, pnpm run lint:web (0 errors; 2 pre-existing warnings)
  • pnpm --dir src/web-ui run test:run src/features/dispatch src/flow_chat/services/flow-chat-manager — 191 passed
  • pnpm run i18n:audit, pnpm run check:repo-hygiene
  • cargo clippy on the touched crates is clean; the remaining workspace clippy
    failures are pre-existing on main (execution_engine.rs,
    instruction_context.rs, agent-runtime-ipc tests) and untouched here.

New tests cover the naming convention and its rejection of hostile labels, the
requeue/claim-once contract for follow-up turns, retry idempotence, and the
refusal to queue one underneath a running turn.

bobleer added 2 commits August 1, 2026 03:48
…urns

Two problems showed up once dispatch was used for real work.

A target checkout was created at `worktrees/<jobId>`, so a dispatch from
BitFun landed in a directory whose name said nothing about BitFun and
carried a full job UUID. It now follows the same convention as a local
managed worktree — `worktrees/<repoKey>/<project>-<short job id>` — with
the project name grouped under its repository's shared clone. The label
is advisory input from the controller: the target sanitizes it, falls
back to the remote URL's basename and then to a constant, and rejects
anything that is not a single safe path component, so the path is never
shaped by an untrusted string. The resolved path is recorded in the
provision record and read back from there, which is also what lets
retention find the checkout now that its name no longer contains the job
id.

A dispatch session also refused every message after the first, because a
job was modelled as one exchange. It is a conversation: while a turn runs
a message steers it through `append`, and once it has finished a message
starts the next turn through the new `continue` verb. That rewinds only
the job's run state, so the target session, its worktree, and its
append-only event log all persist — the worker restores the session
instead of creating it, which is what gives a follow-up the earlier turns
as context, and the controller's cursor and transcript cache keep reading
one growing transcript. Follow-ups carry a caller-generated turn id and
are claimed under the job lock, so an ambiguous response resolves to the
same turn rather than starting a second one.
A follow-up turn only reaches session creation when its restore failed,
and creating then fails on the already-persisted id. Carrying the restore
error into that context makes the report name the real cause instead of
the "already exists" symptom it produces.
@bobleer
bobleer merged commit ef8cea4 into GCWing:main Aug 1, 2026
7 checks passed
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