feat(mcp): target thread launches by project#8678

closed18
agents/mcp-projects/management
agents/mcp-projects/targeted-threads
openedAug 29, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago

Problem

Project creation is incomplete if agents cannot start and manage new threads in the selected project and workspace.

Change

  • Extend create_threads and t3_thread_start with explicit projectId and root, existing-worktree, or new-worktree launch strategies.
  • Extend thread list, read, send, wait, and interrupt targeting to an authorized project in the same environment.
  • Reuse ThreadLaunchService for canonical repository identity, actual-branch checks, provisioning, setup, and durable replay.
  • Carry caller run, provider session, runtime, and interaction ceilings into serialized thread creation and initial-message acceptance.

Behavior

Omitted project selection keeps the current checkout only for the current project. A different project uses its own root/default strategy and never inherits the caller's worktree path. Accepted retries reconstruct durable thread, workspace, model, message, and initial-run identity before mutable provider or filesystem planning. Root retries can recover an in-progress setup after the root temporarily disappears, while fresh branch mismatches reject before creating work. Cross-project parent records use a server-owned path that verifies the durable target project.

Validation

  • 72 focused Git workspace, ThreadLaunch, MCP targeting, lifecycle-ceiling, durable replay, contract, and real MCP/V2 integration tests.
  • Scoped contracts, client-runtime, shared, and server typechecks.
  • Formatting and type-aware lint for the complete stacked diff.

Stack

Upper layer of native stack 8774, dependent on #8677. The stack targets t3code/codex-turn-mapping at 415ed0f73b97f1655b6282492f81d0b2bba3a9cc.

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

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: a2759767-c163-4a8e-aad4-55e6b6f6727c

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.

