diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
index 7b0759ae99b0b..98a7df66e2d7f 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
@@ -2307,6 +2307,93 @@ effectIt.effect.each([
}),
);
+effectIt.effect.each(
+ ["/compact", "/goal finish"].flatMap((command) =>
+ [false, true].map((steered) => ({ command, steered })),
+ ),
+)(
+ "keeps the switch note pending after a resumed $command unless steered: $steered",
+ ({ command, steered }) =>
+ Effect.gen(function* () {
+ const harness = makeLocalCommandHarness({
+ text: "Next prompt",
+ sideChat: { contextRun: "current" },
+ previousTurn: { runStatus: "completed", turnStatus: "completed" },
+ });
+ const projection = harness.projection();
+ const current = projection.runs.at(-1)!;
+ const previous = projection.runs[0]!;
+ const sourceId = RunId.make("unsent-command");
+ const resumedId = RunId.make("resumed-command");
+ const commandId = MessageId.make("command-message");
+ const earlierId = MessageId.make("earlier-message");
+ const resumedMessageId = MessageId.make("resume-message");
+ const resumedAttemptId = RunAttemptId.make("resumed-command-attempt");
+ const previousAttempt = projection.attempts[0]!;
+ harness.setProjection({
+ ...projection,
+ runs: [
+ {
+ ...previous,
+ providerInstanceId: harness.oldInstanceId,
+ userMessageId: earlierId,
+ ordinal: 1,
+ },
+ {
+ ...current,
+ id: sourceId,
+ providerThreadId: null,
+ userMessageId: commandId,
+ status: "interrupted",
+ ordinal: 2,
+ },
+ {
+ ...current,
+ id: resumedId,
+ userMessageId: resumedMessageId,
+ manualContinuationOfRunId: sourceId,
+ status: "completed",
+ ordinal: 3,
+ },
+ { ...current, ordinal: 4 },
+ ],
+ messages: [
+ { ...projection.messages[0]!, id: earlierId, text: "Earlier prompt" },
+ { ...projection.messages[0]!, id: commandId, text: command },
+ {
+ ...projection.messages[0]!,
+ id: resumedMessageId,
+ text: steered ? "Steered prompt" : "",
+ },
+ ...projection.messages,
+ ],
+ attempts: [
+ { ...previousAttempt, providerInstanceId: harness.oldInstanceId },
+ {
+ ...previousAttempt,
+ id: resumedAttemptId,
+ reason: steered ? "steering_restart" : "initial",
+ runId: resumedId,
+ providerInstanceId: harness.newInstanceId,
+ },
+ ...projection.attempts.slice(1),
+ ],
+ providerTurns: [
+ ...projection.providerTurns,
+ {
+ ...projection.providerTurns[0]!,
+ id: ProviderTurnId.make("resumed-command-turn"),
+ runAttemptId: resumedAttemptId,
+ },
+ ],
+ });
+ yield* harness.start;
+ expect(
+ harness.startTurn.mock.calls[0]?.[0].message.text.includes("switched by the user from"),
+ ).toBe(!steered);
+ }),
+);
+
effectIt.effect("leaves delegated children to their own runs when the parent starts a turn", () =>
Effect.gen(function* () {
const harness = makeLocalCommandHarness({
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
index 6587e742c7cd0..bae8ae20f8bc5 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -12,6 +12,7 @@ import {
type OrchestrationV2TurnItem,
isCompactionCommandMessage,
parseGoalCommand,
+ type ProviderInstanceId,
RunId,
RunAttemptId,
NodeId,
@@ -118,6 +119,14 @@ const refusedBeforePrompt = (error: unknown): boolean =>
error.detail === CLAUDE_QUERY_ENDED_BEFORE_TURN_DETAIL) ||
(Predicate.hasProperty(error, "cause") && refusedBeforePrompt(error.cause));
+// Same-driver account switches resume the native conversation, so nothing else tells the agent.
+type NamedInstance = { readonly id: string; readonly displayName?: string | undefined };
+export const providerSwitchNote = (from: NamedInstance, to: NamedInstance): string => {
+ const name = ({ id, displayName }: NamedInstance) =>
+ displayName === undefined ? `\`${id}\`` : `${displayName} (\`${id}\`)`;
+ return `Note: this thread now runs on ${name(to)}, switched by the user from ${name(from)}.`;
+};
+
/** Missing execution records cannot be repaired by retrying a start. */
export class ProviderTurnStartRunStateMissingError extends Schema.TaggedError()(
"ProviderTurnStartRunStateMissingError",
@@ -199,6 +208,7 @@ export const layer: Layer.Layer<
const runtimePolicy = yield* RuntimePolicy.RuntimePolicyV2;
const restoreMissingWorktree = yield* makeRestoreMissingWorktree;
const turnItemPositions = yield* TurnItemPositionStore.TurnItemPositionStoreV2;
+ const serverSettings = yield* ServerSettings.ServerSettingsService;
// Build draining callbacks outside startup scope so they cannot retain full thread history.
const makeRunControls = (input: {
@@ -848,18 +858,19 @@ export const layer: Layer.Layer<
projection.providerTurns,
projection.attempts,
);
+ const commandMessageIds = new Set(
+ projection.messages
+ .filter(
+ (candidate) =>
+ isCompactionCommandMessage(candidate) ||
+ (goalDriver && parseGoalCommand(candidate) !== null),
+ )
+ .map((candidate) => candidate.id),
+ );
const restartCancelledWork = pendingRestartCancelledBackgroundWork({
runs: projection.runs,
providerTurns: projection.providerTurns,
- compactionMessageIds: new Set(
- projection.messages
- .filter(
- (candidate) =>
- isCompactionCommandMessage(candidate) ||
- (goalDriver && parseGoalCommand(candidate) !== null),
- )
- .map((candidate) => candidate.id),
- ),
+ compactionMessageIds: commandMessageIds,
run,
attempts: projection.attempts,
});
@@ -867,6 +878,41 @@ export const layer: Layer.Layer<
restartCancelledWork.length === 0
? ""
: restartCancelledBackgroundWorkNote(restartCancelledWork);
+ // Attempts, not runs: restarting the active run moves it to the new account. Only a provider
+ // turn proves a prompt arrived, so a refused one keeps the note pending.
+ const acceptedAttemptIds = new Set(projection.providerTurns.map((turn) => turn.runAttemptId));
+ const previousAttempt = projection.runs
+ .filter((source) => source.ordinal <= run.ordinal && source.status !== "rolled_back")
+ .flatMap((source) =>
+ projection.attempts.filter(
+ (candidate) =>
+ candidate.runId === source.id &&
+ !commandMessageIds.has(
+ (candidate.reason === "steering_restart"
+ ? source
+ : (unsentManualContinuationSource(source, projection) ?? source)
+ ).userMessageId,
+ ),
+ ),
+ )
+ .findLast(
+ (candidate) => candidate.id !== attempt.id && acceptedAttemptIds.has(candidate.id),
+ );
+ const instances = yield* serverSettings.getSettings.pipe(
+ Effect.map((settings) => settings.providerInstances),
+ Effect.orElseSucceed(() => undefined),
+ );
+ const named = (id: ProviderInstanceId) => ({ id, displayName: instances?.[id]?.displayName });
+ const switchNote =
+ commandMessageIds.has(providerMessage.id) ||
+ previousAttempt === undefined ||
+ previousAttempt.providerInstanceId === run.providerInstanceId
+ ? ""
+ : providerSwitchNote(
+ named(previousAttempt.providerInstanceId),
+ named(run.providerInstanceId),
+ );
+ const turnNote = [restartNote, switchNote].filter((note) => note !== "").join("\n\n");
const tokenCap = yield* handoffTokenCapConfig.pipe(
Effect.orElseSucceed(() => DEFAULT_HANDOFF_TOKEN_CAP),
);
@@ -1012,7 +1058,7 @@ export const layer: Layer.Layer<
return handoffBudget({
tokenCap,
modelContextWindow,
- userText: restartNote === "" ? userText : `${restartNote}\n\n${userText}`,
+ userText: turnNote === "" ? userText : `${turnNote}\n\n${userText}`,
attachments: providerMessage.attachments,
providerThread: budgetProviderThread,
nativeContextEstimate:
@@ -1732,7 +1778,7 @@ export const layer: Layer.Layer<
sideChatHandoff === undefined || compact || goalCommand
? ""
: sideChatHandoff.summaryText;
- const context = [delivery.context, sideChatContext, restartNote]
+ const context = [delivery.context, sideChatContext, turnNote]
.filter((part) => part !== "")
.join("\n\n");
const { restartContinuationOfRunId: _resumedRunId, ...promptedInput } = turnInput;
@@ -1810,7 +1856,7 @@ export const layer: Layer.Layer<
effectiveHandoffs.length === 0 &&
retryHandoff.length === 0 &&
sideChatHandoff === undefined &&
- restartNote === "" &&
+ turnNote === "" &&
!noteContinuation
? session
: makeDeliverySession(session, startWithHandoffs);
diff --git a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
index 7b12f72f4cead..a5758ef46f72c 100644
--- a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
+++ b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
@@ -1016,7 +1016,9 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]
assert.deepEqual(resumes, [
{ instanceId: targetId, nativeId: originalThread.nativeThreadRef?.nativeId },
]);
- assert.deepEqual(messages, ["first", "second"]);
+ const secondTurn =
+ "Note: this thread now runs on Provider 2 (`codex-shadow-account`), switched by the user from `codex-restart-test`.\n\nUser message:\nsecond";
+ assert.deepEqual(messages, ["first", secondTurn]);
assert.equal((yield* Ref.get(state)).closedSessionCount, mode === "pooled" ? 0 : 1);
if (mode === "pooled") {
assert.isFalse(
@@ -1033,7 +1035,19 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]
assert.deepEqual(returnedThread.nativeThreadRef, originalThread.nativeThreadRef);
assert.equal(returnedThread.providerInstanceId, providerInstanceId);
assert.isEmpty(third.contextHandoffs);
- assert.deepEqual(messages, ["first", "second", "third"]);
- }).pipe(Effect.provide(ProviderReplayHarness.layerWithRegistry({ name }, layerRegistry)));
+ assert.deepEqual(messages, [
+ "first",
+ secondTurn,
+ "Note: this thread now runs on `codex-restart-test`, switched by the user from Provider 2 (`codex-shadow-account`).\n\nUser message:\nthird",
+ ]);
+ }).pipe(
+ Effect.provide(
+ ProviderReplayHarness.layerWithRegistry({ name }, layerRegistry, {
+ serverSettings: {
+ providerInstances: { [targetId]: { driver: "codex", displayName: "Provider 2" } },
+ },
+ }),
+ ),
+ );
}),
);
diff --git a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts
index 0ba9137e45dbc..bf019f59538e0 100644
--- a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts
+++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts
@@ -288,6 +288,7 @@ export function layerWithRegistry(
store: ProjectionStore.ProjectionStoreV2Shape,
) => ProjectionStore.ProjectionStoreV2Shape;
readonly managedPolicy?: ManagedPolicyDocument;
+ readonly serverSettings?: Parameters[0];
} = {},
): Layer.Layer<
| Orchestrator.OrchestratorV2
@@ -321,6 +322,7 @@ export function layerWithRegistry(
ServerSettings.layerTest(
{
responseStreamingMode: "turn",
+ ...options.serverSettings,
...(options.continueThreadsAfterServerUpdate === undefined
? {}
: { continueThreadsAfterServerUpdate: options.continueThreadsAfterServerUpdate }),
diff --git a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
index f07d942cbac2f..1ffabdd17fcb4 100644
--- a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
+++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
@@ -65,12 +65,16 @@ import {
GROK_MODEL_SELECTION,
} from "./fixtures/shared.ts";
import * as ProviderReplayHarness from "./ProviderReplayHarness.ts";
+import { withInlineContext } from "../ContextHandoffDelivery.ts";
+import { providerSwitchNote } from "../ProviderTurnStartService.ts";
import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
const threadId = ThreadId.make("thread:provider-switch");
+const switched = (from: string, to: string, text: string) =>
+ withInlineContext(providerSwitchNote({ id: from }, { id: to }), text);
const projectId = ProjectId.make("project:provider-switch");
const CODEX_DRIVER = ProviderDriverKind.make("codex");
const CLAUDE_DRIVER = ProviderDriverKind.make("claudeAgent");
@@ -1066,7 +1070,16 @@ describe("orchestration v2 provider switching", () => {
);
} else {
assert.equal(handoff.delivery?.status, "injected");
- assert.equal((yield* Ref.get(capturedTurns)).at(-1)!.text, current);
+ assert.equal(
+ (yield* Ref.get(capturedTurns)).at(-1)!.text,
+ [
+ "large-current-input",
+ "screenshot-reported-capacity-native",
+ "screenshot-option-change-retry-native",
+ ].includes(scenario)
+ ? switched("codex", "claudeAgent", current)
+ : current,
+ );
assert.include(
yield* encodeJson(yield* Ref.get(injectedHistory)),
"Original request with constraints",
@@ -1213,7 +1226,8 @@ describe("orchestration v2 provider switching", () => {
yield* wait(3, "completed");
const retried = yield* orchestrator.getThreadProjection(threadId);
const lastTurn = (yield* Ref.get(capturedTurns)).at(-1)!;
- assert.equal(lastTurn.text, retryText);
+ // The failed target turn never reached the provider, so the retry carries the note.
+ assert.equal(lastTurn.text, switched("codex", "claudeAgent", retryText));
if (failure !== "injection" && failure !== "exhausted-target-retry") {
const delta = yield* encodeJson(
(yield* Ref.get(injectedHistory)).slice(historyBeforeRetry),
@@ -1888,7 +1902,10 @@ describe("orchestration v2 provider switching", () => {
assert.include(deliveredHistory, partialResponse);
assert.include(deliveredHistory, `run-status=${status}`);
if (native) {
- assert.equal(delivered.text, "Continue");
+ assert.equal(
+ delivered.text,
+ switched(sourceSelection.instanceId, targetSelection.instanceId, "Continue"),
+ );
assert.notInclude(deliveredHistory, '"text":"Continue"');
assert.include(deliveredHistory, '"role":"assistant"');
assert.include(deliveredHistory, '"role":"user"');
@@ -2378,6 +2395,7 @@ describe("orchestration v2 provider switching", () => {
const cwd = yield* checkpointWorkspace("queued-account-switch");
const capturedTurns = yield* Ref.make>([]);
const started = yield* Deferred.make();
+ const release = yield* Deferred.make();
const alternateSelection: ModelSelection = {
...CODEX_MODEL_SELECTION,
instanceId: ProviderInstanceId.make("codex-alternate"),
@@ -2395,6 +2413,7 @@ describe("orchestration v2 provider switching", () => {
responseByRunOrdinal: {},
capturedTurns,
holdFirstTurn: started,
+ releaseFirstTurn: release,
}),
makeTestAdapter({
instanceId: alternateSelection.instanceId,
@@ -2438,7 +2457,6 @@ describe("orchestration v2 provider switching", () => {
const queuedThreadId = ThreadId.make("thread:queued-account-switch");
const projection = yield* Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
- const eventSink = yield* EventSink.EventSinkV2;
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
yield* orchestrator.dispatch({
type: "thread.create",
@@ -2489,20 +2507,7 @@ describe("orchestration v2 provider switching", () => {
(providerThread) => providerThread.id === queued.runs[0]?.providerThreadId,
)?.nativeThreadRef;
assert.isNotNull(sourceNativeRef);
- const now = yield* DateTime.now;
- yield* eventSink.write({
- events: [
- {
- id: EventId.make("event:queued-account-switch:first-complete"),
- type: "run.updated",
- threadId: queuedThreadId,
- runId: queued.runs[0]!.id,
- providerInstanceId: CODEX_MODEL_SELECTION.instanceId,
- occurredAt: now,
- payload: { ...queued.runs[0]!, status: "completed", completedAt: now },
- },
- ],
- });
+ yield* Deferred.succeed(release, undefined);
yield* orchestrator.resumeQueuedRuns;
yield* orchestrator.streamStoredEvents.pipe(
Stream.filter(
@@ -2549,7 +2554,10 @@ describe("orchestration v2 provider switching", () => {
assert.lengthOf(projection.contextHandoffs, 0);
assert.deepEqual(
(yield* Ref.get(capturedTurns)).map((turn) => turn.text),
- ["First account turn", "Alternate account turn"],
+ [
+ "First account turn",
+ "Note: this thread now runs on `codex-alternate`, switched by the user from `codex`.\n\nUser message:\nAlternate account turn",
+ ],
);
}),
);
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index df13d9cd6313b..ae80bd233121c 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -44,4 +44,6 @@
{"id": "settle-after-delayed-merge-waits", "area": "orchestration", "kind": "guard", "pattern": "settle before delayed work finishes", "experience": "Asked to merge a PR once its checks pass and then settle this thread, an agent following the #26749 settle bullet can settle in the first turn; a settled thread refuses watch_pull_request and drops PR-watch wakes, so the merge never happens.", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize. (the request is to settle after the merge, not now)", "privacy": "commit-safe", "notes": "From the CodeRabbit finding on #26765. Settling this thread applies at turn end, and Orchestrator.ts rejects watch wakes on a settled thread. Good: watch #123 and end the turn without settling. gh pr merge --auto plus a settle would still lose the wake that confirms the merge.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi
\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pending"}, {"name": "Test Web", "state": "pending"}]}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Merge it once its checks pass, then settle this thread.", "expect": {"calls": [{"tool": "watch_pull_request", "args": {"url": "~/pull/123"}}], "notCalls": ["t3_thread_organize", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait", "gh pr merge(?![^\\n]*--auto)"]}}
{"id": "settle-after-merge-on-wake", "area": "orchestration", "kind": "bad", "pattern": "settle read as plain English for finish", "experience": "Asked to merge a PR once its checks pass and then settle this thread, the agent merged on the checks-passed wake and ended the turn without calling t3_thread_organize (#26749).", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize.", "privacy": "commit-safe", "notes": "From #26749's delayed pattern. Wake text is copied from pullRequestWatch.ts. Good: merge #123, then settle this thread on the same turn.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}, {"role": "user", "text": "Merge it once its checks pass, then settle this thread."}, {"role": "assistant", "text": "T3 Code is watching [#123](https://github.com/acme/app/pull/123). I'll merge it once Check and Test Web pass, then settle this thread.", "toolCalls": [{"tool": "watch_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "watching": true, "wasWatching": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}, "unwatch_pull_request": "default"}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pass"}, {"name": "Test Web", "state": "pass"}], "mergeStateStatus": "CLEAN"}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Update on pull request #123 (https://github.com/acme/app/pull/123), which T3 Code is watching for you:\n- All 4 checks passed on abc1234.\n\nLook into each item and act on it as your task requires. This PR stays linked and watched, so do not link, watch, or list it again. End your turn when you are done, and T3 Code wakes you on the next change. When you hand the work back to the user, call unwatch_pull_request first so the thread returns to their inbox.", "expect": {"calls": [{"tool": "Bash", "args": {"command": "~gh pr merge"}}, {"tool": "t3_thread_organize", "args": {"action": "settle"}}], "notCalls": ["watch_pull_request", "t3_thread_update", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait"]}}
{"id": "thread-send-fresh-context", "area": "orchestration", "kind": "guard", "pattern": "t3_thread_send that should start a fresh provider session", "experience": "The user wanted a confused long-running thread to start over in the same thread without its old context; a plain send would keep feeding it the history.", "rule": "`freshContext: true` on t3_thread_send starts a new provider session in the same thread, like /clear.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "The thread working on the CSV importer keeps reverting to the old column mapping from early in its history."}, {"role": "assistant", "text": "That thread has a long history with three different mappings in it, so the model keeps picking up the stale one."}], "mocks": {"t3_thread_send": {"threadId": "11111111-1111-4111-8111-111111111111", "link": "", "messageId": "msg-1", "runId": "run-1", "status": "starting", "delivery": "started"}}}, "prompt": "Send it the final mapping below and have it start over with a clean context in that same thread, so the old history stops leaking in: thread 11111111-1111-4111-8111-111111111111.\n\nFinal mapping: id -> customer_id, amt -> amount_cents, ts -> created_at.", "expect": {"calls": [{"tool": "t3_thread_send", "args": {"threadId": "11111111-1111-4111-8111-111111111111", "freshContext": true}}], "notCalls": ["delegate_task", "t3_thread_launch", "create_threads"]}, "source": {"request": "juliusmarminge (maintainer) request, 2026-11-03: fresh provider sessions in one T3 thread"}, "notes": "Pairs with thread-send-to-named-top-level-thread, where an ordinary send must not set freshContext."}
+{"id": "account-switch-delegates-follow-thread", "area": "orchestration", "kind": "bad", "pattern": "delegates pinned to the account the thread left", "experience": "After the user moved a thread from one Claude account to another, the agent pinned new delegates to the old account, citing an earlier rate limit.", "rule": "A same-driver account switch resumes the native conversation; T3 notes the switch so the agent stops reusing the old providerInstanceId.", "privacy": "commit-safe", "setup": {"role": "top", "harness": "claude", "priorTurns": [{"role": "user", "text": "Provider 3 is out of usage, so I moved this thread to Provider 1. Start a background delegate to audit the webhook handlers in src/webhooks for missing idempotency checks."}, {"role": "assistant", "text": "Started the idempotency audit on Provider 1, since Provider 3 hit its rate limit. I'll report back when it finishes.", "toolCalls": [{"tool": "orchestrator_capabilities", "input": {}, "result": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-opus-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Provider 1", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-2", "driverKind": "claudeAgent", "displayName": "Provider 2", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-3", "driverKind": "claudeAgent", "displayName": "Provider 3", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}]}}, {"tool": "delegate_task", "input": {"title": "Webhook idempotency audit", "mode": "async", "clientRequestId": "webhook-audit", "target": {"providerInstanceId": "claudeAgent", "model": "claude-opus-5-5"}, "task": "Audit the webhook handlers in src/webhooks for missing idempotency checks. Read only. List each handler with its risk."}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "claudeAgent", "model": "claude-opus-5-5"}}]}], "providerSwitch": {"from": {"id": "claudeAgent", "displayName": "Provider 1"}, "to": {"id": "claude-2", "displayName": "Provider 2"}}, "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claude-2", "inheritedModel": "claude-opus-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Provider 1", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-2", "driverKind": "claudeAgent", "displayName": "Provider 2", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-3", "driverKind": "claudeAgent", "displayName": "Provider 3", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}]}, "delegate_task": "default"}}, "prompt": "Start two more background delegates: one to review the retry backoff in src/webhooks/retry.ts, and one to check test coverage for src/webhooks/.", "expect": {"minCalls": {"delegate_task": 2}, "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}, {"tool": "delegate_task", "args": {"target.providerInstanceId": "claude-3"}}]}, "source": {"pr": "https://github.com/pingdotgg/t3code/pull/32103"}}
+{"id": "account-switch-named-provider-control", "area": "orchestration", "kind": "control", "pattern": "user names the old account after a switch", "experience": "After an account switch, the user asks for delegates on the account the thread left; the agent should honor it.", "privacy": "commit-safe", "setup": {"role": "top", "harness": "claude", "priorTurns": [{"role": "user", "text": "Provider 3 is out of usage, so I moved this thread to Provider 1. Start a background delegate to audit the webhook handlers in src/webhooks for missing idempotency checks."}, {"role": "assistant", "text": "Started the idempotency audit on Provider 1, since Provider 3 hit its rate limit. I'll report back when it finishes.", "toolCalls": [{"tool": "orchestrator_capabilities", "input": {}, "result": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-opus-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Provider 1", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-2", "driverKind": "claudeAgent", "displayName": "Provider 2", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-3", "driverKind": "claudeAgent", "displayName": "Provider 3", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}]}}, {"tool": "delegate_task", "input": {"title": "Webhook idempotency audit", "mode": "async", "clientRequestId": "webhook-audit", "target": {"providerInstanceId": "claudeAgent", "model": "claude-opus-5-5"}, "task": "Audit the webhook handlers in src/webhooks for missing idempotency checks. Read only. List each handler with its risk."}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "claudeAgent", "model": "claude-opus-5-5"}}]}], "providerSwitch": {"from": {"id": "claudeAgent", "displayName": "Provider 1"}, "to": {"id": "claude-2", "displayName": "Provider 2"}}, "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claude-2", "inheritedModel": "claude-opus-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Provider 1", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-2", "driverKind": "claudeAgent", "displayName": "Provider 2", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}, {"providerInstanceId": "claude-3", "driverKind": "claudeAgent", "displayName": "Provider 3", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}], "canRunChildTask": true, "canRunCrossProviderChildTask": true, "constraints": []}]}, "delegate_task": "default"}}, "prompt": "Start two more background delegates: one to review the retry backoff in src/webhooks/retry.ts, and one to check test coverage for src/webhooks/. Put both on Provider 1.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}], "minCalls": {"delegate_task": 2}, "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": null}}, {"tool": "delegate_task", "args": {"target.providerInstanceId": "claude-2"}}, {"tool": "delegate_task", "args": {"target.providerInstanceId": "claude-3"}}]}, "source": {"pr": "https://github.com/pingdotgg/t3code/pull/32103"}}
{"id": "child-async-delegate-keeps-working", "area": "orchestration", "kind": "bad", "pattern": "async delegate ends a child's turn with work left", "experience": "A delegated child told to start an advisory async check and then rebase ended its turn right after delegate_task, because the result's next said to end the turn, and sat idle until the check finished.", "rule": "Follow a running delegate's `next`. (intended: keep doing other work; end the turn only when none is left)", "privacy": "commit-safe", "setup": {"role": "delegated-child", "delegateRole": "implementation", "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "codex", "inheritedModel": "gpt-6.1-sol", "inheritedOptions": [{"id": "reasoningEffort", "value": "xhigh"}, {"id": "serviceTier", "value": "priority"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Claude", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "fastMode", "type": "boolean", "default": false}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-haiku-5-5", "label": "Claude Haiku 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "codex", "driverKind": "codex", "displayName": "Codex", "models": [{"id": "gpt-6.1-sol", "label": "GPT-6.1 Sol", "options": [{"id": "reasoningEffort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "serviceTier", "values": ["default", "priority"], "default": "default"}]}]}, {"providerInstanceId": "muse", "driverKind": "muse", "displayName": "Muse Code", "runtimeModes": ["approval-required", "full-access"], "models": [{"id": "muse-wonder-full", "label": "Muse Wonder Full", "options": [{"id": "reasoningEffort", "values": ["minimal", "low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "grok", "driverKind": "grok", "displayName": "Grok", "models": [{"id": "grok-4.7", "label": "Grok 4.7", "options": []}]}]}, "delegate_task": "default", "task_status": "default"}, "world": {"repo": "acme/app", "branch": "feat/lexer", "status": "", "commits": [{"sha": "5c1e9a2", "subject": "lex strings"}, {"sha": "9b0d7f4", "subject": "lex numbers"}, {"sha": "2f8a6c1", "subject": "add token kinds"}], "main": "main's newest commit 7d3e0b5 'rename readChar to peek' changes lex.ts; rebasing feat/lexer onto main stops at a conflict in lex.ts on 'lex numbers'.", "files": {"lex.ts": "export function readChar(s: string, i: number) {\n return s[i];\n}\n\nexport function lexNumber(s: string, i: number) {\n return readChar(s, i);\n}\n", "kinds.ts": "export type Kind = \"num\" | \"str\";\n", "str.ts": "export function lexString(s: string) {\n return s.slice(1, -1);\n}\n", "/home/user/BRIEF.md": "# Rebase with an advisory background check\n\nRepository: `/home/user/app`. Work only in `/home/user/`.\n\nThe job: rebase `feat/lexer` onto `main` and resolve any conflicts so the code stays consistent with `main` (`main` renamed `readChar` to `peek`). Write `/home/user/report.md` with the new HEAD SHA and the check's output.\n\n## The background check\n\nDo this first, before the rebase: call the T3 tool `delegate_task` once with\n- `target`: `{\"providerInstanceId\": \"claudeAgent\", \"model\": \"claude-haiku-5-5\", \"options\": [{\"id\": \"effort\", \"value\": \"low\"}]}`\n- `mode`: `\"async\"`\n- `task`: `Run the shell command \\`sleep 120 && echo CHECK OK\\` and reply with its output only. Do not edit files or start subagents.`\n\nThe check is advisory: never wait for it. Start the rebase at once. Its result reaches you as a message. Start no other subagents.\n\nBefore each step, append `date -u +%T` and the step name to `/home/user/log.txt`.\n\nYour final message is one line.\n"}}}, "prompt": "Read /home/user/BRIEF.md and follow it.", "expect": {"calls": [{"tool": "delegate_task", "args": {"mode": "async", "target.model": "claude-haiku-5-5"}}], "shellMust": ["git\\s+(-C\\s+\\S+\\s+)?rebase\\b"], "maxCalls": {"task_status": 0, "t3_thread_wait": 0}, "shellMustNot": ["\\bsleep\\b"]}, "source": {"issue": "https://github.com/pingdotgg/t3code/issues/27126"}, "notes": "The reporter's brief and prompt, with Acme paths. The check is advisory, so the rebase must start in the same turn as delegate_task."}
diff --git a/evals/mcp-tool-use/run.test.ts b/evals/mcp-tool-use/run.test.ts
index ec1a121fee485..090b381dea348 100644
--- a/evals/mcp-tool-use/run.test.ts
+++ b/evals/mcp-tool-use/run.test.ts
@@ -751,6 +751,42 @@ it("replays earlier capability results with the model under test as the caller",
}
});
+it.each(["claude-opus-5-5", "gpt-6.1-sol"] as const)(
+ "untargeted delegates inherit the normalized %s caller",
+ (model) => {
+ const base = buildBase(template, { model, keepUserInstructions: false });
+ const sample = makeSample(
+ {
+ ...childCase,
+ setup: {
+ role: "top",
+ mocks: { orchestrator_capabilities: { inheritedProviderInstanceId: "claude-2" } },
+ },
+ },
+ model.startsWith("gpt-")
+ ? { ...base, codex: { input: [], reasoning: { effort: "xhigh" } } }
+ : base,
+ );
+ expect(t3Default(sample, "delegate_task", {})).toMatchObject({
+ providerInstanceId: model.startsWith("gpt-") ? "codex" : "claude-2",
+ });
+ },
+);
+
+it("sends a side chat's context and an account switch note together, as T3 does", () => {
+ const base = buildBase(template, { model: "claude-opus-5-5", keepUserInstructions: false });
+ const setup = {
+ role: "top" as const,
+ sideChat: { parentThreadId: "thread-parent", digest: ["The permissions fix is committed."] },
+ providerSwitch: { from: { id: "claude-1" }, to: { id: "claude-2" } },
+ };
+ const sent = JSON.stringify(
+ buildMessages(makeSample({ ...childCase, prompt: "Go", setup }, base)),
+ );
+ assert.include(sent, "The permissions fix is committed.");
+ assert.include(sent, JSON.stringify("from `claude-1`.\n\nUser message:\nGo").slice(1, -1));
+});
+
it("gives each launched thread its own id, and reading it back shows that thread's brief", () => {
const base = buildBase(template, { model: "claude-sonnet-5-5", keepUserInstructions: false });
const sample = makeSample(childCase, base);
diff --git a/evals/mcp-tool-use/run.ts b/evals/mcp-tool-use/run.ts
index 5786de748eaf1..4ba3663c0638b 100644
--- a/evals/mcp-tool-use/run.ts
+++ b/evals/mcp-tool-use/run.ts
@@ -52,6 +52,7 @@ import { formatThreadLink } from "@t3tools/shared/threadLinks";
import { buildCodexAdditionalContext } from "../../apps/server/src/provider/CodexDeveloperInstructions.ts";
import { makeSideChatContext } from "../../apps/server/src/orchestration-v2/ContextHandoffService.ts";
import { withInlineContext } from "../../apps/server/src/orchestration-v2/ContextHandoffDelivery.ts";
+import { providerSwitchNote } from "../../apps/server/src/orchestration-v2/ProviderTurnStartService.ts";
import { AttachmentToolkit } from "../../apps/server/src/mcp/toolkits/attachment/tools.ts";
import { DeviceToolkit } from "../../apps/server/src/mcp/toolkits/device/tools.ts";
import { EnvironmentToolkit } from "../../apps/server/src/mcp/toolkits/environment/tools.ts";
@@ -337,6 +338,13 @@ export const McpToolUseCase = Schema.Struct({
sideChat: Schema.optional(
Schema.Struct({ parentThreadId: Schema.String, digest: Schema.Array(Schema.String) }),
),
+ /** The user moved the thread to another provider instance: T3 notes it ahead of `prompt`. */
+ providerSwitch: Schema.optional(
+ Schema.Struct({
+ from: Schema.Struct({ id: Schema.String, displayName: Schema.optional(Schema.String) }),
+ to: Schema.Struct({ id: Schema.String, displayName: Schema.optional(Schema.String) }),
+ }),
+ ),
}),
/** Empty to resume a top-level thread without side-chat context after its last tool result. */
prompt: Schema.String,
@@ -1098,8 +1106,14 @@ export const buildMessages = (sample: Sample) => {
testCase.promptContext === undefined
? testCase.prompt
: projectComposerContextForProvider({ text: testCase.prompt, ...testCase.promptContext });
- const prompt =
- sideChatContext === undefined ? userText : withInlineContext(sideChatContext, userText);
+ const providerSwitch = testCase.setup.providerSwitch;
+ const inlineContext = [
+ sideChatContext,
+ providerSwitch && providerSwitchNote(providerSwitch.from, providerSwitch.to),
+ ]
+ .filter(Boolean)
+ .join("\n\n");
+ const prompt = inlineContext === "" ? userText : withInlineContext(inlineContext, userText);
const childPrompt = () => taskPrompt({ task: prompt, role: delegateRole });
if (testCase.prompt !== "")
push("user", text(role === "delegated-child" ? childPrompt() : prompt));
@@ -1203,8 +1217,7 @@ export const t3Default = (
...newThread(request, isRecord(request) ? request.prompt : undefined),
createdBy: "agent",
creationSource: "mcp",
- providerInstanceId:
- target.providerInstanceId ?? (sample.base.codex ? "codex" : "claudeAgent"),
+ providerInstanceId: target.providerInstanceId ?? inheritedInstance(sample),
model: target.model ?? sample.base.model,
};
}),
@@ -1225,8 +1238,7 @@ export const t3Default = (
status: "running",
workState: "working",
hasPendingChildRuns: false,
- providerInstanceId:
- target.providerInstanceId ?? (sample.base.codex ? "codex" : "claudeAgent"),
+ providerInstanceId: target.providerInstanceId ?? inheritedInstance(sample),
model: target.model ?? sample.base.model,
summary: null,
hasPendingQuestion: false,
@@ -1368,6 +1380,17 @@ const unsavedSecretNote = (Contracts as Record).SECRET_REQUEST_
| Readonly>
| undefined;
+/** The account a call without a target inherits: the case's capabilities mock, else the harness's. */
+const inheritedInstance = (sample: Sample) => {
+ const { base, testCase } = sample;
+ const mocked = capabilities(sample, testCase.setup.mocks?.orchestrator_capabilities);
+ return isRecord(mocked) && typeof mocked.inheritedProviderInstanceId === "string"
+ ? mocked.inheritedProviderInstanceId
+ : base.codex
+ ? "codex"
+ : "claudeAgent";
+};
+
/** Fields T3 adds to real thread and task results, applied to mocks and defaults alike. */
const withProductFields = (name: string, value: unknown, child: boolean): unknown => {
if (!isRecord(value)) return value;