feat(mcp): let agents change thread workspaces#8680

closed19
agents/mcp-workspaces/inventory
agents/mcp-workspaces/checkout
openedAug 29, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 22h ago

Agents need a safe way to move an existing thread to a branch, project root, existing checkout, or new worktree while keeping durable thread metadata aligned with actual Git state.

This dependent layer adds t3_thread_checkout over the existing VCS, V2 command, provider-session, and continuation workflows. It verifies requested refs, canonical repository/worktree identity, dirty state, active and shared ownership, and the final checked-out state before committing the binding. Physical workspace admission covers aliases and nested project roots. New-worktree handoff rechecks ownership after creation and retains any checkout whose cleanup ownership becomes uncertain.

Pre-mutation reads remain cancellable. Once mutation starts, the narrow Git/binding/rollback section preserves atomicity and truthful partial-failure outcomes. A successful move ends the current provider turn; continuationPrompt schedules the replacement turn in the selected workspace. Status broadcasting is refreshed after committed changes without allowing refresh failure to rewrite the completed result.

Focused validation:

  • 314 tests across the combined inventory/checkout stack, including real Git ref handling, cancellation, concurrent ownership, rollback retention, provider detach/continuation, contracts, registration, and presentation
  • vp run --filter t3 typecheck
  • vp run --filter @t3tools/contracts typecheck
  • vp run --filter @t3tools/shared typecheck
  • vp run --filter @t3tools/client-runtime typecheck
  • Targeted vp lint, vp fmt --check, and git diff --check

Depends on #8685 for canonical bounded workspace discovery. Native stack: #8685 โ†’ #8680, rooted on t3code/codex-turn-mapping at 415ed0f73b97f1655b6282492f81d0b2bba3a9cc.

Implemented by GPT-5.6-Sol via Codex in T3 Code.

Checks

All checks passed
Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

โš™๏ธ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a2fe674c-aa02-4f15-950f-20e9f297b529

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ๐Ÿ” Trigger review

Warning

Your free Security trial is over. An organization admin can activate Security or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

Synced from GitHub

Thread transfer impact

โš ๏ธ The latest CI run did not produce a thread transfer result for 84f260d.

This comment will update automatically after the next completed run.

