From 8d18c0cc291adcf069c7666ca28984c41800536d Mon Sep 17 00:00:00 2001
From: Josh Gimenes
Date: Fri, 6 Nov 2026 10:04:40 -0700
Subject: [PATCH 1/9] fix(server): agents learn when the user moves their
thread to another account
Switching a thread between accounts of the same provider (for example two
Claude subscriptions) resumes the same native conversation, so the agent
never learns it moved. It keeps acting on earlier capability results and
usage limits, and can pin subagents to the old account.
The first prompted turn on a new provider instance now carries a one-line
note naming both instances (display name and id). It says earlier provider
facts may be stale and that leaving providerInstanceId unset keeps
subagents on the new one. Compaction and /goal turns stay verbatim; the
note waits for the next prompted turn.
Co-Authored-By: Claude Opus 5.5
---
.../ProviderTurnStartService.ts | 82 +++++++++++++++----
.../SelectionRestart.integration.test.ts | 12 ++-
.../ProviderSwitch.integration.test.ts | 21 ++++-
3 files changed, 96 insertions(+), 19 deletions(-)
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
index 9880da3408a77..8aca54e5e083a 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -12,7 +12,9 @@ import {
type OrchestrationV2TurnItem,
isCompactionCommandMessage,
parseGoalCommand,
+ type ProviderInstanceId,
RunId,
+ type ServerSettings as ServerSettingsValue,
ThreadId,
} from "@t3tools/contracts";
import * as Context from "effect/Context";
@@ -116,6 +118,19 @@ const refusedBeforePrompt = (error: unknown): boolean =>
PROMPT_REFUSAL_TAGS.some((tag) => Predicate.isTagged(error, tag)) ||
(Predicate.hasProperty(error, "cause") && refusedBeforePrompt(error.cause));
+// A same-driver account switch resumes the native conversation, so nothing else tells the agent.
+export const providerSwitchNote = (
+ instances: ServerSettingsValue["providerInstances"],
+ from: ProviderInstanceId,
+ to: ProviderInstanceId,
+): string => {
+ const name = (id: ProviderInstanceId) => {
+ const displayName = instances[id]?.displayName;
+ return displayName === undefined ? `\`${id}\`` : `${displayName} (\`${id}\`)`;
+ };
+ return `Note: the user moved this thread from ${name(from)} to ${name(to)}. Earlier provider choices and usage limits in this conversation may be out of date. To keep subagents on ${name(to)}, leave \`providerInstanceId\` unset.`;
+};
+
export interface ProviderTurnStartServiceV2Shape {
readonly start: (input: {
readonly threadId: ThreadId;
@@ -164,6 +179,7 @@ export const layer: Layer.Layer<
const runExecution = yield* RunExecutionService.RunExecutionServiceV2;
const runtimePolicy = yield* RuntimePolicy.RuntimePolicyV2;
const turnItemPositions = yield* TurnItemPositionStore.TurnItemPositionStoreV2;
+ const serverSettings = yield* ServerSettings.ServerSettingsService;
const isManagedWorktreePath = yield* makeIsManagedWorktreePath;
const resolveRecreateBase = Effect.fn("ProviderTurnStartService.resolveRecreateBase")(
@@ -859,25 +875,63 @@ 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,
});
- const restartNote =
+ // A restart of the active run moves that run to the new account, so compare attempts.
+ // Commands go verbatim, so the note waits for the next prompted turn.
+ const promptedRuns = new Map(
+ projection.runs
+ .filter((source) => !commandMessageIds.has(source.userMessageId))
+ .map((source) => [source.id, source.ordinal]),
+ );
+ const previousAttempt = projection.attempts
+ .filter(
+ (source) =>
+ source.id !== attempt.id &&
+ source.startedAt !== null &&
+ (promptedRuns.get(source.runId) ?? Infinity) <= run.ordinal,
+ )
+ .toSorted(
+ (a, b) =>
+ promptedRuns.get(a.runId)! - promptedRuns.get(b.runId)! ||
+ a.attemptOrdinal - b.attemptOrdinal,
+ )
+ .at(-1);
+ const switchNote =
+ commandMessageIds.has(message.id) ||
+ previousAttempt === undefined ||
+ previousAttempt.providerInstanceId === run.providerInstanceId
+ ? ""
+ : providerSwitchNote(
+ yield* serverSettings.getSettings.pipe(
+ Effect.map((settings) => settings.providerInstances),
+ Effect.orElseSucceed(() => ({})),
+ ),
+ previousAttempt.providerInstanceId,
+ run.providerInstanceId,
+ );
+ const turnNote = [
restartCancelledWork.length === 0
? ""
- : restartCancelledBackgroundWorkNote(restartCancelledWork);
+ : restartCancelledBackgroundWorkNote(restartCancelledWork),
+ switchNote,
+ ]
+ .filter((note) => note !== "")
+ .join("\n\n");
const tokenCap = yield* handoffTokenCapConfig.pipe(
Effect.orElseSucceed(() => DEFAULT_HANDOFF_TOKEN_CAP),
);
@@ -1023,7 +1077,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: message.attachments,
providerThread: budgetProviderThread,
nativeContextEstimate:
@@ -1668,7 +1722,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;
@@ -1735,7 +1789,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 ba49f0bcfba53..f3e803ff0445c 100644
--- a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
+++ b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
@@ -31,6 +31,8 @@ import * as EventSink from "./EventSink.ts";
import * as Orchestrator from "./Orchestrator.ts";
import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
import * as ProviderReplayHarness from "./testkit/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";
@@ -988,7 +990,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 switched = (from: typeof targetId, to: typeof targetId, text: string) =>
+ withInlineContext(providerSwitchNote({}, from, to), text);
+ assert.deepEqual(messages, ["first", switched(providerInstanceId, targetId, "second")]);
assert.equal((yield* Ref.get(state)).closedSessionCount, mode === "pooled" ? 0 : 1);
if (mode === "pooled") {
assert.isFalse(
@@ -1005,7 +1009,11 @@ 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"]);
+ assert.deepEqual(messages, [
+ "first",
+ switched(providerInstanceId, targetId, "second"),
+ switched(targetId, providerInstanceId, "third"),
+ ]);
}).pipe(Effect.provide(ProviderReplayHarness.layerWithRegistry({ name }, layerRegistry)));
}),
),
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 728e4f2189c26..94c7ca172fb71 100644
--- a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
+++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
@@ -1021,7 +1021,16 @@ describe("orchestration v2 provider switching", () => {
);
} else {
assert.equal(handoff.delivery?.status, "injected");
- assert.equal((yield* Ref.get(capturedTurns)).at(-1)!.text, current);
+ const text = (yield* Ref.get(capturedTurns)).at(-1)!.text;
+ if (
+ scenario === "large-current-input" ||
+ scenario === "screenshot-reported-capacity-native"
+ ) {
+ assert.match(text, /^Note: the user moved this thread /);
+ assert.isTrue(text.endsWith(`\n\nUser message:\n${current}`));
+ } else {
+ assert.equal(text, current);
+ }
assert.include(
yield* encodeJson(yield* Ref.get(injectedHistory)),
"Original request with constraints",
@@ -1749,7 +1758,10 @@ describe("orchestration v2 provider switching", () => {
assert.include(deliveredHistory, partialResponse);
assert.include(deliveredHistory, `run-status=${status}`);
if (native) {
- assert.equal(delivered.text, "Continue");
+ assert.match(
+ delivered.text,
+ /^Note: the user moved this thread .+\n\nUser message:\nContinue$/s,
+ );
assert.notInclude(deliveredHistory, '"text":"Continue"');
assert.include(deliveredHistory, '"role":"assistant"');
assert.include(deliveredHistory, '"role":"user"');
@@ -2300,7 +2312,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: the user moved this thread from `codex` to `codex-alternate`. Earlier provider choices and usage limits in this conversation may be out of date. To keep subagents on `codex-alternate`, leave `providerInstanceId` unset.\n\nUser message:\nAlternate account turn",
+ ],
);
}),
),
From e78be2c8153048950387c4beff5eee8aa183f2d3 Mon Sep 17 00:00:00 2001
From: Josh Gimenes
Date: Fri, 6 Nov 2026 10:35:00 -0700
Subject: [PATCH 2/9] fix(server): shorten the account switch note and keep it
after refused prompts
The note now reads "Note: this thread now runs on Extra (`claude-extra`),
switched by the user from Work (`claudeAgent`)."
An attempt's startedAt is set before the provider accepts the prompt, so a
refused first prompt on the new account consumed the note. The previous
attempt now has to have a provider turn, the same proof the restart note
uses.
Adds mcp-tool-use cases: delegates follow an account switch (bad) and the
user can still name the old account (control).
Co-Authored-By: Claude Opus 5.5
---
.../ProviderTurnStartService.ts | 33 ++--
.../SelectionRestart.integration.test.ts | 2 +-
.../ProviderSwitch.integration.test.ts | 148 +++++++++++++++++-
evals/mcp-tool-use/cases/orchestration.ndjson | 2 +
evals/mcp-tool-use/run.ts | 17 +-
5 files changed, 176 insertions(+), 26 deletions(-)
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
index 8aca54e5e083a..364da3272d095 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -14,7 +14,6 @@ import {
parseGoalCommand,
type ProviderInstanceId,
RunId,
- type ServerSettings as ServerSettingsValue,
ThreadId,
} from "@t3tools/contracts";
import * as Context from "effect/Context";
@@ -119,16 +118,11 @@ const refusedBeforePrompt = (error: unknown): boolean =>
(Predicate.hasProperty(error, "cause") && refusedBeforePrompt(error.cause));
// A same-driver account switch resumes the native conversation, so nothing else tells the agent.
-export const providerSwitchNote = (
- instances: ServerSettingsValue["providerInstances"],
- from: ProviderInstanceId,
- to: ProviderInstanceId,
-): string => {
- const name = (id: ProviderInstanceId) => {
- const displayName = instances[id]?.displayName;
- return displayName === undefined ? `\`${id}\`` : `${displayName} (\`${id}\`)`;
- };
- return `Note: the user moved this thread from ${name(from)} to ${name(to)}. Earlier provider choices and usage limits in this conversation may be out of date. To keep subagents on ${name(to)}, leave \`providerInstanceId\` unset.`;
+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)}.`;
};
export interface ProviderTurnStartServiceV2Shape {
@@ -898,11 +892,13 @@ export const layer: Layer.Layer<
.filter((source) => !commandMessageIds.has(source.userMessageId))
.map((source) => [source.id, source.ordinal]),
);
+ // Only a provider turn proves the prompt arrived; a refused prompt keeps the note pending.
+ const acceptedAttemptIds = new Set(projection.providerTurns.map((turn) => turn.runAttemptId));
const previousAttempt = projection.attempts
.filter(
(source) =>
source.id !== attempt.id &&
- source.startedAt !== null &&
+ acceptedAttemptIds.has(source.id) &&
(promptedRuns.get(source.runId) ?? Infinity) <= run.ordinal,
)
.toSorted(
@@ -911,18 +907,19 @@ export const layer: Layer.Layer<
a.attemptOrdinal - b.attemptOrdinal,
)
.at(-1);
+ 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(message.id) ||
previousAttempt === undefined ||
previousAttempt.providerInstanceId === run.providerInstanceId
? ""
: providerSwitchNote(
- yield* serverSettings.getSettings.pipe(
- Effect.map((settings) => settings.providerInstances),
- Effect.orElseSucceed(() => ({})),
- ),
- previousAttempt.providerInstanceId,
- run.providerInstanceId,
+ named(previousAttempt.providerInstanceId),
+ named(run.providerInstanceId),
);
const turnNote = [
restartCancelledWork.length === 0
diff --git a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
index f3e803ff0445c..a62a3675a7556 100644
--- a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
+++ b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
@@ -991,7 +991,7 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]
{ instanceId: targetId, nativeId: originalThread.nativeThreadRef?.nativeId },
]);
const switched = (from: typeof targetId, to: typeof targetId, text: string) =>
- withInlineContext(providerSwitchNote({}, from, to), text);
+ withInlineContext(providerSwitchNote({ id: from }, { id: to }), text);
assert.deepEqual(messages, ["first", switched(providerInstanceId, targetId, "second")]);
assert.equal((yield* Ref.get(state)).closedSessionCount, mode === "pooled" ? 0 : 1);
if (mode === "pooled") {
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 94c7ca172fb71..efa8b83bc1c54 100644
--- a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
+++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
@@ -62,6 +62,8 @@ 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";
@@ -1024,9 +1026,10 @@ describe("orchestration v2 provider switching", () => {
const text = (yield* Ref.get(capturedTurns)).at(-1)!.text;
if (
scenario === "large-current-input" ||
- scenario === "screenshot-reported-capacity-native"
+ scenario === "screenshot-reported-capacity-native" ||
+ scenario === "screenshot-option-change-retry-native"
) {
- assert.match(text, /^Note: the user moved this thread /);
+ assert.match(text, /^Note: this thread now runs on /);
assert.isTrue(text.endsWith(`\n\nUser message:\n${current}`));
} else {
assert.equal(text, current);
@@ -1179,7 +1182,14 @@ 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,
+ withInlineContext(
+ providerSwitchNote({ id: "codex" }, { id: "claudeAgent" }),
+ retryText,
+ ),
+ );
if (failure !== "injection" && failure !== "exhausted-target-retry") {
const delta = yield* encodeJson(
(yield* Ref.get(injectedHistory)).slice(historyBeforeRetry),
@@ -1760,7 +1770,7 @@ describe("orchestration v2 provider switching", () => {
if (native) {
assert.match(
delivered.text,
- /^Note: the user moved this thread .+\n\nUser message:\nContinue$/s,
+ /^Note: this thread now runs on .+\n\nUser message:\nContinue$/s,
);
assert.notInclude(deliveredHistory, '"text":"Continue"');
assert.include(deliveredHistory, '"role":"assistant"');
@@ -2264,6 +2274,27 @@ describe("orchestration v2 provider switching", () => {
occurredAt: now,
payload: { ...queued.runs[0]!, status: "completed", completedAt: now },
},
+ // A real provider records its turn; the held fixture turn stops before that.
+ {
+ id: EventId.make("event:queued-account-switch:first-turn"),
+ type: "provider-turn.updated",
+ threadId: queuedThreadId,
+ runId: queued.runs[0]!.id,
+ nodeId: queued.runs[0]!.rootNodeId!,
+ providerInstanceId: CODEX_MODEL_SELECTION.instanceId,
+ occurredAt: now,
+ payload: {
+ id: ProviderTurnId.make("provider-turn:queued-account-switch:first"),
+ providerThreadId: queued.runs[0]!.providerThreadId!,
+ nodeId: queued.runs[0]!.rootNodeId!,
+ runAttemptId: queued.runs[0]!.activeAttemptId,
+ nativeTurnRef: null,
+ ordinal: 1,
+ status: "completed",
+ startedAt: now,
+ completedAt: now,
+ },
+ },
],
});
yield* orchestrator.resumeQueuedRuns;
@@ -2314,13 +2345,120 @@ describe("orchestration v2 provider switching", () => {
(yield* Ref.get(capturedTurns)).map((turn) => turn.text),
[
"First account turn",
- "Note: the user moved this thread from `codex` to `codex-alternate`. Earlier provider choices and usage limits in this conversation may be out of date. To keep subagents on `codex-alternate`, leave `providerInstanceId` unset.\n\nUser message:\nAlternate account turn",
+ "Note: this thread now runs on `codex-alternate`, switched by the user from `codex`.\n\nUser message:\nAlternate account turn",
],
);
}),
),
);
+ it.live("keeps the account switch note pending after a refused prompt", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const cwd = yield* checkpointWorkspace("refused-account-switch");
+ const capturedTurns = yield* Ref.make>([]);
+ const refuseStarts = yield* Ref.make(0);
+ const alternateSelection: ModelSelection = {
+ ...CLAUDE_MODEL_SELECTION,
+ instanceId: ProviderInstanceId.make("claude-alternate"),
+ };
+ const adapters = [CLAUDE_MODEL_SELECTION, alternateSelection].map((modelSelection) =>
+ makeTestAdapter({
+ instanceId: modelSelection.instanceId,
+ driver: CLAUDE_DRIVER,
+ capabilities: ClaudeProviderCapabilitiesV2,
+ modelSelection,
+ responseByRunOrdinal: {},
+ capturedTurns,
+ refuseStarts,
+ }),
+ );
+ const layerRegistry = Layer.succeed(
+ ProviderAdapterRegistry.ProviderAdapterRegistryV2,
+ ProviderAdapterRegistry.ProviderAdapterRegistryV2.of({
+ get: (instanceId) =>
+ Effect.succeed(adapters.find((adapter) => adapter.instanceId === instanceId)!),
+ list: () => Effect.succeed(adapters.map((adapter) => adapter.instanceId)),
+ getMetadata: () =>
+ Effect.succeed({
+ driver: CLAUDE_DRIVER,
+ continuationKey: "claude:shared-native-account-history",
+ enabled: true,
+ capabilities: ClaudeProviderCapabilitiesV2,
+ }),
+ }),
+ );
+ yield* Effect.gen(function* () {
+ const orchestrator = yield* Orchestrator.OrchestratorV2;
+ const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
+ const run = Effect.fn("run")(function* (ordinal: number, modelSelection: ModelSelection) {
+ yield* orchestrator.dispatch({
+ type: "message.dispatch",
+ commandId: CommandId.make(`refused-account-switch:${ordinal}`),
+ threadId,
+ messageId: MessageId.make(`refused-account-switch:${ordinal}`),
+ createdBy: "user",
+ creationSource: "web",
+ text: `Request ${ordinal}`,
+ attachments: [],
+ modelSelection,
+ dispatchMode: { type: "start_immediately" },
+ });
+ yield* orchestrator.streamStoredEvents.pipe(
+ Stream.filter(
+ ({ event }) =>
+ event.type === "run.updated" &&
+ event.payload.ordinal === ordinal &&
+ (event.payload.status === "completed" || event.payload.status === "failed"),
+ ),
+ Stream.runHead,
+ );
+ yield* worker.drain();
+ return (yield* orchestrator.getThreadProjection(threadId)).runs.at(-1)?.status;
+ });
+ yield* orchestrator.dispatch({
+ type: "thread.create",
+ commandId: CommandId.make("refused-account-switch:create"),
+ threadId,
+ projectId,
+ createdBy: "user",
+ creationSource: "web",
+ title: "Refused account switch",
+ modelSelection: CLAUDE_MODEL_SELECTION,
+ runtimeMode: "full-access",
+ interactionMode: "default",
+ branch: null,
+ worktreePath: null,
+ });
+ assert.equal(yield* run(1, CLAUDE_MODEL_SELECTION), "completed");
+ yield* Ref.set(refuseStarts, 1);
+ assert.equal(yield* run(2, alternateSelection), "failed");
+ assert.equal(yield* run(3, alternateSelection), "completed");
+ const turns = yield* Ref.get(capturedTurns);
+ assert.lengthOf(turns, 2);
+ assert.include(
+ turns[1]!.text,
+ "Note: this thread now runs on `claude-alternate`, switched by the user from `claudeAgent`.",
+ );
+ }).pipe(
+ Effect.provide(
+ ProviderReplayHarness.layerWithRegistry(
+ {
+ name: "refused-account-switch",
+ runtimePolicyOverride: {
+ cwd,
+ approvalPolicy: "never",
+ sandboxPolicy: { type: "readOnly" },
+ },
+ },
+ layerRegistry,
+ ),
+ ),
+ );
+ }),
+ ),
+ );
+
it.live("finishes earlier queued Codex turns before handing context to queued Claude", () =>
Effect.scoped(
Effect.gen(function* () {
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index 520bd2abf10d3..26fc417773462 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -40,3 +40,5 @@
{"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": "Personal is out of usage, so I moved this thread to Work. Start a background delegate to audit the webhook handlers in src/webhooks for missing idempotency checks."}, {"role": "assistant", "text": "Started the idempotency audit on Work, since Personal 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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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": "Work"}, "to": {"id": "claude-extra", "displayName": "Extra"}}, "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claude-extra", "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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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": {"calls": [{"tool": "delegate_task"}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}]}, "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": "Personal is out of usage, so I moved this thread to Work. Start a background delegate to audit the webhook handlers in src/webhooks for missing idempotency checks."}, {"role": "assistant", "text": "Started the idempotency audit on Work, since Personal 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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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": "Work"}, "to": {"id": "claude-extra", "displayName": "Extra"}}, "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claude-extra", "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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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 Work.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claude-extra"}}]}, "source": {"pr": "https://github.com/pingdotgg/t3code/pull/32103"}}
diff --git a/evals/mcp-tool-use/run.ts b/evals/mcp-tool-use/run.ts
index 7a7a8400fe612..b3790d17ad613 100644
--- a/evals/mcp-tool-use/run.ts
+++ b/evals/mcp-tool-use/run.ts
@@ -50,6 +50,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";
@@ -320,6 +321,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,
@@ -1021,8 +1029,13 @@ 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 prompt = withInlineContext(
+ [sideChatContext, providerSwitch && providerSwitchNote(providerSwitch.from, providerSwitch.to)]
+ .filter((part) => part !== undefined)
+ .join("\n\n"),
+ userText,
+ );
const childPrompt = () => taskPrompt({ task: prompt, role: delegateRole });
if (testCase.prompt !== "")
push("user", text(role === "delegated-child" ? childPrompt() : prompt));
From a9d0483dec032df8a4ec4a83a28bb2d9ed1addcf Mon Sep 17 00:00:00 2001
From: Josh Gimenes
Date: Fri, 6 Nov 2026 10:57:01 -0700
Subject: [PATCH 3/9] refactor(server): trim the account switch note change
Leaves the restart note untouched and joins both notes in one line. Finds
the previous accepted attempt by walking runs in order. Drops the
refused-prompt test, which duplicated the existing recovery tests: they
already fail when a pre-prompt failure consumes the note. The queued
account switch test now lets its first turn finish instead of faking the
completion.
Co-Authored-By: Claude Opus 5.5
---
.../ProviderTurnStartService.ts | 42 ++--
.../ProviderSwitch.integration.test.ts | 181 ++----------------
evals/mcp-tool-use/run.ts | 11 +-
3 files changed, 38 insertions(+), 196 deletions(-)
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
index 364da3272d095..0386815501e9e 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -117,7 +117,7 @@ const refusedBeforePrompt = (error: unknown): boolean =>
PROMPT_REFUSAL_TAGS.some((tag) => Predicate.isTagged(error, tag)) ||
(Predicate.hasProperty(error, "cause") && refusedBeforePrompt(error.cause));
-// A same-driver account switch resumes the native conversation, so nothing else tells the agent.
+// 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) =>
@@ -885,28 +885,23 @@ export const layer: Layer.Layer<
run,
attempts: projection.attempts,
});
- // A restart of the active run moves that run to the new account, so compare attempts.
- // Commands go verbatim, so the note waits for the next prompted turn.
- const promptedRuns = new Map(
- projection.runs
- .filter((source) => !commandMessageIds.has(source.userMessageId))
- .map((source) => [source.id, source.ordinal]),
- );
- // Only a provider turn proves the prompt arrived; a refused prompt keeps the note pending.
+ const restartNote =
+ 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.attempts
+ const previousAttempt = projection.runs
.filter(
- (source) =>
- source.id !== attempt.id &&
- acceptedAttemptIds.has(source.id) &&
- (promptedRuns.get(source.runId) ?? Infinity) <= run.ordinal,
+ (source) => source.ordinal <= run.ordinal && !commandMessageIds.has(source.userMessageId),
)
- .toSorted(
- (a, b) =>
- promptedRuns.get(a.runId)! - promptedRuns.get(b.runId)! ||
- a.attemptOrdinal - b.attemptOrdinal,
+ .flatMap((source) =>
+ projection.attempts.filter((candidate) => candidate.runId === source.id),
)
- .at(-1);
+ .findLast(
+ (candidate) => candidate.id !== attempt.id && acceptedAttemptIds.has(candidate.id),
+ );
const instances = yield* serverSettings.getSettings.pipe(
Effect.map((settings) => settings.providerInstances),
Effect.orElseSucceed(() => undefined),
@@ -921,14 +916,7 @@ export const layer: Layer.Layer<
named(previousAttempt.providerInstanceId),
named(run.providerInstanceId),
);
- const turnNote = [
- restartCancelledWork.length === 0
- ? ""
- : restartCancelledBackgroundWorkNote(restartCancelledWork),
- switchNote,
- ]
- .filter((note) => note !== "")
- .join("\n\n");
+ const turnNote = [restartNote, switchNote].filter((note) => note !== "").join("\n\n");
const tokenCap = yield* handoffTokenCapConfig.pipe(
Effect.orElseSucceed(() => DEFAULT_HANDOFF_TOKEN_CAP),
);
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 efa8b83bc1c54..5b00d5dec9525 100644
--- a/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
+++ b/apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
@@ -70,6 +70,8 @@ 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");
@@ -1023,17 +1025,16 @@ describe("orchestration v2 provider switching", () => {
);
} else {
assert.equal(handoff.delivery?.status, "injected");
- const text = (yield* Ref.get(capturedTurns)).at(-1)!.text;
- if (
- scenario === "large-current-input" ||
- scenario === "screenshot-reported-capacity-native" ||
- scenario === "screenshot-option-change-retry-native"
- ) {
- assert.match(text, /^Note: this thread now runs on /);
- assert.isTrue(text.endsWith(`\n\nUser message:\n${current}`));
- } else {
- assert.equal(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",
@@ -1183,13 +1184,7 @@ describe("orchestration v2 provider switching", () => {
const retried = yield* orchestrator.getThreadProjection(threadId);
const lastTurn = (yield* Ref.get(capturedTurns)).at(-1)!;
// The failed target turn never reached the provider, so the retry carries the note.
- assert.equal(
- lastTurn.text,
- withInlineContext(
- providerSwitchNote({ id: "codex" }, { id: "claudeAgent" }),
- retryText,
- ),
- );
+ 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),
@@ -1768,9 +1763,9 @@ describe("orchestration v2 provider switching", () => {
assert.include(deliveredHistory, partialResponse);
assert.include(deliveredHistory, `run-status=${status}`);
if (native) {
- assert.match(
+ assert.equal(
delivered.text,
- /^Note: this thread now runs on .+\n\nUser message:\nContinue$/s,
+ switched(sourceSelection.instanceId, targetSelection.instanceId, "Continue"),
);
assert.notInclude(deliveredHistory, '"text":"Continue"');
assert.include(deliveredHistory, '"role":"assistant"');
@@ -2151,6 +2146,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"),
@@ -2168,6 +2164,7 @@ describe("orchestration v2 provider switching", () => {
responseByRunOrdinal: {},
capturedTurns,
holdFirstTurn: started,
+ releaseFirstTurn: release,
}),
makeTestAdapter({
instanceId: alternateSelection.instanceId,
@@ -2211,7 +2208,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",
@@ -2262,41 +2258,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 },
- },
- // A real provider records its turn; the held fixture turn stops before that.
- {
- id: EventId.make("event:queued-account-switch:first-turn"),
- type: "provider-turn.updated",
- threadId: queuedThreadId,
- runId: queued.runs[0]!.id,
- nodeId: queued.runs[0]!.rootNodeId!,
- providerInstanceId: CODEX_MODEL_SELECTION.instanceId,
- occurredAt: now,
- payload: {
- id: ProviderTurnId.make("provider-turn:queued-account-switch:first"),
- providerThreadId: queued.runs[0]!.providerThreadId!,
- nodeId: queued.runs[0]!.rootNodeId!,
- runAttemptId: queued.runs[0]!.activeAttemptId,
- nativeTurnRef: null,
- ordinal: 1,
- status: "completed",
- startedAt: now,
- completedAt: now,
- },
- },
- ],
- });
+ yield* Deferred.succeed(release, undefined);
yield* orchestrator.resumeQueuedRuns;
yield* orchestrator.streamStoredEvents.pipe(
Stream.filter(
@@ -2352,113 +2314,6 @@ describe("orchestration v2 provider switching", () => {
),
);
- it.live("keeps the account switch note pending after a refused prompt", () =>
- Effect.scoped(
- Effect.gen(function* () {
- const cwd = yield* checkpointWorkspace("refused-account-switch");
- const capturedTurns = yield* Ref.make>([]);
- const refuseStarts = yield* Ref.make(0);
- const alternateSelection: ModelSelection = {
- ...CLAUDE_MODEL_SELECTION,
- instanceId: ProviderInstanceId.make("claude-alternate"),
- };
- const adapters = [CLAUDE_MODEL_SELECTION, alternateSelection].map((modelSelection) =>
- makeTestAdapter({
- instanceId: modelSelection.instanceId,
- driver: CLAUDE_DRIVER,
- capabilities: ClaudeProviderCapabilitiesV2,
- modelSelection,
- responseByRunOrdinal: {},
- capturedTurns,
- refuseStarts,
- }),
- );
- const layerRegistry = Layer.succeed(
- ProviderAdapterRegistry.ProviderAdapterRegistryV2,
- ProviderAdapterRegistry.ProviderAdapterRegistryV2.of({
- get: (instanceId) =>
- Effect.succeed(adapters.find((adapter) => adapter.instanceId === instanceId)!),
- list: () => Effect.succeed(adapters.map((adapter) => adapter.instanceId)),
- getMetadata: () =>
- Effect.succeed({
- driver: CLAUDE_DRIVER,
- continuationKey: "claude:shared-native-account-history",
- enabled: true,
- capabilities: ClaudeProviderCapabilitiesV2,
- }),
- }),
- );
- yield* Effect.gen(function* () {
- const orchestrator = yield* Orchestrator.OrchestratorV2;
- const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
- const run = Effect.fn("run")(function* (ordinal: number, modelSelection: ModelSelection) {
- yield* orchestrator.dispatch({
- type: "message.dispatch",
- commandId: CommandId.make(`refused-account-switch:${ordinal}`),
- threadId,
- messageId: MessageId.make(`refused-account-switch:${ordinal}`),
- createdBy: "user",
- creationSource: "web",
- text: `Request ${ordinal}`,
- attachments: [],
- modelSelection,
- dispatchMode: { type: "start_immediately" },
- });
- yield* orchestrator.streamStoredEvents.pipe(
- Stream.filter(
- ({ event }) =>
- event.type === "run.updated" &&
- event.payload.ordinal === ordinal &&
- (event.payload.status === "completed" || event.payload.status === "failed"),
- ),
- Stream.runHead,
- );
- yield* worker.drain();
- return (yield* orchestrator.getThreadProjection(threadId)).runs.at(-1)?.status;
- });
- yield* orchestrator.dispatch({
- type: "thread.create",
- commandId: CommandId.make("refused-account-switch:create"),
- threadId,
- projectId,
- createdBy: "user",
- creationSource: "web",
- title: "Refused account switch",
- modelSelection: CLAUDE_MODEL_SELECTION,
- runtimeMode: "full-access",
- interactionMode: "default",
- branch: null,
- worktreePath: null,
- });
- assert.equal(yield* run(1, CLAUDE_MODEL_SELECTION), "completed");
- yield* Ref.set(refuseStarts, 1);
- assert.equal(yield* run(2, alternateSelection), "failed");
- assert.equal(yield* run(3, alternateSelection), "completed");
- const turns = yield* Ref.get(capturedTurns);
- assert.lengthOf(turns, 2);
- assert.include(
- turns[1]!.text,
- "Note: this thread now runs on `claude-alternate`, switched by the user from `claudeAgent`.",
- );
- }).pipe(
- Effect.provide(
- ProviderReplayHarness.layerWithRegistry(
- {
- name: "refused-account-switch",
- runtimePolicyOverride: {
- cwd,
- approvalPolicy: "never",
- sandboxPolicy: { type: "readOnly" },
- },
- },
- layerRegistry,
- ),
- ),
- );
- }),
- ),
- );
-
it.live("finishes earlier queued Codex turns before handing context to queued Claude", () =>
Effect.scoped(
Effect.gen(function* () {
diff --git a/evals/mcp-tool-use/run.ts b/evals/mcp-tool-use/run.ts
index b3790d17ad613..3805f46a70e66 100644
--- a/evals/mcp-tool-use/run.ts
+++ b/evals/mcp-tool-use/run.ts
@@ -1030,12 +1030,11 @@ export const buildMessages = (sample: Sample) => {
? testCase.prompt
: projectComposerContextForProvider({ text: testCase.prompt, ...testCase.promptContext });
const providerSwitch = testCase.setup.providerSwitch;
- const prompt = withInlineContext(
- [sideChatContext, providerSwitch && providerSwitchNote(providerSwitch.from, providerSwitch.to)]
- .filter((part) => part !== undefined)
- .join("\n\n"),
- userText,
- );
+ const inlineContext =
+ sideChatContext ??
+ (providerSwitch && providerSwitchNote(providerSwitch.from, providerSwitch.to));
+ const prompt =
+ inlineContext === undefined ? userText : withInlineContext(inlineContext, userText);
const childPrompt = () => taskPrompt({ task: prompt, role: delegateRole });
if (testCase.prompt !== "")
push("user", text(role === "delegated-child" ? childPrompt() : prompt));
From 6fe6c9c904c4d3c0212c027c78471886279a0066 Mon Sep 17 00:00:00 2001
From: Josh Gimenes
Date: Fri, 6 Nov 2026 11:01:33 -0700
Subject: [PATCH 4/9] test(evals): use generic account names in the account
switch cases
Co-Authored-By: Claude Opus 5.5
---
evals/mcp-tool-use/cases/orchestration.ndjson | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index 26fc417773462..fb5427598ccd6 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -40,5 +40,5 @@
{"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": "Personal is out of usage, so I moved this thread to Work. Start a background delegate to audit the webhook handlers in src/webhooks for missing idempotency checks."}, {"role": "assistant", "text": "Started the idempotency audit on Work, since Personal 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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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": "Work"}, "to": {"id": "claude-extra", "displayName": "Extra"}}, "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claude-extra", "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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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": {"calls": [{"tool": "delegate_task"}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}]}, "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": "Personal is out of usage, so I moved this thread to Work. Start a background delegate to audit the webhook handlers in src/webhooks for missing idempotency checks."}, {"role": "assistant", "text": "Started the idempotency audit on Work, since Personal 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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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": "Work"}, "to": {"id": "claude-extra", "displayName": "Extra"}}, "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claude-extra", "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": "Work", "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-extra", "driverKind": "claudeAgent", "displayName": "Extra", "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-secondary", "driverKind": "claudeAgent", "displayName": "Personal", "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 Work.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claude-extra"}}]}, "source": {"pr": "https://github.com/pingdotgg/t3code/pull/32103"}}
+{"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": {"calls": [{"tool": "delegate_task"}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}]}, "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"}}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claude-2"}}]}, "source": {"pr": "https://github.com/pingdotgg/t3code/pull/32103"}}
From d35be376c7e7b11b02f14ea789e15fc5f95302a7 Mon Sep 17 00:00:00 2001
From: Josh Gimenes
Date: Wed, 11 Nov 2026 11:12:42 -0700
Subject: [PATCH 5/9] fix(server): skip rolled-back runs when naming the
previous account
Also tests display names end to end, sends side chat context and the switch
note together in evals, and checks every delegate's account in the eval cases.
Co-Authored-By: Claude Opus 5.5
---
.../ProviderTurnStartService.ts | 5 ++++-
.../SelectionRestart.integration.test.ts | 22 ++++++++++++-------
.../testkit/ProviderReplayHarness.ts | 2 ++
evals/mcp-tool-use/cases/orchestration.ndjson | 4 ++--
evals/mcp-tool-use/run.test.ts | 14 ++++++++++++
evals/mcp-tool-use/run.ts | 12 +++++-----
6 files changed, 43 insertions(+), 16 deletions(-)
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
index 0386815501e9e..5bf5c84914afa 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -894,7 +894,10 @@ export const layer: Layer.Layer<
const acceptedAttemptIds = new Set(projection.providerTurns.map((turn) => turn.runAttemptId));
const previousAttempt = projection.runs
.filter(
- (source) => source.ordinal <= run.ordinal && !commandMessageIds.has(source.userMessageId),
+ (source) =>
+ source.ordinal <= run.ordinal &&
+ source.status !== "rolled_back" &&
+ !commandMessageIds.has(source.userMessageId),
)
.flatMap((source) =>
projection.attempts.filter((candidate) => candidate.runId === source.id),
diff --git a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
index a62a3675a7556..ae6fbafdbd3d9 100644
--- a/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
+++ b/apps/server/src/orchestration-v2/SelectionRestart.integration.test.ts
@@ -31,8 +31,6 @@ import * as EventSink from "./EventSink.ts";
import * as Orchestrator from "./Orchestrator.ts";
import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
import * as ProviderReplayHarness from "./testkit/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";
@@ -990,9 +988,9 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]
assert.deepEqual(resumes, [
{ instanceId: targetId, nativeId: originalThread.nativeThreadRef?.nativeId },
]);
- const switched = (from: typeof targetId, to: typeof targetId, text: string) =>
- withInlineContext(providerSwitchNote({ id: from }, { id: to }), text);
- assert.deepEqual(messages, ["first", switched(providerInstanceId, targetId, "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(
@@ -1011,10 +1009,18 @@ it.live.each(["active", "idle", "selection-command", "pooled", "separate-home"]
assert.isEmpty(third.contextHandoffs);
assert.deepEqual(messages, [
"first",
- switched(providerInstanceId, targetId, "second"),
- switched(targetId, providerInstanceId, "third"),
+ 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)));
+ }).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 363164b688043..d049af065ca3f 100644
--- a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts
+++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts
@@ -283,6 +283,7 @@ export function layerWithRegistry(
readonly recoverOnStartup?: boolean;
readonly continueThreadsAfterServerUpdate?: boolean;
readonly managedPolicy?: ManagedPolicyDocument;
+ readonly serverSettings?: Parameters[0];
} = {},
): Layer.Layer<
| Orchestrator.OrchestratorV2
@@ -314,6 +315,7 @@ export function layerWithRegistry(
ServerSettings.layerTest(
{
responseStreamingMode: "turn",
+ ...options.serverSettings,
...(options.continueThreadsAfterServerUpdate === undefined
? {}
: { continueThreadsAfterServerUpdate: options.continueThreadsAfterServerUpdate }),
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index fb5427598ccd6..a4820534ffdc5 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -40,5 +40,5 @@
{"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": {"calls": [{"tool": "delegate_task"}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claudeAgent"}}]}, "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"}}], "callsMustNot": [{"tool": "delegate_task", "args": {"target.providerInstanceId": "claude-2"}}]}, "source": {"pr": "https://github.com/pingdotgg/t3code/pull/32103"}}
+{"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"}}
diff --git a/evals/mcp-tool-use/run.test.ts b/evals/mcp-tool-use/run.test.ts
index c8b64d1a3563f..853a40906c4a9 100644
--- a/evals/mcp-tool-use/run.test.ts
+++ b/evals/mcp-tool-use/run.test.ts
@@ -624,6 +624,20 @@ it("replays earlier capability results with the model under test as the caller",
}
});
+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 3805f46a70e66..be37f39c0c8d7 100644
--- a/evals/mcp-tool-use/run.ts
+++ b/evals/mcp-tool-use/run.ts
@@ -1030,11 +1030,13 @@ export const buildMessages = (sample: Sample) => {
? testCase.prompt
: projectComposerContextForProvider({ text: testCase.prompt, ...testCase.promptContext });
const providerSwitch = testCase.setup.providerSwitch;
- const inlineContext =
- sideChatContext ??
- (providerSwitch && providerSwitchNote(providerSwitch.from, providerSwitch.to));
- const prompt =
- inlineContext === undefined ? userText : withInlineContext(inlineContext, userText);
+ 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));
From ceee7c0f81472d604ba631f25b78fbbb65688051 Mon Sep 17 00:00:00 2001
From: Josh Gimenes
Date: Wed, 11 Nov 2026 13:17:26 -0700
Subject: [PATCH 6/9] test(evals): report the case's inherited account for
untargeted delegates
Co-Authored-By: Claude Opus 5.5
---
evals/mcp-tool-use/run.ts | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/evals/mcp-tool-use/run.ts b/evals/mcp-tool-use/run.ts
index be37f39c0c8d7..9b460f964662a 100644
--- a/evals/mcp-tool-use/run.ts
+++ b/evals/mcp-tool-use/run.ts
@@ -1140,8 +1140,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,
};
}),
@@ -1162,8 +1161,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,
@@ -1301,6 +1299,16 @@ 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 = ({ base, testCase }: Sample) => {
+ const mocked = 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;
From 1c96d34ed806e712da127dbb2c6961d9c44de86b Mon Sep 17 00:00:00 2001
From: maria-rcks
Date: Sun, 15 Nov 2026 02:21:47 +0000
Subject: [PATCH 7/9] fix(server): retain account switch notes after resumed
commands
---
.../ProviderTurnStartService.test.ts | 78 +++++++++++++++++++
.../ProviderTurnStartService.ts | 6 +-
evals/mcp-tool-use/run.test.ts | 22 ++++++
evals/mcp-tool-use/run.ts | 5 +-
4 files changed, 107 insertions(+), 4 deletions(-)
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
index 7b0759ae99b0b..9339a97cc7dce 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
@@ -2307,6 +2307,84 @@ effectIt.effect.each([
}),
);
+effectIt.effect.each(["/compact", "/goal finish"])(
+ "keeps the switch note pending after a resumed %s",
+ (command) =>
+ 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: "" },
+ ...projection.messages,
+ ],
+ attempts: [
+ { ...previousAttempt, providerInstanceId: harness.oldInstanceId },
+ {
+ ...previousAttempt,
+ id: resumedAttemptId,
+ 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).toContain(
+ "switched by the user from",
+ );
+ }),
+);
+
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 786aca8e1f485..f19c26f7fa1e7 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -886,7 +886,9 @@ export const layer: Layer.Layer<
(source) =>
source.ordinal <= run.ordinal &&
source.status !== "rolled_back" &&
- !commandMessageIds.has(source.userMessageId),
+ !commandMessageIds.has(
+ (unsentManualContinuationSource(source, projection) ?? source).userMessageId,
+ ),
)
.flatMap((source) =>
projection.attempts.filter((candidate) => candidate.runId === source.id),
@@ -900,7 +902,7 @@ export const layer: Layer.Layer<
);
const named = (id: ProviderInstanceId) => ({ id, displayName: instances?.[id]?.displayName });
const switchNote =
- commandMessageIds.has(message.id) ||
+ commandMessageIds.has(providerMessage.id) ||
previousAttempt === undefined ||
previousAttempt.providerInstanceId === run.providerInstanceId
? ""
diff --git a/evals/mcp-tool-use/run.test.ts b/evals/mcp-tool-use/run.test.ts
index c515a7bd52f64..1f6d0a9bd6514 100644
--- a/evals/mcp-tool-use/run.test.ts
+++ b/evals/mcp-tool-use/run.test.ts
@@ -751,6 +751,28 @@ it("replays earlier capability results with the model under test as the caller",
}
});
+it.each(["claude-opus-5-5", "gpt-6.1-sol"])(
+ "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 = {
diff --git a/evals/mcp-tool-use/run.ts b/evals/mcp-tool-use/run.ts
index f656543528c1d..4ba3663c0638b 100644
--- a/evals/mcp-tool-use/run.ts
+++ b/evals/mcp-tool-use/run.ts
@@ -1381,8 +1381,9 @@ const unsavedSecretNote = (Contracts as Record).SECRET_REQUEST_
| undefined;
/** The account a call without a target inherits: the case's capabilities mock, else the harness's. */
-const inheritedInstance = ({ base, testCase }: Sample) => {
- const mocked = testCase.setup.mocks?.orchestrator_capabilities;
+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
From ebe106bd23d4e44defbecb6c6528aa9179539a48 Mon Sep 17 00:00:00 2001
From: maria-rcks
Date: Sun, 15 Nov 2026 02:30:58 +0000
Subject: [PATCH 8/9] fix(server): keep steering restarts in account switch
history
---
.../ProviderTurnStartService.test.ts | 23 +++++++++++++------
.../ProviderTurnStartService.ts | 20 ++++++++--------
2 files changed, 27 insertions(+), 16 deletions(-)
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
index 9339a97cc7dce..98a7df66e2d7f 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts
@@ -2307,9 +2307,13 @@ effectIt.effect.each([
}),
);
-effectIt.effect.each(["/compact", "/goal finish"])(
- "keeps the switch note pending after a resumed %s",
- (command) =>
+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",
@@ -2356,7 +2360,11 @@ effectIt.effect.each(["/compact", "/goal finish"])(
messages: [
{ ...projection.messages[0]!, id: earlierId, text: "Earlier prompt" },
{ ...projection.messages[0]!, id: commandId, text: command },
- { ...projection.messages[0]!, id: resumedMessageId, text: "" },
+ {
+ ...projection.messages[0]!,
+ id: resumedMessageId,
+ text: steered ? "Steered prompt" : "",
+ },
...projection.messages,
],
attempts: [
@@ -2364,6 +2372,7 @@ effectIt.effect.each(["/compact", "/goal finish"])(
{
...previousAttempt,
id: resumedAttemptId,
+ reason: steered ? "steering_restart" : "initial",
runId: resumedId,
providerInstanceId: harness.newInstanceId,
},
@@ -2379,9 +2388,9 @@ effectIt.effect.each(["/compact", "/goal finish"])(
],
});
yield* harness.start;
- expect(harness.startTurn.mock.calls[0]?.[0].message.text).toContain(
- "switched by the user from",
- );
+ expect(
+ harness.startTurn.mock.calls[0]?.[0].message.text.includes("switched by the user from"),
+ ).toBe(!steered);
}),
);
diff --git a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
index f19c26f7fa1e7..8b711aa59bea4 100644
--- a/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
+++ b/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
@@ -882,16 +882,18 @@ export const layer: Layer.Layer<
// 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" &&
- !commandMessageIds.has(
- (unsentManualContinuationSource(source, projection) ?? source).userMessageId,
- ),
- )
+ .filter((source) => source.ordinal <= run.ordinal && source.status !== "rolled_back")
.flatMap((source) =>
- projection.attempts.filter((candidate) => candidate.runId === source.id),
+ 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),
From fb31d6361a4760247ca1e03d5677acfb6312d89f Mon Sep 17 00:00:00 2001
From: maria-rcks
Date: Sun, 15 Nov 2026 02:39:00 +0000
Subject: [PATCH 9/9] fix(evals): preserve model literal types in inheritance
cases
---
evals/mcp-tool-use/run.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/evals/mcp-tool-use/run.test.ts b/evals/mcp-tool-use/run.test.ts
index 1f6d0a9bd6514..090b381dea348 100644
--- a/evals/mcp-tool-use/run.test.ts
+++ b/evals/mcp-tool-use/run.test.ts
@@ -751,7 +751,7 @@ it("replays earlier capability results with the model under test as the caller",
}
});
-it.each(["claude-opus-5-5", "gpt-6.1-sol"])(
+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 });