Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
76d6c7c
feat(server): /mcp narrows a client's modes from T3-Mode-Limit
juliusmarminge Oct 6, 2026
6152622
feat(server): t3_thread_launch takes clientRequestId
juliusmarminge Oct 7, 2026
e13f1d4
feat(server): link to another environment
juliusmarminge Oct 7, 2026
4fbb8de
Merge current main into the mode-limit layer (#16653)
zainmitchell Oct 21, 2026
c6f7adc
Merge current main into the mode-limit layer (#16653)
zainmitchell Oct 21, 2026
b7951ac
Merge the launch-request-key layer (#16654) into the updated mode-lim…
zainmitchell Oct 21, 2026
3b721d1
fix(server): replay accepted launches before mutable preflights
juliusmarminge Oct 19, 2026
a615bda
fix(server): validate worktree membership on launch retries
juliusmarminge Oct 19, 2026
662caf2
fix(server): preserve accepted launch preparation and limits
juliusmarminge Oct 19, 2026
9621c64
docs(server): describe t3_thread_launch retry keys
zainmitchell Oct 21, 2026
104b5f1
Merge the peer-link layer (#16655) into the updated launch-request-ke…
zainmitchell Oct 21, 2026
8bc5fbe
fix(server): handle peer response and cleanup failures
juliusmarminge Oct 19, 2026
d868b62
feat(server): /mcp narrows a client's modes from T3-Mode-Limit
juliusmarminge Oct 6, 2026
1aa278b
feat(server): t3_thread_launch takes clientRequestId
juliusmarminge Oct 7, 2026
16800ae
feat(server): link to another environment
juliusmarminge Oct 7, 2026
becd347
Merge the rebased peer-links target into #22447
zainmitchell Oct 22, 2026
071e661
feat(server): t3_thread_launch takes clientRequestId
juliusmarminge Oct 7, 2026
1979307
feat(server): link to another environment
juliusmarminge Oct 7, 2026
fc53254
Merge the amended peer-links target into #22447
zainmitchell Oct 22, 2026
56f88be
feat(server): /mcp narrows a client's modes from T3-Mode-Limit
juliusmarminge Oct 6, 2026
8a5f64e
feat(server): t3_thread_launch takes clientRequestId
juliusmarminge Oct 7, 2026
b3745b6
feat(server): link to another environment
juliusmarminge Oct 7, 2026
25835af
feat(server): link to another environment
juliusmarminge Oct 7, 2026
dbdbb2d
Merge the rebased peer-links target into #22447
zainmitchell Oct 26, 2026
2205c04
fix(server): retain peer support fixes on the updated links base
zainmitchell Oct 26, 2026
4fac0b1
feat(server): /mcp narrows a client's modes from T3-Mode-Limit
juliusmarminge Oct 6, 2026
bb78361
feat(server): t3_thread_launch takes clientRequestId
juliusmarminge Oct 7, 2026
374b79b
feat(server): link to another environment
juliusmarminge Oct 7, 2026
1d427ff
Merge the rebased peer-links target into #22447
zainmitchell Oct 27, 2026
ed9f466
feat(server): /mcp narrows a client's modes from T3-Mode-Limit
juliusmarminge Oct 6, 2026
a2a4066
feat(server): t3_thread_launch takes clientRequestId
juliusmarminge Oct 7, 2026
697badc
feat(server): link to another environment
juliusmarminge Oct 7, 2026
2f8f5eb
Merge the rebased peer-links target into #22447
zainmitchell Oct 27, 2026
9900e80
Merge the rebased peer-links target into #22447
zainmitchell Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/mcp/toolkits/project/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const clientLaunchHarness = (input: {
},
}),
Layer.mock(ThreadLaunch.ThreadLaunchService)({
getAcceptedLaunch: () => Effect.succeedNone,
checkWorktreeBase: input.checkWorktreeBase ?? (() => Effect.void),
launch: (launch) => {
input.launched.push(launch);
Expand Down
20 changes: 18 additions & 2 deletions apps/server/src/mcp/toolkits/project/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
),
)).projectId
: yield* resolveProjectId(context, input.projectId);
// Accepted creates skip mutable base/default-model preflights.
// Message intake still handles their replay and preparation.
const acceptedLaunch =
input.clientRequestId === undefined
? undefined
: Option.getOrUndefined(
yield* ThreadLaunch.ThreadLaunchService.pipe(
Effect.flatMap((launches) =>
launches.getAcceptedLaunch({ commandId, threadId, projectId }),
),
Effect.mapError(unavailable),
),
);
const readProject = Project.ProjectService.pipe(
Effect.flatMap((projects) => projects.getById(projectId)),
Effect.mapError(unavailable),
Expand All @@ -131,7 +144,9 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
const launchedModel =
launched === null || launched.deletedAt !== null ? undefined : launched.modelSelection;
const replaying = launchedModel !== undefined;
if (!replaying && input.workspaceStrategy?.type === "existing_worktree") {
// A retry may bind a workspace to an idle accepted launch, so membership
// is checked on every launch, unlike the checks below.
if (input.workspaceStrategy?.type === "existing_worktree") {
const project = yield* readProject;
if (project === undefined)
return yield* new OrchestratorMcpFailure({
Expand All @@ -142,7 +157,7 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
}
// Reject unresolved base refs before accepting a launch; worktree provisioning happens later.
const strategy = input.workspaceStrategy;
if (!replaying && strategy?.type === "worktree")
if (acceptedLaunch === undefined && !replaying && strategy?.type === "worktree")
yield* ThreadLaunch.ThreadLaunchService.pipe(
Effect.flatMap((launches) =>
launches.checkWorktreeBase({ projectId, workspaceStrategy: strategy }),
Expand Down Expand Up @@ -176,6 +191,7 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
)
: undefined;
const modelSelection =
acceptedLaunch?.modelSelection ??
input.modelSelection ??
launchedModel ??
caller?.modelSelection ??
Expand Down
27 changes: 27 additions & 0 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12193,6 +12193,33 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
}),
),
);
if (
command.type === "message.dispatch" &&
command.dispatchMode?.type === "defer_start" &&
(yield* DispatchModeLimit) !== undefined
) {
const runCreated = storedEvents.find(
(stored) => stored.event.type === "run.created",
)?.event;
if (runCreated?.type === "run.created") {
const current = yield* projectionStore
.getThreadRecords(command.threadId, ["runs"])
.pipe(
Effect.mapError(
(cause) => new OrchestratorProjectionError({ threadId: command.threadId, cause }),
),
);
// Replaying this receipt can resume workspace preparation, so it must
// respect the caller's current ceiling just like a fresh dispatch.
if (
current.runs.some(
(run) => run.id === runCreated.payload.id && run.status === "preparing",
)
) {
yield* refuseAboveDispatchModeLimit(command, command.threadId, current.thread);
}
}
}
if (command.type === "queue.resume" || command.type === "queued-run.start") {
yield* mapDispatchError(command)(
startNextQueuedRun(
Expand Down
224 changes: 213 additions & 11 deletions apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ComposerContextId,
type ChatAttachment,
CommandId,
EnvironmentId,
DEFAULT_SERVER_SETTINGS,
GitCommandError,
MessageId,
Expand Down Expand Up @@ -59,6 +60,10 @@ import * as EffectOutbox from "./EffectOutbox.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
import * as ThreadLaunch from "./ThreadLaunchService.ts";
import * as McpInvocationContext from "../mcp/McpInvocationContext.ts";
import * as McpToolAccess from "../mcp/McpToolAccess.ts";
import * as ProjectHandlers from "../mcp/toolkits/project/handlers.ts";
import { ProjectToolkit } from "../mcp/toolkits/project/tools.ts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import * as ThreadManagement from "./ThreadManagementService.ts";
import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts";
Expand Down Expand Up @@ -114,6 +119,7 @@ const claudeAdapter = {
} as ProviderAdapter.ProviderAdapterV2["Service"];

interface HarnessOptions {
readonly getProject?: ProjectService.ProjectService["Service"]["getById"];
readonly closeTerminal?: TerminalManager.TerminalManager["Service"]["close"];
readonly adapters?: ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]>;
readonly managedFolders?: Layer.Layer<ManagedProjectFolders.ManagedProjectFolders>;
Expand Down Expand Up @@ -179,14 +185,16 @@ function makeHarness(options: HarnessOptions = {}) {
bootstrap: () => Effect.die("unused"),
update: () => Effect.die("unused"),
delete: () => Effect.die("unused"),
getById: (id) =>
Effect.succeed(
id === projectId
? Option.some(project)
: id === otherProjectId
? Option.some(otherProject)
: Option.none(),
),
getById:
options.getProject ??
((id) =>
Effect.succeed(
id === projectId
? Option.some(project)
: id === otherProjectId
? Option.some(otherProject)
: Option.none(),
)),
getByWorkspaceRoot: () => Effect.succeedSome(project),
snapshot: Effect.die("unused"),
getShell: () => Effect.die("unused"),
Expand Down Expand Up @@ -1386,6 +1394,180 @@ it.effect("replays a server-allocated launch", () =>
}),
);

it.effect("MCP replays accepted launches and checks worktree membership", () =>
Effect.gen(function* () {
const setupEntered = yield* Deferred.make<void>();
const allowSetup = yield* Deferred.make<void>();
let changed = false;
const harness = makeHarness({
getProject: (id) =>
Effect.sync(() =>
id === projectId
? Option.some({ ...project, defaultModelSelection: changed ? null : modelSelection })
: id === otherProjectId
? Option.some(otherProject)
: Option.none(),
),
hasCommit: ({ refName }) => Effect.sync(() => refName === "HEAD" || !changed),
git: { hasRefNamed: () => Effect.succeed(false) },
runSetup: () =>
Deferred.succeed(setupEntered, undefined).pipe(
Effect.andThen(Deferred.await(allowSetup)),
Effect.as({ status: "no-script" as const }),
),
});
const context = McpInvocationContext.McpInvocationContext.of({
environmentId: EnvironmentId.make("launch-retry"),
requestNamespace: "client:launch-retry",
thread: undefined,
client: { sessionId: "launch-retry", label: "Test", access: "full-access" },
issuedAt: 0,
capabilities: new Set(["orchestration" as const]),
});
const dependencies = Layer.mergeAll(
harness.layer,
NodeCrypto.layer,
DocumentText.layer.pipe(Layer.provideMerge(NodeServices.layer)),
Layer.succeed(McpInvocationContext.McpInvocationContext, context),
Layer.mock(GitVcsDriver.GitVcsDriver)({
listWorktreePaths: () => Effect.succeed([project.workspaceRoot, "/repo-worktrees/feature"]),
}),
ServerConfig.layerTest(process.cwd(), { prefix: "t3-launch-retry-" }).pipe(
Layer.provide(NodeServices.layer),
),
);
yield* Effect.gen(function* () {
const toolkit = yield* ProjectToolkit.pipe(
Effect.provide(McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer)),
);
const input = {
title: "Only once",
projectId,
clientRequestId: "lost-response",
runtimeMode: "approval-required" as const,
message: "Do the work",
workspaceStrategy: { type: "worktree" as const, baseRef: "temporary-base" },
};
const launch = (params: Parameters<typeof toolkit.handle<"t3_thread_launch">>[1]) =>
toolkit.handle("t3_thread_launch", params).pipe(
Stream.unwrap,
Stream.runCollect,
Effect.map((results) => results.at(-1)?.result),
);
const fs = yield* FileSystem.FileSystem;
const outsidePath = yield* fs.makeTempDirectoryScoped({ prefix: "launch-unregistered-" });
const outsideStrategy = { type: "existing_worktree" as const, worktreePath: outsidePath };
const refusal = {
code: "invalid_request",
message:
"worktreePath must be one of the project's git worktrees. t3_worktree_list shows them.",
};
const idleInput = {
title: "Idle accepted launch",
projectId,
clientRequestId: "idle-retry",
workspaceStrategy: { type: "root" as const },
};
const idleLaunch = yield* launch(idleInput);
assert.include(idleLaunch, { runId: null, status: null });
assert(idleLaunch !== undefined && "threadId" in idleLaunch);
assert.include(
yield* launch({
...idleInput,
message: "Run outside the project",
workspaceStrategy: outsideStrategy,
}),
refusal,
);
const threads = yield* ThreadManagement.ThreadManagementService;
const idle = yield* threads.getThreadProjection(idleLaunch.threadId);
assert.equal(idle.messages.length, 0);
assert.equal(idle.runs.length, 0);
assert.equal(idle.thread.worktreePath, null);
assert.equal(harness.createWorktree.mock.calls.length, 0);
assert.equal(harness.runSetup.mock.calls.length, 0);

const first = yield* launch(input);
assert(first !== undefined && "threadId" in first);
assert.notProperty(first, "code");
yield* Deferred.await(setupEntered);
changed = true;

assert.include(yield* launch({ ...input, workspaceStrategy: outsideStrategy }), refusal);

assert.deepEqual(yield* launch(input), first);
assert.deepEqual(yield* launch({ ...input, modelSelection: claudeModelSelection }), first);
assert.include(yield* launch({ ...input, projectId: otherProjectId }), {
code: "orchestration_error",
});
assert.include(
yield* launch(input).pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, {
...context,
client: { ...context.client!, access: "read-only" },
}),
),
{ code: "capability_denied" },
);
assert.include(yield* launch({ ...input, clientRequestId: "fresh" }), {
code: "invalid_request",
});
assert.include(
yield* launch({
...input,
clientRequestId: "fresh-root",
workspaceStrategy: { type: "root" },
}),
{ code: "invalid_request" },
);
const threadId = first.threadId;
assert.equal((yield* threads.getThreadShell(threadId))?.runtimeMode, "approval-required");
yield* threads.dispatch({
type: "thread.runtime-mode.set",
commandId: CommandId.make("owner:raise-launch-modes"),
threadId,
runtimeMode: "full-access",
});
assert.include(
yield* launch(input).pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, {
...context,
client: { ...context.client!, access: "approval-required" },
}),
),
{ code: "orchestration_error" },
);
const projection = yield* threads.getThreadProjection(threadId);
assert.equal(projection.thread.runtimeMode, "full-access");
assert.equal(projection.messages.length, 1);
assert.equal(projection.runs.length, 1);
assert.equal(projection.runs[0]?.status, "preparing");
assert.deepEqual(projection.runs[0]?.workspacePreparation, input.workspaceStrategy);
assert.equal(projection.thread.worktreePath, "/repo-worktrees/feature");
assert.equal(harness.createWorktree.mock.calls.length, 1);
assert.equal(harness.runSetup.mock.calls.length, 1);
yield* Deferred.succeed(allowSetup, undefined);
yield* threads.streamStoredEventsFrom({ threadId }).pipe(
Stream.filter(
(stored) =>
stored.commandId === CommandId.make(`${threadId}:release`) &&
stored.event.type === "run.updated",
),
Stream.runHead,
);
assert.include(
yield* launch(input).pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, {
...context,
client: { ...context.client!, access: "approval-required" },
}),
),
{ threadId, status: "starting" },
);
}).pipe(Effect.provide(dependencies));
}),
);