apps/server/src/mcp/WorktreeMcpService.tsline 982Outdated ยท a671e35Open thread
โ€ฆโ€ฆ@@ -463,21 +631,654 @@ const make = Effect.gen(function* () {
979+ targetWorkspacePath === currentWorkspacePath ? "unchanged" : "reused";
980+ let createdBranch: string | null = null;
981+
982+ if (shouldMutateCheckout && requestedBranch !== undefined) {
MmacroscopeappBoton line 982Aug 29, 2026

๐ŸŸ  High mcp/WorktreeMcpService.ts:982

checkout can let transitions from different threads concurrently mutate the same project root or worktree, leaving the earlier thread's durable branch binding inconsistent with the branch Git leaves checked out and allowing two threads to share one mutable workspace. The per-thread workspaceTransitionsInFlight guard does not prevent this, and each dispatch validates only that thread's prior binding, so both optimistic updates can commit. Add workspace-level serialization or recheck workspace ownership after the Git mutation and before dispatch.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 982:
2
3`checkout` can let transitions from different threads concurrently mutate the same project root or worktree, leaving the earlier thread's durable branch binding inconsistent with the branch Git leaves checked out and allowing two threads to share one mutable workspace. The per-thread `workspaceTransitionsInFlight` guard does not prevent this, and each dispatch validates only that thread's prior binding, so both optimistic updates can commit. Add workspace-level serialization or recheck workspace ownership after the Git mutation and before dispatch.

Fixed in 98151e0c2e. Checkout now acquires an in-process guard for the canonical target workspace in addition to the per-thread guard, then rechecks Git inventory, status, and thread ownership while holding it. Added a two-caller race test targeting the same worktree.

apps/server/src/mcp/WorktreeMcpService.tsline 1129Outdated ยท a671e35Open thread
โ€ฆโ€ฆ@@ -463,21 +631,654 @@ const make = Effect.gen(function* () {
1126+
1127+ if (bindingChanged) {
1128+ const dispatchExit = yield* Effect.exit(
1129+ threadManagement.dispatch({
MmacroscopeappBoton line 1129Aug 29, 2026

๐ŸŸ  High mcp/WorktreeMcpService.ts:1129

performCheckout can leave Git inconsistent with the durable thread binding when another actor changes branch or worktreePath after the initial projection read. It switches the checkout before the optimistic dispatch at threadManagement.dispatch detects the stale binding, then rolls Git back to targetBefore while the concurrent metadata update remains committed; add a binding recheck immediately before mutating Git, as performHandoff does.

โœ… Resolved in 98151e0c2ee5b55e702164a7ae31136b96d351d9

Fixed in 98151e0c2e. Immediately before any Git mutation or binding move, checkout reloads the calling thread and rejects if branch, worktree path, or archive state changed. The metadata command still carries expectedBranch/expectedWorktreePath as the durable compare-and-set backstop. Added a regression test that changes the binding during preparation and verifies Git is untouched.

Addressed in 8aafc41636ec84adab80a84e289c7cefb0757f1b with a durable backstop rather than relying on another adjacent read. Workspace-binding metadata updates now carry optional expectedArchived: false alongside the branch/worktree CAS, and the production V2 decider checks all three inside its serialized command decision. A real V2 regression archives first, then dispatches the previously prepared workspace update and proves the binding is rejected and unchanged. Rollback also refuses to act once caller lifecycle or ownership differs.

The CAS and ownership-aware rollback improve recovery, but do not close the race: the recheck at 1176 is followed by resolveCommit calls before mutation. A concurrent actor can bind the target in that window with its observed branch, after which this checkout changes Git; rollback then correctly declines as unsafe, but that actorโ€™s durable binding can disagree with Git. The binding/ownership validation still needs to be adjacent to mutation, or all binding writers need a shared lock.

Fixed in 9335400d20. Ref/commit resolution now completes before a final adjacent gate re-reads the caller lifecycle, physical bindings across projects, and target ref/dirty state immediately before createRef/switchRef. The deterministic regression changes the caller binding during commit resolution and verifies checkout_in_progress with no Git mutation or metadata dispatch.

apps/server/src/mcp/WorktreeMcpService.tslines 776โ€“776Outdated ยท a671e35Open thread
โ€ฆโ€ฆ@@ -463,21 +631,654 @@ const make = Effect.gen(function* () {
773+ },
774+ projection,
775+ );
776+ const actual = yield* readWorkspaceStatus(handoff.worktreePath);
MmacroscopeappBoton lines 776โ€“776Aug 29, 2026

๐ŸŸ  High mcp/WorktreeMcpService.ts:776

checkout fails after performHandoff has durably created and bound the new worktree if this readWorkspaceStatus is interrupted or fails, so the caller receives no successful result even though the session is detached and the thread now points at the new worktree. Use the branch returned by performHandoff (or otherwise make this post-commit read best-effort) so status-read failure cannot turn a completed transition into a failed checkout.

โœ… Resolved in 98151e0c2ee5b55e702164a7ae31136b96d351d9

Fixed in 98151e0c2e. The new-worktree checkout result now uses the completed handoff receipt branch and no longer performs a post-commit status read that could misreport a successful durable handoff as failed. Added a regression test where the next status read would fail.

docs/user/source-control.mdlines 85โ€“85Outdated ยท 98151e0Open thread
โ€ฆโ€ฆ@@ -65,14 +65,24 @@ The **Source Control settings** page shows you exactly what's connected:
82+Moving between workspace paths restarts the agent session in the selected checkout. The agent can
83+queue a continuation before that restart, so longer work resumes without needing the browser to
84+stay open. These controls apply only to the calling thread and its current project. They do not
85+remove, prune, or revive worktrees.
MmacroscopeappBoton lines 85โ€“85Aug 29, 2026

๐ŸŸข Low user/source-control.md:85

A failed t3_worktree_handoff can remove the newly created target worktree, so this absolute statement misleads callers troubleshooting a failed handoff. The rollback calls gitWorkflow.removeWorktree when the pre-commit recheck or durable binding update fails; please qualify the documentation to distinguish that cleanup from normal worktree controls.

suggestion
1They do not remove, prune, or revive existing worktrees; a failed handoff removes a newly created target worktree when its pre-commit recheck or durable binding update fails.
Also found in 1 other location(s)

docs/orchestration-v2/orchestrator-mcp-server.md:345

t3_worktree_handoff does remove its newly created target worktree when the pre-commit recheck or durable binding update fails: removeCreatedWorktree calls gitWorkflow.removeWorktree on both rollback paths. Thus the absolute statement that neither tool removes a target worktree is incorrect and can mislead callers troubleshooting a failed handoff.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @docs/user/source-control.md around line 85:
2
3A failed `t3_worktree_handoff` can remove the newly created target worktree, so this absolute statement misleads callers troubleshooting a failed handoff. The rollback calls `gitWorkflow.removeWorktree` when the pre-commit recheck or durable binding update fails; please qualify the documentation to distinguish that cleanup from normal worktree controls.
4
5Also found in 1 other location(s):
6- docs/orchestration-v2/orchestrator-mcp-server.md:345 -- `t3_worktree_handoff` does remove its newly created target worktree when the pre-commit recheck or durable binding update fails: `removeCreatedWorktree` calls `gitWorkflow.removeWorktree` on both rollback paths. Thus the absolute statement that neither tool removes a target worktree is incorrect and can mislead callers troubleshooting a failed handoff.

Fixed in d1e40d08db. Both user and internal docs now distinguish existing-worktree controls from rollback cleanup: a failed new-worktree transition may remove only the checkout it just created when the durable binding did not commit.

docs/orchestration-v2/orchestrator-mcp-server.mdlines 337โ€“374Outdated ยท 98151e0Open thread
MmacroscopeappBoton lines 337โ€“374Aug 29, 2026

๐ŸŸข Low orchestration-v2/orchestrator-mcp-server.md:337

The documentation says moving to a shared project root is allowed when other threads are idle, but t3_thread_checkout rejects the root mutation whenever any other thread is bound there, including idle threads. Update the allowance to state that the move is rejected whenever another thread is bound to the root.

No longer relevant as of 1ed8ca43207f300ed83e1f991be080eb0b9f3e54

This is already accurate in d1e40d08db. For a project-root move without a branch switch, shouldMutateCheckout is false: the workspace_shared root guard applies only when shouldMutateCheckout is true, while the preceding workspace_in_use guard rejects the move only when another root-bound thread has an active run. An idle root owner therefore remains allowed exactly as documented.

apps/server/src/mcp/WorktreeMcpService.tsline 200Outdated ยท d1e40d0Open thread
โ€ฆโ€ฆ@@ -154,6 +194,61 @@ const make = Effect.gen(function* () {
197+ const loadActiveWorkspaceBindings = Effect.fn("WorktreeMcpService.loadActiveWorkspaceBindings")(
198+ function* (repositoryCommonDir: string) {
199+ const snapshot = yield* threadManagement
200+ .getShellSnapshot({ location: "active" })
MmacroscopeappBoton line 200Aug 29, 2026

๐ŸŸ  High mcp/WorktreeMcpService.ts:200

loadActiveWorkspaceBindings excludes archived threads, so an active thread can claim or switch an archived thread's worktreePath; after thread.unarchive, both threads retain usable bindings to the same mutable checkout. Include archived threads in this ownership recheck so unarchiving cannot create a shared worktree.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 200:
2
3`loadActiveWorkspaceBindings` excludes archived threads, so an active thread can claim or switch an archived thread's `worktreePath`; after `thread.unarchive`, both threads retain usable bindings to the same mutable checkout. Include archived threads in this ownership recheck so unarchiving cannot create a shared worktree.

Fixed in 5ef4d4c3499e4b6114850f44717f98ebc9d60a93. Canonical ownership discovery now combines active and archived shell snapshots before repository/worktree identity resolution, so an archived binding reserves the same physical checkout and blocks reuse. Added a regression proving an archived owner produces workspace_shared without Git mutation or binding dispatch.

apps/server/src/mcp/WorktreeMcpService.tsline 1329Outdated ยท 5ef4d4cOpen thread
โ€ฆโ€ฆ@@ -691,7 +880,744 @@ const make = Effect.gen(function* () {
1326+ actualBranch: null,
1327+ rollback: "not_possible",
1328+ },
1329+ );
CcursorBoton line 1329Aug 29, 2026

Detached remote checkout skips rollback

High Severity

Checking out a remote ref can succeed as a detached HEAD. switchRef then returns refName of null, which performCheckout reports as partial_failure with rollback: "not_possible" without restoring the previous branch. Git is left detached while the durable binding is unchanged. This is the documented real-Git outcome when origin/feature is requested and a same-named untracked local branch exists.

Reviewed by Cursor Bugbot for commit 5ef4d4c3499e4b6114850f44717f98ebc9d60a93. Configure here.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable โ€” This PR adds an agent-facing checkout workflow and changes existing handoff behavior across Git mutation, durable thread bindings, ownership arbitration, rollback, and provider-session detachment. The breadth and side effects of these production changes warrant human review.

No code changes detected at 84f260d. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

apps/server/src/mcp/WorktreeMcpService.tsline 1055Outdated ยท 8aafc41Open thread
โ€ฆโ€ฆ@@ -707,7 +894,770 @@ const make = Effect.gen(function* () {
1052+ : (selectedWorktreePath ??
1053+ (projection.thread.worktreePath !== null && selectedRef?.isDefault === true
1054+ ? projectWorktreeRoot
1055+ : currentWorkspacePath));
CcursorBoton line 1055Aug 29, 2026

Remote default checkout targets project root

High Severity

Auto workspace resolution sends an attached thread that requests the remote default ref (for example origin/main) to projectWorktreeRoot and then mutates that checkout. Remote refs always have a null worktreePath, and origin's HEAD is marked isDefault, so the local-default heuristic misfires. A non-default remote such as origin/feature correctly stays on the current worktree. The result is an unexpected branch switch or detach on the shared project root instead of checking out the requested remote ref in the thread's current workspace.

Reviewed by Cursor Bugbot for commit 8aafc41636ec84adab80a84e289c7cefb0757f1b. Configure here.

apps/server/src/mcp/WorktreeMcpService.tsline 933Outdated ยท 8aafc41Open thread
โ€ฆโ€ฆ@@ -707,7 +894,770 @@ const make = Effect.gen(function* () {
930+ : { continuationPrompt: input.continuationPrompt }),
931+ },
932+ projection,
933+ );
CcursorBoton line 933Aug 29, 2026

New worktree skips workspace serialization

Medium Severity

new_worktree calls performHandoff without taking the per-physical-workspace guard, and handoff never claims the created path. After createWorktree succeeds and before the binding commits, another thread can list the new checkout, pass ownership checks, and bind to it. Both threads then share one mutable worktree, which the new guards were meant to prevent.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 8aafc41636ec84adab80a84e289c7cefb0757f1b. Configure here.

apps/server/src/mcp/WorktreeMcpService.tsline 1433Outdated ยท 9335400Open thread
โ€ฆโ€ฆ@@ -725,7 +926,819 @@ const make = Effect.gen(function* () {
1430+ })
1431+ .pipe(asOperationFailed(`Unable to create branch '${requestedBranch}'`));
1432+ }
1433+ const switchExit = yield* Effect.exit(
MmacroscopeappBoton line 1433Aug 29, 2026

๐ŸŸก Medium mcp/WorktreeMcpService.ts:1433

When target.create successfully creates the branch but switchRef fails, the checkout returns operation_failed while leaving the newly created local branch behind. rollbackOwnedCheckout only restores checkout state and never calls deleteLocalBranch, so retries with the same branch are rejected as already existing. Clean up the created branch on this failure path when it is safe to do so.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 1433:
2
3When `target.create` successfully creates the branch but `switchRef` fails, the checkout returns `operation_failed` while leaving the newly created local branch behind. `rollbackOwnedCheckout` only restores checkout state and never calls `deleteLocalBranch`, so retries with the same branch are rejected as already existing. Clean up the created branch on this failure path when it is safe to do so.

Intentional conservative behavior in 05f8e6450d, matching the parent review direction. A failed switch does not establish safe ownership of the created ref: it may have become attached or advanced elsewhere, so deleting it to make the original create request retryable risks data loss. The result preserves the branch and reports the failure; a retry can select/reuse the retained branch after inspecting current state. The focused test verifies no delete is attempted.

apps/server/src/mcp/WorktreeMcpService.tsline 489Outdated ยท 9335400Open thread
โ€ฆโ€ฆ@@ -319,8 +476,14 @@ const make = Effect.gen(function* () {
486+ ),
324487 Effect.andThen(
325488 Effect.suspend(() =>
326489 gitWorkflow.deleteLocalBranch({
MmacroscopeappBoton line 489Aug 29, 2026

๐ŸŸ  High mcp/WorktreeMcpService.ts:489

Rollback can delete commits belonging to another actor. After removeWorktree succeeds, deleteLocalBranch runs git branch -D on worktree.worktree.refName without confirming that the branch still points to the commit created by this handoff, so a concurrent checkout or branch update can cause unrelated commits to be deleted. Verify the branch tip is unchanged before deleting it, or use a compare-and-delete operation.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 489:
2
3Rollback can delete commits belonging to another actor. After `removeWorktree` succeeds, `deleteLocalBranch` runs `git branch -D` on `worktree.worktree.refName` without confirming that the branch still points to the commit created by this handoff, so a concurrent checkout or branch update can cause unrelated commits to be deleted. Verify the branch tip is unchanged before deleting it, or use a compare-and-delete operation.

Fixed in 05f8e6450d. Failed handoff cleanup still removes the newly created worktree path, but no longer force-deletes the branch by name; the branch is retained because concurrent activity means this operation cannot prove the ref is still unattached and unchanged. Focused handoff race and dispatch-failure tests assert removal plus branch retention.

apps/server/src/mcp/WorktreeMcpService.tsline 258Outdated ยท 05f8e64Open thread
โ€ฆโ€ฆ@@ -162,34 +202,141 @@ const make = Effect.gen(function* () {
255+ inventory.value.currentWorktreeRoot === null
256+ ) {
257+ return [];
258+ }
CcursorBoton line 258Aug 29, 2026

Unresolved same-repo owners skipped

Medium Severity

loadActiveWorkspaceBindings drops a thread when inventory belongs to the target repository but currentWorktreeRoot is null. That is an unresolved same-repo owner, yet checkout continues as if nobody holds the checkout instead of failing closed.

Reviewed by Cursor Bugbot for commit 05f8e6450db277e831f923c4c278f790716a9090. Configure here.

apps/server/src/mcp/WorktreeMcpService.tsline 728Outdated ยท 05f8e64Open thread
โ€ฆโ€ฆ@@ -533,7 +725,7 @@ const make = Effect.gen(function* () {
534726 : null;
535727 const agreement =
536- !actual.isRepo && !workspaceExists
728+ !actual.isRepo && !workspaceExists && Option.isNone(workspaceInventory)
CcursorBoton line 728Aug 29, 2026

List omits archived checkout owners

Medium Severity

listWorktrees builds bindings only from listProjectThreads, which reads snapshot.threads and ignores archivedThreads. Checkout now treats an archived binding as occupying the physical worktree, so list can report available with no owners for a checkout that t3_thread_checkout will reject as workspace_shared.

Reviewed by Cursor Bugbot for commit 05f8e6450db277e831f923c4c278f790716a9090. Configure here.

apps/server/src/mcp/WorktreeMcpService.tsline 992Outdated ยท 0a622c2Open thread
โ€ฆโ€ฆ@@ -730,7 +927,818 @@ const make = Effect.gen(function* () {
989+ const [inventory, currentInventory] = yield* Effect.all(
990+ [loadWorktrees(projectWorkspaceRoot), loadWorktrees(recordedWorkspacePath)],
991+ { concurrency: 2 },
992+ );
CcursorBoton line 992Aug 29, 2026

Missing worktrees block all moves

Medium Severity

t3_thread_checkout and attached t3_worktree_handoff both require the thread's recorded workspace to still be a readable project worktree before any move. A workspace_missing binding therefore cannot return to the project root, reuse another listed worktree, or create a new one, even though the project repository itself is available.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 0a622c2b38dc25c215036226855d9392ff539540. Configure here.

Confirmed and fixed in 03d0e2bce4. A recorded checkout now resolves through the canonical binding inventory as optional: only a confirmed non-repository/missing source is treated as stale, while unresolved Git failures still fail closed. Recovery targets are always selected from the healthy project inventory, so return-to-root and listed-worktree reuse never touch the missing checkout; new-worktree handoff resolves its base from the physical project root. Focused regressions cover all three recovery paths.

apps/server/src/mcp/WorktreeMcpService.tsline 1057Outdated ยท 0a622c2Open thread
โ€ฆโ€ฆ@@ -730,7 +927,818 @@ const make = Effect.gen(function* () {
1054+ "target.create requires target.branch when checking out the project root.",
1055+ );
1056+ }
1057+ break;
CcursorBoton line 1057Aug 29, 2026

Project root skips branch checks

Low Severity

target.type='project_root' never applies the existence checks used for target.type='branch'. create=true on an existing local branch still counts as a mutation, so dirty and shared-root guards fire and createRef later fails. A missing branch without create also falls through to switchRef and surfaces as operation_failed.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 0a622c2b38dc25c215036226855d9392ff539540. Configure here.

Confirmed and fixed in 03d0e2bce4. project_root requests with a branch now resolve the same local/remote ref maps and reject create=true for an existing local branch or create=false for a missing ref before dirty/shared/mutation checks. The focused regression asserts both typed invalid_request outcomes and that neither createRef nor switchRef runs.

apps/server/src/mcp/WorktreeMcpService.tsline 1168Outdated ยท 4453207Open thread
โ€ฆโ€ฆ@@ -730,7 +954,851 @@ const make = Effect.gen(function* () {
1165+ return yield* failure(
1166+ "invalid_request",
1167+ `Checkout target '${targetWorkspacePath}' is not a git repository.`,
1168+ );
CcursorBoton line 1168Aug 30, 2026

Status failure reported as not a repo

Low Severity

When the recorded checkout inventory succeeds but the follow-up status read fails, that failure is turned into { isRepo: false }. A same-workspace checkout then rejects the target as not a Git repository instead of failing closed as an unresolved Git error.

Reviewed by Cursor Bugbot for commit 4453207fd2655b3debb17b3146e8bc45110f1958. Configure here.

Fixed in 1d545798de018346d5a02146a79727b602844f6d. Once inventory confirms the recorded checkout exists, the status read stays in the typed error channel and returns before ref selection or mutation. The synthetic result is now limited to inventory-confirmed missing/non-repository paths; the regression asserts no switch, ref creation, or metadata dispatch.

docs/orchestration-v2/orchestrator-mcp-server.mdlines 49โ€“50Outdated ยท 1d54579Open thread
โ€ฆโ€ฆ@@ -44,10 +44,10 @@ Before `ProviderSessionManager` opens a new V2 provider session, it asks
47+The credential grants `orchestration` and `worktree` capabilities. It grants `preview` only when
48+the provider session reports `browserToolsAvailable`; disabling agent browser access removes that
49+capability. Credentials expire after a maximum lifetime, expire when idle, and are revoked when
50+the provider session is released. The raw token is not persisted in orchestration state.
MmacroscopeappBoton lines 49โ€“50Aug 30, 2026

๐ŸŸก Medium orchestration-v2/orchestrator-mcp-server.md:49

The documentation promises that credentials expire after a maximum lifetime, but McpSessionRegistry.touch runs for provider turns, so an active session can keep its credential indefinitely; the stated security bound is not enforced. Remove the maximum-lifetime claim and document only the liveness-based expiry.

suggestion
1capability. Credentials expire when idle and are revoked when
2the provider session is released. The raw token is not persisted in orchestration state.
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @docs/orchestration-v2/orchestrator-mcp-server.md around lines 49-50:
2
3The documentation promises that credentials expire after a maximum lifetime, but `McpSessionRegistry.touch` runs for provider turns, so an active session can keep its credential indefinitely; the stated security bound is not enforced. Remove the maximum-lifetime claim and document only the liveness-based expiry.

Fixed in 76ccdb2bbb30da271f74fa152ba3c3df14409ba9. The docs now match McpSessionRegistry: resolve and provider-turn touch refresh lastAliveAt, pruning uses only livenessWindowMs, and provider-session release revokes eagerly. The conditional preview wording remains intact. Targeted formatter and diff checks pass.

apps/server/src/mcp/WorktreeMcpService.tsline 1465Outdated ยท 1d54579Open thread
โ€ฆโ€ฆ@@ -759,7 +983,851 @@ const make = Effect.gen(function* () {
1462+ cwd: targetWorkspacePath,
1463+ refName: targetBefore.refName!,
1464+ })
1465+ : Effect.void,
CcursorBoton line 1465Aug 30, 2026

Rollback ignores moved original branch

Medium Severity

rollbackOwnedCheckout attests the post-switch checkout, then switchRefs back to targetBefore.refName without checking that this ref still points at targetBeforeCommit. If that branch moved during the attempt, rollback lands on a different commit than the workspace started from, which breaks the rule that unattested Git state is left untouched.

Reviewed by Cursor Bugbot for commit 1d545798de018346d5a02146a79727b602844f6d. Configure here.

apps/server/src/mcp/WorktreeMcpService.tsline 1664Outdated ยท 1d54579Open thread
โ€ฆโ€ฆ@@ -759,7 +983,851 @@ const make = Effect.gen(function* () {
1661+ : projection.thread.worktreePath;
1662+ const bindingChanged =
1663+ nextBranch !== projection.thread.branch ||
1664+ nextWorktreePath !== projection.thread.worktreePath;
CcursorBoton line 1664Aug 30, 2026

Project-root binding not normalized

Medium Severity

A project_root target only writes worktreePath: null when the physical checkout changes. If the recorded path is a nested directory or alias of the project root, workspaceChanged is false and the durable binding keeps the old path. Later turns can keep using that nested cwd even though the caller asked to return to the main checkout.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 1d545798de018346d5a02146a79727b602844f6d. Configure here.

docs/user/source-control.mdlines 84โ€“86Outdated ยท 69afd25Open thread
โ€ฆโ€ฆ@@ -65,16 +65,27 @@ The **Source Control settings** page shows you exactly what's connected:
83+
84+Moving between workspace paths restarts the agent session in the selected checkout. The agent can
85+queue a continuation before that restart, so longer work resumes without needing the browser to
86+stay open. These controls apply only to the calling thread and its current project. They do not
MmacroscopeappBoton lines 84โ€“86Aug 30, 2026

๐ŸŸข Low user/source-control.md:84

The documentation incorrectly says every workspace-path move restarts the agent session, but a path-only checkout ends the current turn unless continuationPrompt is supplied; users must send another message to continue. Please document the conditional continuation behavior.

suggestion
1Moving between workspace paths ends the current agent turn in the selected checkout. When the agent queues a continuation before that transition, the session resumes without needing the browser to stay open.
๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @docs/user/source-control.md around lines 84-86:
2
3The documentation incorrectly says every workspace-path move restarts the agent session, but a path-only checkout ends the current turn unless `continuationPrompt` is supplied; users must send another message to continue. Please document the conditional continuation behavior.

Fixed in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284. The user guide now says a workspace-path move ends the current turn; continuationPrompt queues a replacement turn, while an omitted continuation leaves the thread idle until its next message. The internal MCP documentation uses the same conditional wording.

apps/server/src/mcp/WorktreeMcpService.tsline 1284Outdated ยท 69afd25Open thread
โ€ฆโ€ฆ@@ -759,7 +983,851 @@ const make = Effect.gen(function* () {
1281+
1282+ const ids = yield* transitionIds(scope, "checkout");
1283+ const workspaceGuardKey = `workspace:${inventory.repositoryCommonDir}:${targetWorkspacePath}`;
1284+ return yield* Effect.uninterruptibleMask(() =>
MmacroscopeappBoton line 1284Aug 30, 2026

๐ŸŸ  High mcp/WorktreeMcpService.ts:1284

performCheckout executes the entire guarded transition inside Effect.uninterruptibleMask without calling restore, so Git, status, inventory, and dispatch operations ignore cancellation. A stalled or abandoned request therefore keeps the per-thread and per-workspace guard entries until completion, causing subsequent calls to fail with checkout_in_progress. Use restore around the transition after acquiring the guard and registering its finalizer, as handoff does.

โœ… Resolved in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284

Fixed in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284 with a narrow cancellation boundary. Projection, inventory, owner, status, and ref-resolution preflight readsโ€”including the final mutation-adjacent recheckโ€”are interruptible. Once that recheck passes, Git mutation, durable binding/continuation, and ownership-proven rollback remain uninterruptible. The Deferred regression requests interruption synchronously before releasing the read gate, proves zero Git mutation/dispatch, and proves a later checkout succeeds after both guards are released. The focused WorktreeMcpService suite passes 117/117.

apps/server/src/mcp/WorktreeMcpService.tsline 584Outdated ยท 69afd25Open thread
โ€ฆโ€ฆ@@ -321,77 +510,130 @@ const make = Effect.gen(function* () {
581+ expectedBranch: projection.thread.branch,
582+ expectedWorktreePath: projection.thread.worktreePath,
583+ expectedArchived: false,
584+ }),
CcursorBoton line 584Aug 30, 2026

New worktrees can be double-bound

High Severity

performHandoff creates the worktree and then rechecks only the caller thread before binding. It never takes the physical checkout guard and never inspects other owners of the new path. A concurrent t3_thread_checkout of that branch or path can bind another thread first; the handoff still commits, so two threads share one mutable checkout.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 69afd25d7a604c150202fec55efd927f0c8eda83. Configure here.

Fixed in 875fd5e4a1832f0ce7632c4b264cc98c0dbe5284. After creation, handoff resolves the actual physical checkout, acquires the same common-dir/worktree-root guard as checkout, and rereads global active/archived bindings before committing. The deterministic handoff-versus-checkout race proves the winning binding survives and the losing handoff retains rather than deletes the checkout. Every cleanup path now re-proves no competing owner, unchanged caller binding, exact physical identity, clean/ref state, and the pre-create expected HEAD; ambiguous or changed state is retained. The focused suite passes 117/117.

MmacroscopeappBotreviewed ยท 1 inline threadAug 30, 2026
Synced from GitHub

Effect service conventions review of the new WorktreeMcpService.checkout path: service tag, inline interface, make/layer, subpath namespace imports, dependency acquisition (yield* Foo.Foo), and error construction all follow the conventions. One test-organization issue noted inline.

Posted via Macroscope โ€” Effect Service Conventions

apps/server/src/mcp/WorktreeMcpService.test.tslines 2658โ€“2658Outdated ยท 875fd5eOpen thread
โ€ฆโ€ฆ@@ -1756,6 +2019,1512 @@ describe("t3_worktree_list", () => {
2655+ });
2656+ });
2657+
2658+ it.effect("bounds nested binding identity reads by the requested page", () => {
MmacroscopeappBoton lines 2658โ€“2658Aug 30, 2026

mcp/WorktreeMcpService.test.ts:2658

This block (through the does not attribute a nested independent repository... case) is t3_worktree_list coverage living inside the t3_thread_checkout suite, and three of the cases โ€” includes archived thread bindings retained on a physical checkout, attributes a nested recorded cwd to its physical worktree root, and does not attribute a nested independent repository to the project worktree โ€” are byte-identical duplicates of tests still present in the t3_worktree_list describe above. Looks like a merge/rebase artifact. Consider deleting the duplicates here and moving the remaining runList cases back into describe("t3_worktree_list") so the checkout suite only exercises runCheckout.

No longer relevant as of 1ed8ca43207f300ed83e1f991be080eb0b9f3e54

Fixed mechanically in 1ed8ca43207f300ed83e1f991be080eb0b9f3e54. I verified the archived-binding, nested-cwd attribution, and nested-independent-repository runList case bodies duplicated the existing t3_worktree_list coverage, then removed only the checkout-describe copies. The three unique pagination/bounded-identity runList cases were moved intact into the t3_worktree_list describe. No production code or assertions changed. WorktreeMcpService passes 114/114, canonical t3 typecheck exits 0, and targeted formatting/diff check pass.

apps/server/src/mcp/WorktreeMcpService.tsline 1657Outdated ยท d653ad1Open thread
โ€ฆโ€ฆ@@ -759,7 +1111,862 @@ const make = Effect.gen(function* () {
1654+ yield* gitWorkflow
1655+ .createRef({
1656+ cwd: targetWorkspacePath,
1657+ refName: requestedBranch,
MmacroscopeappBoton line 1657Aug 30, 2026

๐ŸŸก Medium mcp/WorktreeMcpService.ts:1657

A target.create=true request with branch: "--unset-upstream" executes git branch --unset-upstream, changing the current branchโ€™s upstream configuration instead of creating a branch; the subsequent checkout then fails. Validate requestedBranch as a Git ref name and ensure the driver terminates options before passing user input to git branch.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 1657:
2
3A `target.create=true` request with `branch: "--unset-upstream"` executes `git branch --unset-upstream`, changing the current branchโ€™s upstream configuration instead of creating a branch; the subsequent checkout then fails. Validate `requestedBranch` as a Git ref name and ensure the driver terminates options before passing user input to `git branch`.
CcursorBotreviewed ยท 1 inline threadAug 30, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit d653ad1c5d343d0257fd4b82a2acc03840db68b5. Configure here.

apps/server/src/mcp/WorktreeMcpService.tsline 1567Outdated ยท d653ad1Open thread
โ€ฆโ€ฆ@@ -759,7 +1111,862 @@ const make = Effect.gen(function* () {
1564+ Effect.exit(threadManagement.getThreadProjection(scope.threadId)),
1565+ ],
1566+ { concurrency: 3 },
1567+ );
CcursorBoton line 1567Aug 30, 2026

Rollback fails closed without partial details

Medium Severity

rollbackOwnedCheckout lets loadActiveWorkspaceBindings fail the effect instead of treating that unresolved-owner case as not_possible. After Git has already moved, callers then surface a generic operation_failed with no partial payload, so agents cannot tell that checkout changed, that the binding did not commit, or that rollback was skipped.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit d653ad1c5d343d0257fd4b82a2acc03840db68b5. Configure here.

apps/server/src/mcp/WorktreeMcpService.tsline 1715Outdated ยท e7221b8Open thread
โ€ฆโ€ฆ@@ -756,7 +1110,862 @@ const make = Effect.gen(function* () {
1712+ },
1713+ );
1714+ }
1715+ checkoutAction = createBranch ? "created" : "switched";
MmacroscopeappBoton line 1715Sep 5, 2026

๐ŸŸก Medium mcp/WorktreeMcpService.ts:1715

After a successful branch switch, checkout leaves VcsStatusBroadcaster's cached status unchanged, so subscribers and later getStatus calls can continue reporting the previous branch/PR status until another refresh occurs. The normal vcsSwitchRef path refreshes this cache; call vcsStatusBroadcaster.refreshStatus(targetWorkspacePath) after the successful checkout here as well.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/WorktreeMcpService.ts around line 1715:
2
3After a successful branch switch, `checkout` leaves `VcsStatusBroadcaster`'s cached status unchanged, so subscribers and later `getStatus` calls can continue reporting the previous branch/PR status until another refresh occurs. The normal `vcsSwitchRef` path refreshes this cache; call `vcsStatusBroadcaster.refreshStatus(targetWorkspacePath)` after the successful checkout here as well.

Fixed in 48384161a148da3e5965a62a47ed586b2bd37f03. After a switched or newly-created checkout is durably bound, the service starts the existing broadcaster refresh as an ignored detached effect. Refresh failure therefore cannot relabel a committed checkout as failed. Focused upstack coverage passes 115/115.

Added 19 commits

  • fix(mcp): fail closed on checkout status errors055fd40
  • fix(vcs): reject option-like branch names0619411
  • test(mcp): group worktree inventory coverage14449f2
  • fix(mcp): recover threads from missing worktrees1ac65ff
  • fix(mcp): verify local checkout ownership4ffce0b
  • fix(mcp): preserve changed handoff worktrees80f4aab
  • fix(mcp): refresh status after checkout84f260d
  • fix(mcp): serialize physical workspace checkout8da9669
  • fix(mcp): guard workspace transition ownership951c085
  • fix(mcp): guard checkout transitions atomicallya98ab4e
  • fix(mcp): preserve unowned checkout stateafff7ee
  • fix(mcp): preserve reviewed checkout compositionb28f4cc
  • feat(mcp): let agents change thread workspacesbdbd1bd
  • fix(mcp): guard nested checkout bindingsbe4b3ba
  • fix(mcp): retain uncertain handoff branchesbf1f859
  • fix(mcp): fail closed on unresolved ownerse0d12fe
  • docs(mcp): clarify checkout rollback cleanupf231073
  • test(vcs): match guarded branch creationf45e686
  • docs(mcp): clarify credential livenessf942bae

Superseded as the active MCP proposal by #10566 in the rebuilt MCP stack, following the maintainer request to review thin service wrappers separately from service hardening.

This is not a full feature-parity replacement. This discovers branch-backed refs/worktrees only. Detached unbound inventory and switching an existing checkout require a shared service operation and are intentionally not rebuilt inside MCP. The old workspace work is retained for that extraction.

Closing this mixed proposal without merging. The remote branch agents/mcp-workspaces/checkout and exact head 84f260db922bef1a7b54624c2e2f9bc6805f8094 are retained so the service changes can be extracted for separate review later.