apps/server/src/mcp/OrchestratorMcpService.tsline 1486Outdated · 6b41e5dOpen thread
……@@ -1370,67 +1441,46 @@ const make = Effect.gen(function* () {
1483+ const projection = launched.projection;
14341484 const run = projection.runs.at(-1);
14351485 yield* threadManagement
14361486 .dispatch({
MmacroscopeappBoton line 1486Aug 29, 2026

🟠 High mcp/OrchestratorMcpService.ts:1486

Cross-project createThreads returns an orchestration_error after already creating and potentially starting the target thread, leaving an unreported orphan thread. thread.created.record rejects targets whose project differs from the parent, so make the recording path accept cross-project targets or otherwise prevent the launch from committing before recording succeeds.

✅ Resolved in 192e70c07f29260141b44f5d6ae4d76dadae0815

Fixed in baad86f8cc. thread.created.record now carries the explicitly authorized targetProjectId, and the orchestrator verifies it against the durable target-thread projection while retaining the legacy same-project check when the field is omitted. The integration test now exercises the actual MCP service, ThreadLaunchService, and event-sourced orchestrator for a cross-project launch and parent timeline record.

packages/contracts/src/orchestratorMcp.tsline 212Outdated · 6b41e5dOpen thread
……@@ -202,10 +202,35 @@ export const OrchestratorMcpTaskCancelResult = Schema.Struct({
209+ }),
210+ Schema.Struct({
211+ type: Schema.Literal("existing_worktree"),
212+ worktreePath: TrimmedNonEmptyString,
MmacroscopeappBoton line 212Aug 29, 2026

🟠 High src/orchestratorMcp.ts:212

existing_worktree.worktreePath accepts any non-empty path, so a project-scoped thread can launch with cwd set to /etc or another project's checkout. The value is passed unchanged through resolveLaunchWorkspace into ThreadLaunchService, which persists it and uses it for setup scripts; validate that the path belongs to the selected project before accepting the strategy.

Also found in 1 other location(s)

apps/server/src/mcp/OrchestratorMcpService.ts:766

resolveLaunchWorkspace returns an explicit existing_worktree strategy without verifying that its worktreePath belongs to the selected project. A caller can select any existing local directory (for example another project's checkout) and ThreadLaunchService persists that path and uses it as the launched run's cwd; thus a credential scoped to one project can start an agent against an arbitrary local checkout, bypassing the project scope introduced by this change.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @packages/contracts/src/orchestratorMcp.ts around line 212:
2
3`existing_worktree.worktreePath` accepts any non-empty path, so a project-scoped thread can launch with `cwd` set to `/etc` or another project's checkout. The value is passed unchanged through `resolveLaunchWorkspace` into `ThreadLaunchService`, which persists it and uses it for setup scripts; validate that the path belongs to the selected project before accepting the strategy.
4
5Also found in 1 other location(s):
6- apps/server/src/mcp/OrchestratorMcpService.ts:766 -- `resolveLaunchWorkspace` returns an explicit `existing_worktree` strategy without verifying that its `worktreePath` belongs to the selected project. A caller can select any existing local directory (for example another project's checkout) and `ThreadLaunchService` persists that path and uses it as the launched run's `cwd`; thus a credential scoped to one project can start an agent against an arbitrary local checkout, bypassing the project scope introduced by this change.

Fixed in baad86f8cc. Existing-worktree paths are resolved canonically, detected through the VCS registry, required to be a repository root, and compared by canonical Git common-dir identity with the selected project. The accepted checkout’s actual branch/path are passed to ThreadLaunchService. Real Git tests cover a sibling worktree, symlink canonicalization, another repository, a plain directory, and branch mismatches.

Synced from GitHub

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 99e60af.

This comment will update automatically after the next completed run.

apps/server/src/orchestration-v2/Orchestrator.tsline 4910Outdated · baad86fOpen thread
……@@ -4896,13 +4896,26 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
49044907 });
49054908 }
4909+ if (
4910+ command.targetProjectId !== undefined &&
MmacroscopeappBoton line 4910Aug 29, 2026

🟠 High orchestration-v2/Orchestrator.ts:4910

A client can submit thread.created.record with an arbitrary targetProjectId and record a cross-project thread in the parent timeline. Because the branch treats the caller-supplied field as authorization, it bypasses the same-project check and copies the target's title, project ID, run ID, provider, and model into the parent thread; authorization must come from a trusted boundary rather than this untrusted command field.

✅ Resolved in 1f905034f7c91d154ad77f5c0fba447ef694f573

Fixed in 1f905034f7. Cross-project authorization is no longer part of the public orchestration command. The MCP path calls a server-owned recordServerCreatedThread method with the authorized target project supplied out-of-band; public dispatch retains the same-project check. The real MCP/ThreadLaunch/Orchestrator test also asserts that an untrusted public cross-project record command is rejected.

MmacroscopeappBotreviewed · 2 inline threadsAug 29, 2026
Synced from GitHub

Effect service conventions review: two import-namespace deviations around the new VcsDriverRegistry dependency. Everything else (environment-based dependency acquisition in make, the updated layer requirement type, ThreadLaunchService consumption, and the test seams) follows the conventions.

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/OrchestratorMcpService.targeting.test.tslines 28–29Outdated · baad86fOpen thread
……@@ -0,0 +1,524 @@
26+import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
27+import { ScheduledTaskService } from "../scheduledTasks/ScheduledTaskService.ts";
28+import * as VcsDriverRegistryModule from "../vcs/VcsDriverRegistry.ts";
29+import { VcsDriverRegistry, type VcsDriverHandle } from "../vcs/VcsDriverRegistry.ts";
MmacroscopeappBoton lines 28–29Aug 29, 2026

mcp/OrchestratorMcpService.targeting.test.ts:29

The same service module is imported twice, and the namespace copy has to be aliased to VcsDriverRegistryModule because the named import took the module's own name — that alias erases the module namespace this repo uses everywhere else for this module. Consider a single import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; and referencing VcsDriverRegistry.VcsDriverRegistry, VcsDriverRegistry.layer, and VcsDriverRegistry.VcsDriverHandle at the use sites.

No longer relevant as of 192e70c07f29260141b44f5d6ae4d76dadae0815

apps/server/src/mcp/OrchestratorMcpService.tslines 80–80Outdated · baad86fOpen thread
……@@ -70,7 +75,9 @@ import {
7277import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
78+import * as ProjectService from "../project/ProjectService.ts";
7379import { ScheduledTaskService } from "../scheduledTasks/ScheduledTaskService.ts";
80+import { VcsDriverRegistry, type VcsDriverHandle } from "../vcs/VcsDriverRegistry.ts";
MmacroscopeappBoton lines 80–80Aug 29, 2026

VcsDriverRegistry.ts is a service module and every other consumer in apps/server imports it as a namespace (import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts", then VcsDriverRegistry.VcsDriverRegistry / .layer). Consider matching that here, as this PR already does for the other new service imports (ThreadLaunch, ProjectService), and referencing the handle type as VcsDriverRegistry.VcsDriverHandle.

Posted via Macroscope — Effect Service Conventions

Fixed in 1f905034f7. The service and focused tests now use the VcsDriverRegistry module namespace consistently for the service tag, layer, and handle type.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces project-scoped MCP access, new workspace and worktree launch workflows, and shared orchestration policy and lifecycle enforcement across multiple production components. The expanded authorization scope and substantial runtime behavior changes require human review.

Not approved because:

  • Per-PR cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

No code changes detected at 99e60af. Prior analysis still applies.

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

packages/contracts/src/orchestratorMcp.tsline 224Outdated · 13bd326Open thread
……@@ -202,10 +202,35 @@ export const OrchestratorMcpTaskCancelResult = Schema.Struct({
221+]).annotate({
222+ description:
223+ "Workspace for the new thread. Omit to reuse the caller's checkout in the current project, or the target project's root when projectId selects another project.",
224+});
CcursorBoton line 224Aug 29, 2026

Shared MCP schema type carries description annotation

Low Severity

OrchestratorMcpThreadWorkspaceStrategy is a shared/reusable schema type used in MCP tool input parameters (OrchestratorMcpCreateThreadsInput and OrchestratorMcpThreadStartInput), and it carries a .annotate({ description: ... }). Per project conventions, shared schema types in MCP tool inputs need to be description-free to avoid compiled JSON schema issues with some LLM providers. The description text belongs on the concrete field annotations in OrchestratorMcpCreateThreadRequest.workspaceStrategy and OrchestratorMcpThreadStartInput.workspaceStrategy instead.

Triggered by learned rule: MCP tool input schemas: only one description per allOf branch in Effect Schema compositions

Reviewed by Cursor Bugbot for commit 13bd326fdcbe6765fbbdd5a8793fdfcd0dd7901b. Configure here.

Fixed in f17378fdad. The reusable OrchestratorMcpThreadWorkspaceStrategy is now description-free; the same guidance is attached only to the concrete create_threads and t3_thread_start workspaceStrategy fields. Contracts typecheck passes and the real MCP toolkit integration test passes.

apps/server/src/mcp/OrchestratorMcpService.tsline 950Outdated · 7f96f2bOpen thread
……@@ -704,17 +730,231 @@ const make = Effect.gen(function* () {
947+ }
948+ const workspace = yield* inspectLaunchWorkspace(project, project.workspaceRoot, false);
949+ const expectedBranch = requested?.type === "root" ? requested.branch : undefined;
950+ yield* enforceExpectedBranch(expectedBranch, workspace.branch, project.workspaceRoot);
MmacroscopeappBoton line 950Aug 30, 2026

🟠 High mcp/OrchestratorMcpService.ts:950

createThreads can launch on a different branch than the requested one while still reporting the branch validated at line 950. A concurrent git checkout between enforceExpectedBranch and the asynchronous ThreadLaunchService.launch changes the root or existing worktree after validation, but launch receives only the stale observed branch and never rechecks it. Move the branch check into the launch/setup critical section (or otherwise revalidate immediately before setup) so the expected-branch constraint is enforced atomically.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/OrchestratorMcpService.ts around line 950:
2
3`createThreads` can launch on a different branch than the requested one while still reporting the branch validated at line 950. A concurrent `git checkout` between `enforceExpectedBranch` and the asynchronous `ThreadLaunchService.launch` changes the root or existing worktree after validation, but launch receives only the stale observed branch and never rechecks it. Move the branch check into the launch/setup critical section (or otherwise revalidate immediately before setup) so the expected-branch constraint is enforced atomically.

Fixed in 8c7a4106baa159cc39c2be3ae4c68454193e0bbd. Canonical repository/project authorization remains in MCP preflight, while expected-branch enforcement now re-reads symbolic-ref inside the receipt-aware ThreadLaunch boundary immediately before a fresh launch. Accepted retries consult the durable receipt first and return the original launch even if the checkout later changes; a fresh mismatched key rejects before thread creation. The real MCP and direct ThreadLaunch regressions cover both cases. This is the strongest truthful in-process acceptance guarantee: an arbitrary external Git writer is not governed by T3’s service lock and can still change a checkout after acceptance.

apps/server/src/orchestration-v2/ThreadLaunchService.tsline 475Outdated · 8c7a410Open thread
……@@ -452,7 +462,30 @@ export const make = Effect.gen(function* () {
472+ input.workspaceStrategy.type === "existing_worktree"
473+ ? input.workspaceStrategy.worktreePath
474+ : project.workspaceRoot;
475+ const currentBranch = yield* git
MmacroscopeappBoton line 475Aug 30, 2026

🟡 Medium orchestration-v2/ThreadLaunchService.ts:475

git.currentBranch reports repository errors such as an unreadable or corrupt .git/HEAD as null, so this validation returns an expected-branch mismatch claiming the workspace is detached instead of propagating the GitCommandError. Because git symbolic-ref --quiet --short HEAD uses status 1 for a detached HEAD but status 128 for errors, currentBranch should return null only for status 1 and preserve other failures.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/orchestration-v2/ThreadLaunchService.ts around line 475:
2
3`git.currentBranch` reports repository errors such as an unreadable or corrupt `.git/HEAD` as `null`, so this validation returns an expected-branch mismatch claiming the workspace is detached instead of propagating the `GitCommandError`. Because `git symbolic-ref --quiet --short HEAD` uses status `1` for a detached HEAD but status `128` for errors, `currentBranch` should return `null` only for status `1` and preserve other failures.

Fixed in 0e9b8981986fff1e2557d067e2a94611f7b363dc. GitWorkflowService.currentBranch now maps only symbolic-ref exit code 1 to detached HEAD and propagates every other GitCommandError. Expected-branch validation remains inside receipt-aware launch admission. Tests cover detached HEAD, non-1 failure propagation, accepted retry after branch change, and fresh mismatch rejection.

MmacroscopeappBotreviewed · 1 inline threadAug 30, 2026
Synced from GitHub

One finding on caller-visible error classification in createThreads. Everything else in the new service wiring (namespace imports, environment-acquired dependencies, Layer.mock test seams, structured ThreadLaunchError/OrchestratorDispatchError construction) matches the conventions.

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/OrchestratorMcpService.tslines 1589–1596Outdated · 8c7a410Open thread
……@@ -1351,14 +1586,21 @@ const make = Effect.gen(function* () {
1593+ const interactionMode = resolveLaunchInteractionMode(
13591594 parent.thread.interactionMode,
13601595 request.interactionMode,
13611596 );
MmacroscopeappBoton lines 1589–1596Aug 30, 2026

Swapping resolveRuntimeMode/resolveInteractionMode for the new non-failing resolveLaunch* helpers drops the caller-visible runtime_mode_escalation_denied / interaction_mode_escalation_denied failure codes on create_threads: an ordinary (non-racing) escalation request now surfaces as a generic orchestration_error wrapping the orchestrator ceiling message, even though those codes remain in OrchestratorMcpFailure and in docs/orchestration-v2/orchestrator-mcp-server.md. Consider keeping the eager typed check here — the policyCeiling still covers the concurrent-downgrade race — and dropping the duplicated resolveLaunchRuntimeMode/resolveLaunchInteractionMode helpers.

suggestion
1 const runtimeMode = yield* resolveRuntimeMode(
2 parent.thread.runtimeMode,
3 request.runtimeMode,
4 );
5 const interactionMode = yield* resolveInteractionMode(
6 parent.thread.interactionMode,
7 request.interactionMode,
8 );

Posted via Macroscope — Effect Service Conventions

Addressed in 0e9b8981986fff1e2557d067e2a94611f7b363dc without restoring eager checks, because eager mutable-mode validation would reject an already accepted retry after caller or target modes changed. Fresh launches enforce captured and current runtime/interaction ceilings under serialized caller/target locks. Accepted receipts preserve authenticated scope and key ownership but skip mutable mode revalidation. ThreadLaunch policy failures map to runtime_mode_escalation_denied / interaction_mode_escalation_denied, and Deferred tests assert the typed code plus truthful captured/current ceiling messages.

apps/server/src/mcp/OrchestratorMcpService.tsline 1641Outdated · 0e9b898Open thread
……@@ -1370,107 +1638,72 @@ const make = Effect.gen(function* () {
1415- return yield* threadManagement.dispatch(createCommand);
1416- }),
1417- )
1641+ const launched = yield* threadLaunch
MmacroscopeappBoton line 1641Aug 30, 2026

🟠 High mcp/OrchestratorMcpService.ts:1641

createThreads can launch and start a child thread after the scoped parent run has finished or changed provider ownership. The initial check occurs before target, project, and workspace resolution, and ThreadLaunchService.launch does not revalidate that active-run/session condition; recheck it immediately before launching, as the previous withProjectCreationAdmission path did.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/OrchestratorMcpService.ts around line 1641:
2
3`createThreads` can launch and start a child thread after the scoped parent run has finished or changed provider ownership. The initial check occurs before target, project, and workspace resolution, and `ThreadLaunchService.launch` does not revalidate that active-run/session condition; recheck it immediately before launching, as the previous `withProjectCreationAdmission` path did.

Fixed in 4dd8caf935f836ecd4ee8c5fe3eb681619285293. create_threads carries the captured caller run and provider identity on the existing optional policy ceiling. Fresh thread.create and initial message.dispatch revalidate that exact active, rooted, provider-owned run under the existing sorted caller-and-target locks. Accepted receipts still replay before mutable checks, and cross-project authorization is unchanged. Real MCP/V2 stored-event and Deferred tests cover the parent becoming terminal before create (no child) and between create and message dispatch (no child turn).

apps/server/src/mcp/OrchestratorMcpService.tsline 1680Outdated · 0e9b898Open thread
……@@ -1370,107 +1638,72 @@ const make = Effect.gen(function* () {
1677+ failure(
1678+ "orchestration_error",
1679+ `Unable to launch thread ${index + 1}: ${errorMessage(error)}`,
1680+ ),
CcursorBoton line 1680Aug 30, 2026

Stale parent run still creates threads

Medium Severity

create_threads now checks that the caller run is active only at request start, then launches through ThreadLaunchService without the previous admission-time recheck. A parent run that completes during workspace inspect or provisioning still creates the target thread, and recordServerCreatedThread accepts a completed parent run, so the child can land after the MCP turn has already ended.

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

Fixed in 4dd8caf935f836ecd4ee8c5fe3eb681619285293. create_threads carries the captured caller run and provider identity on the existing optional policy ceiling. Fresh thread.create and initial message.dispatch revalidate that exact active, rooted, provider-owned run under the existing sorted caller-and-target locks. Accepted receipts still replay before mutable checks, and cross-project authorization is unchanged. Real MCP/V2 stored-event and Deferred tests cover the parent becoming terminal before create (no child) and between create and message dispatch (no child turn).

apps/server/src/mcp/OrchestratorMcpService.tsline 958Outdated · a587051Open thread
……@@ -705,17 +763,219 @@ const make = Effect.gen(function* () {
955+ project.id === parent.thread.projectId &&
956+ parent.thread.worktreePath !== null;
957+ if (explicitExisting !== undefined || useParentWorktree) {
958+ const requestedPath = explicitExisting?.worktreePath ?? parent.thread.worktreePath!;
MmacroscopeappBoton line 958Aug 30, 2026

🟡 Medium mcp/OrchestratorMcpService.ts:958

A retry with the same clientRequestId returns invalid_request instead of replaying the accepted thread when an existing_worktree has been deleted or is otherwise unresolvable. resolveLaunchWorkspace calls inspectLaunchWorkspace before ThreadLaunchService.launch can read the accepted command receipt, so workspace validation fails before idempotent recovery; resolve the receipt before validating the workspace.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/OrchestratorMcpService.ts around line 958:
2
3A retry with the same `clientRequestId` returns `invalid_request` instead of replaying the accepted thread when an `existing_worktree` has been deleted or is otherwise unresolvable. `resolveLaunchWorkspace` calls `inspectLaunchWorkspace` before `ThreadLaunchService.launch` can read the accepted command receipt, so workspace validation fails before idempotent recovery; resolve the receipt before validating the workspace.

Fixed in 661a3b184abb70fd97afd2951af28b8f547a6c41. createThreads now validates the stable create receipt before mutable workspace planning. An accepted durable existing-worktree binding is replayed at its persisted path without filesystem inspection while ThreadLaunch still handles the exact initial-message receipt and unfinished preparation. The gated integration removes the physical checkout while R1 is preparing and proves the same child/R1 returns without rewriting its binding to root.

apps/server/src/mcp/OrchestratorMcpService.tsline 973Outdated · a587051Open thread
……@@ -705,17 +763,219 @@ const make = Effect.gen(function* () {
970+ const expectedBranch = requested?.type === "root" ? requested.branch : undefined;
971+ return {
972+ type: "root",
973+ ...(workspace.branch === null ? {} : { branch: workspace.branch }),
MmacroscopeappBoton line 973Aug 30, 2026

🟡 Medium mcp/OrchestratorMcpService.ts:973

When no branch is requested, resolveLaunchWorkspace leaves expectedBranch unset, so ThreadLaunchService.launch skips its currentBranch validation and can persist the snapshot from a checkout that has since changed. Thread details and worktree.status then report a branch different from the checkout used by the run. Populate expectedBranch with the observed workspace.branch when no explicit branch was supplied for both root and existing-worktree strategies.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/OrchestratorMcpService.ts around line 973:
2
3When no branch is requested, `resolveLaunchWorkspace` leaves `expectedBranch` unset, so `ThreadLaunchService.launch` skips its `currentBranch` validation and can persist the snapshot from a checkout that has since changed. Thread details and `worktree.status` then report a branch different from the checkout used by the run. Populate `expectedBranch` with the observed `workspace.branch` when no explicit branch was supplied for both root and existing-worktree strategies.

Fixed in 661a3b184abb70fd97afd2951af28b8f547a6c41. Fresh root and existing-worktree launches carry the observed branch, including detached HEAD as null, into ThreadLaunch's receipt-aware acceptance check. Non-Git roots remain unconstrained only when branch is omitted; explicit branches reject. Tests cover a fresh branch race, detached transitions, and accepted retry after the checkout later changes.

apps/server/src/mcp/OrchestratorMcpService.tsline 1725Outdated · a587051Open thread
……@@ -1485,7 +1724,8 @@ const make = Effect.gen(function* () {
14851724 );
14861725 return {
MmacroscopeappBoton line 1725Aug 30, 2026

🟡 Medium mcp/OrchestratorMcpService.ts:1725

An idempotent createThreads replay can return a thread with the wrong projectId, providerInstanceId, and model: these fields come from the new request even though ThreadLaunchService returns the previously persisted projection. Clients can therefore route follow-up operations to the wrong project or provider. Serialize these values from projection.thread (or reject mismatched replays).

✅ Resolved in 661a3b184abb70fd97afd2951af28b8f547a6c41

Fixed in 661a3b184abb70fd97afd2951af28b8f547a6c41. Accepted retries derive project, provider/model, modes, title, and returned/recorded thread identity from the durable projection, with both pre-launch and post-launch stable thread/project guards. The real MCP/V2 regression inserts a later R2 and proves the retry and parent record still return the exact durable R1.

packages/contracts/src/orchestrationV2.tslines 2014–2049Outdated · 06b6059Open thread
MmacroscopeappBoton lines 2014–2049Aug 30, 2026

🟠 High src/orchestrationV2.ts:2014

An authenticated WebSocket client can set thread.create.policyCeiling.callerThreadId to another active thread, causing dispatch to acquire that thread's threadDispatch lock and block its dispatches without authorization. Because policyCeiling is decoded from the public command, derive this server-internal value server-side instead of accepting it here.

suggestion
1
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @packages/contracts/src/orchestrationV2.ts around line 2014:
2
3An authenticated WebSocket client can set `thread.create.policyCeiling.callerThreadId` to another active thread, causing dispatch to acquire that thread's `threadDispatch` lock and block its dispatches without authorization. Because `policyCeiling` is decoded from the public command, derive this server-internal value server-side instead of accepting it here.

Verified against the actual RPC boundary. dispatchCommand requires the environment-wide orchestration operate scope in RpcAuthorization before ws dispatch, and that principal may already dispatch commands to arbitrary thread IDs in the environment. policyCeiling only adds caller lifecycle/mode restrictions; its sorted caller/target locks do not grant additional authority. MCP constructs the ceiling from its authenticated invocation scope. No change.

apps/server/src/orchestration-v2/Orchestrator.tslines 289–292Outdated · 06b6059Open thread
……@@ -224,6 +282,23 @@ function nextRunOrdinal(projection: OrchestrationV2ThreadProjection): number {
289+ case "auto-accept-edits":
290+ return 1;
291+ case "auto":
292+ return 2;
MmacroscopeappBoton lines 289–292Aug 30, 2026

🟠 High orchestration-v2/Orchestrator.ts:289

A caller capped at "auto" can launch a child in "auto-accept-edits", allowing edits that should still require approval. runtimeModeRank assigns "auto-accept-edits" rank 1 and "auto" rank 2, so the ceiling check incorrectly considers the less restrictive mode valid; assign "auto" the lower rank.

diff
1- return 1;
2+ return 2;
3@@
4- return 2;
5+ return 1;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/orchestration-v2/Orchestrator.ts around lines 289-292:
2
3A caller capped at `"auto"` can launch a child in `"auto-accept-edits"`, allowing edits that should still require approval. `runtimeModeRank` assigns `"auto-accept-edits"` rank 1 and `"auto"` rank 2, so the ceiling check incorrectly considers the less restrictive mode valid; assign `"auto"` the lower rank.

Verified against the V2 adapters: this ordering is intentional. Codex maps auto-accept-edits to workspace-write/on-request with user review, while auto uses the same filesystem boundary with auto_review; Claude likewise maps them to acceptEdits versus auto. Swapping the ranks would let the narrower auto-accept-edits ceiling authorize the more autonomous auto mode. No change.

apps/server/src/mcp/OrchestratorMcpService.tsline 220Outdated · 661a3b1Open thread
……@@ -181,6 +194,32 @@ function errorMessage(error: unknown): string {
217+ "Thread creation requires an active run owned by this MCP provider session.",
218+ );
219+ }
220+ return null;
MmacroscopeappBoton line 220Aug 30, 2026

🟡 Medium mcp/OrchestratorMcpService.ts:220

When the caller thread is archived or deleted after MCP preflight, createThreads reports generic orchestration_error instead of documented parent_not_active. enforcePolicyCeiling wraps the inactive-thread message as a string in OrchestratorDispatchError, so this function only recognizes the typed OrchestratorCallerRunCeilingError and returns null; handle that string-wrapped lifecycle error as parent_not_active too.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/OrchestratorMcpService.ts around line 220:
2
3When the caller thread is archived or deleted after MCP preflight, `createThreads` reports generic `orchestration_error` instead of documented `parent_not_active`. `enforcePolicyCeiling` wraps the inactive-thread message as a string in `OrchestratorDispatchError`, so this function only recognizes the typed `OrchestratorCallerRunCeilingError` and returns `null`; handle that string-wrapped lifecycle error as `parent_not_active` too.

Fixed in 8f61ffb24b3b03854cc4f20dda37d7cbb039cc1b without string matching. When the ceiling carries callerRunId and callerProviderInstanceId, an archived/deleted caller now retains the existing typed OrchestratorCallerRunCeilingError, which the MCP boundary maps to parent_not_active. Legacy ceiling-only callers retain the generic cause. A real gated MCP→ThreadLaunch→V2 archive race proves the typed result and no child creation; restoring the old string cause made that assertion fail with orchestration_error.

docs/orchestration-v2/orchestrator-mcp-server.mdlines 289–291Outdated · 661a3b1Open thread
……@@ -261,31 +262,56 @@ type CreateThreadsInput = {
288+
289+Project selection does not raise the caller's authority. Provider, model, runtime mode, and
290+interaction mode still inherit from the caller, and requested modes cannot exceed its permission
291+ceiling. Project model and workspace-mode defaults remain client defaults; the MCP request's
MmacroscopeappBoton lines 289–291Aug 30, 2026

🟢 Low orchestration-v2/orchestrator-mcp-server.md:289

The documentation incorrectly says provider and model inherit from the caller, so clients targeting another available provider or model through target.providerInstanceId, target.driverKind, or target.model are told the launch will retain the caller's settings. resolveTarget selects the explicitly requested enabled target and only falls back when those fields are omitted; document that resolution behavior instead.

suggestion
1Project selection does not raise the caller's authority. Provider and model resolve from the
2requested `target` when supplied, otherwise inherit from the caller; requested modes cannot exceed
3its permission ceiling.
🚀 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 289-291:
2
3The documentation incorrectly says `provider` and `model` inherit from the caller, so clients targeting another available provider or model through `target.providerInstanceId`, `target.driverKind`, or `target.model` are told the launch will retain the caller's settings. `resolveTarget` selects the explicitly requested enabled target and only falls back when those fields are omitted; document that resolution behavior instead.

Fixed in 8f61ffb24b3b03854cc4f20dda37d7cbb039cc1b. The docs now state that an explicit target selects provider/model and omitted target fields inherit from the caller; runtime and interaction modes still inherit by default and remain bounded by the caller ceiling.

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 661a3b184abb70fd97afd2951af28b8f547a6c41. Configure here.

apps/server/src/mcp/OrchestratorMcpService.tsline 1790Outdated · 661a3b1Open thread
……@@ -1341,139 +1625,245 @@ const make = Effect.gen(function* () {
1787+ parent,
1788+ yield* loadLaunchProject(projectId),
1789+ request.workspaceStrategy,
1790+ )
CcursorBoton line 1790Aug 30, 2026

Partial root replay rechecks workspace

Medium Severity

When a root thread is already accepted but the initial message is still preparing or missing, mayReplayDurableRoot is false and createThreads re-runs resolveLaunchWorkspace. That re-reads the live checkout and can fail the retry if the branch or path changed, even though ThreadLaunchService skips expectedBranch once a create receipt exists. Existing-worktree replays avoid this by using the stored path.

Reviewed by Cursor Bugbot for commit 661a3b184abb70fd97afd2951af28b8f547a6c41. Configure here.

Fixed in 8f61ffb24b3b03854cc4f20dda37d7cbb039cc1b. Accepted durable non-null paths still replay as existing_worktree, and a retry marked new_worktree retains provisioning recovery. Accepted null-path root/omitted retries now reconstruct root directly from the durable branch, leaving ThreadLaunch in charge of message receipts, fresh missing-message ceilings, and preparation reservation. The real setup-gated test removes the root while exact R1 is preparing and proves the same thread/R1/status, one message, null binding, and unchanged branch; the pre-fix planning path fails that replay.

MmacroscopeappBotreviewed · 1 inline threadAug 30, 2026
Synced from GitHub

One finding on error translation in createThreads; the rest of the new service/layer wiring, tagged errors, and namespace imports follow the conventions.

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/OrchestratorMcpService.tslines 1651–1654Outdated · abf52e5Open thread
……@@ -1341,139 +1625,192 @@ const make = Effect.gen(function* () {
1651+ Effect.mapError(() =>
1652+ failure(
1653+ "orchestration_error",
1654+ `Unable to read the accepted launch for thread ${index + 1}.`,
MmacroscopeappBoton lines 1651–1654Aug 30, 2026

This translation boundary drops the underlying failure entirely: the callback ignores its argument, and OrchestratorMcpFailure has no cause field, so a ThreadManagementError/OrchestratorV2Error from withProjectCreationAdmission becomes an opaque, context-free message. The previous code preserved it via errorMessage(error). Consider keeping the upstream detail, as every other mapError in this module does.

suggestion
1 Effect.mapError((error) =>
2 failure(
3 "orchestration_error",
4 `Unable to read the accepted launch for thread ${index + 1}: ${errorMessage(error)}`,

Posted via Macroscope — Effect Service Conventions

Verified against exact head abf52e5ba2eb5644b1fe134e9291b88425d2c18d. No source change: this is an intentionally redacted caller-facing MCP boundary. withProjectCreationAdmission can carry nested ThreadManagement or Orchestrator failures whose messages include internal project, repository, command, or provider details. Appending errorMessage(error) would serialize those details through OrchestratorMcpFailure. The public response keeps the safe operation and thread index; known safe structured failures are mapped separately. The c01 source-cleared head used this exact mapping, and the refresh range-diff is unchanged. Internal diagnostics can retain a cause at a non-wire boundary, but the proposed public message is not safe.

Synced from GitHub

Macroscope skipped reviewing this pull request. Per-PR cost limit exceeded (workspace setting).

Reviews on this PR have cost $48.41 so far. This review would add an estimated $3.86, bringing the total to $52.27 — above your per-PR limit of $30.00.

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude large or generated files from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

Added 18 commits

  • fix(mcp): keep workspace strategy schema descriptions concrete4c81fd9
  • feat(mcp): target thread launches by project7427303
  • fix(mcp): replay accepted branch-targeted launches130b6d7
  • fix(git): distinguish detached branch lookup156b90c
  • fix(mcp): revalidate launch branches at acceptance35d1c23
  • style(git): use tagged error handlers5153106
  • fix(mcp): clarify launch admission failures561d3d5
  • test(mcp): isolate branch mismatch rejection6c2e653
  • fix(server): enforce fresh thread launch ceilings8027c13
  • fix(mcp): preserve launched run identityf7e8e42
  • fix(mcp): preserve launch ceiling failure codesf9307fe
  • fix(mcp): make targeted thread retries durable1451ad6
  • fix(mcp): replay accepted root launches durably530cae5
  • fix(mcp): revalidate delegated launch ownership58a5e2a
  • test(mcp): compose targeted launch fixtures99e60af
  • fix(mcp): preserve inactive caller failure codesaa61af0
  • docs(mcp): clarify targeted provider resolutionb08e339
  • test(mcp): remove duplicate launch fixture importe9a212a

Superseded as the active MCP proposal by #10565 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. Existing create_threads/t3_thread_start are untouched. Requires a full-access/default caller. Each call is a new launch; failures can leave an accepted partial thread. Preparation and workspace behavior remain in ThreadLaunchService. No new parent-run admission or retry journal.

Closing this mixed proposal without merging. The remote branch agents/mcp-projects/targeted-threads and exact head 99e60afa114984f01756807ccd61d190cec9ac22 are retained so the service changes can be extracted for separate review later.