it.effect("replays an accepted launch after the project disables the instance", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down Expand Up @@ -1742,19 +1924,38 @@ it.effect("schedules an accepted preparing message exactly once across concurren
text: "Resume preparation",
attachments: [],
modelSelection: input.modelSelection,
dispatchMode: { type: "defer_start" },
dispatchMode: { type: "defer_start", workspaceStrategy: input.workspaceStrategy },
createdBy: input.createdBy,
creationSource: input.creationSource,
});
const preparing = yield* threads.getThreadProjection(input.threadId);
assert.equal(preparing.runs[0]?.status, "preparing");

const [first, second] = yield* Effect.all([launches.launch(input), launches.launch(input)], {
const retry = { ...input, workspaceStrategy: { type: "root" as const } };
const [first, second] = yield* Effect.all([launches.launch(retry), launches.launch(retry)], {
concurrency: "unbounded",
});
yield* Deferred.await(setupEntered);
const prepared = yield* Effect.raceFirst(
Deferred.await(setupEntered).pipe(Effect.as(true)),
threads.streamStoredEventsFrom({ threadId: input.threadId }).pipe(
Stream.filter(
(stored) =>
stored.commandId === CommandId.make(input.commandId + ":release") &&
stored.event.type === "run.updated",
),
Stream.runHead,
Effect.as(false),
),
);
assert.isTrue(prepared, "the accepted worktree setup must run before releasing the run");
assert.isTrue(first.resumed);
assert.isTrue(second.resumed);
assert.equal(harness.createWorktree.mock.calls.length, 1);
assert.equal(harness.runSetup.mock.calls[0]?.[0].worktreePath, "/repo-worktrees/feature");
assert.deepEqual(
(yield* threads.getThreadProjection(input.threadId)).runs[0]?.workspacePreparation,
input.workspaceStrategy,
);
assert.equal(harness.runSetup.mock.calls.length, 1);
yield* Deferred.succeed(allowSetup, undefined);
}).pipe(Effect.provide(harness.layer));
Expand Down Expand Up @@ -1867,6 +2068,7 @@ it.effect("shared intake preserves durable attachment bytes after a lost launch
}),
),
),
getAcceptedLaunch: launches.getAcceptedLaunch,
checkWorktreeBase: launches.checkWorktreeBase,
retryPreparation: launches.retryPreparation,
prepareRun: launches.prepareRun,
Expand Down
Loading
Loading