diff --git a/apps/server/src/mcp/toolkits/core.test.ts b/apps/server/src/mcp/toolkits/core.test.ts index a978dbc4d1ff9..d70afdf040865 100644 --- a/apps/server/src/mcp/toolkits/core.test.ts +++ b/apps/server/src/mcp/toolkits/core.test.ts @@ -10,6 +10,7 @@ import { NodeId, OrchestratorMcpThreadReadResult, ProviderDriverKind, + ProjectId, ProviderInstanceId, RunId, RuntimeRequestId, @@ -35,6 +36,10 @@ import { } from "../../orchestration-v2/Orchestrator.ts"; import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; +import * as CommandReceiptStore from "../../orchestration-v2/CommandReceiptStore.ts"; +import * as Orchestrator from "../../orchestration-v2/Orchestrator.ts"; +import * as ProviderReplayHarness from "../../orchestration-v2/testkit/ProviderReplayHarness.ts"; +import * as SqlitePersistence from "../../persistence/Sqlite.ts"; import * as ServerConfig from "../../config.ts"; import * as ProviderAdapterRegistry from "../../orchestration-v2/ProviderAdapterRegistry.ts"; import * as ThreadLaunchService from "../../orchestration-v2/ThreadLaunchService.ts"; @@ -579,6 +584,92 @@ it.effect("a read-only client reads threads and is refused every write before it ); const dispatched: Array = []; +const layerPersistedThreads = (() => { + const layerDatabase = SqlitePersistence.layerMemory; + const layerOrchestrator = ProviderReplayHarness.layerWithRegistry( + { name: "mcp-core-auto-settle" }, + ProviderAdapterRegistry.layerFromAdapters([]), + { databaseLayer: layerDatabase, runEffectWorker: false }, + ); + return Layer.merge( + layerOrchestrator, + ThreadManagement.layer.pipe( + Layer.provide(layerOrchestrator), + Layer.provide(ServerSettings.layerTest()), + Layer.provide(CommandReceiptStore.layer.pipe(Layer.provide(layerDatabase))), + ), + ); +})(); + +it.effect("an agent turns auto-settle off and back on for only the thread it targets", () => + Effect.gen(function* () { + const server = yield* McpServer.McpServer; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const threads = yield* ThreadManagement.ThreadManagementService; + const targetId = ThreadId.make("auto-settle-target"); + const siblingId = ThreadId.make("auto-settle-sibling"); + for (const id of [targetId, siblingId]) { + yield* orchestrator.dispatch({ + type: "thread.create", + commandId: CommandId.make(`create-${id}`), + threadId: id, + projectId: ProjectId.make("project:mcp-core"), + title: id, + modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + createdBy: "user", + creationSource: "web", + }); + } + yield* orchestrator.dispatch({ + type: "thread.unsettle", + commandId: CommandId.make("manual-auto-settle-override"), + threadId: targetId, + reason: "user", + }); + const manualOverride = (yield* threads.getThreadShell(targetId))?.settledOverride; + expect(manualOverride).toBe("active"); + const organize = ( + action: string, + invocation: McpInvocationContext.McpInvocationScope = clientScope("full-access"), + ) => + server + .callTool({ name: "t3_thread_organize", arguments: { action, threadId: targetId } }) + .pipe( + Effect.provideService(McpInvocationContext.McpInvocationContext, invocation), + Effect.provideService(McpSchema.McpServerClient, client), + ); + const autoSettleDisabled = (id: ThreadId) => + threads.getThreadShell(id).pipe(Effect.map((shell) => shell?.autoSettleDisabledAt != null)); + + const disabled = yield* organize("disable_auto_settle"); + expect(disabled.isError).toBe(false); + expect(yield* autoSettleDisabled(targetId)).toBe(true); + expect(yield* autoSettleDisabled(siblingId)).toBe(false); + expect((yield* threads.getThreadShell(targetId))?.settledOverride).toEqual(manualOverride); + + const readOnly = yield* organize("enable_auto_settle", clientScope("read-only")); + expect(declaredFailure(readOnly)).toMatchObject({ code: "capability_denied" }); + expect(yield* autoSettleDisabled(targetId)).toBe(true); + + const enabled = yield* organize("enable_auto_settle"); + expect(enabled.isError).toBe(false); + expect(yield* autoSettleDisabled(targetId)).toBe(false); + expect((yield* threads.getThreadShell(targetId))?.settledOverride).toEqual(manualOverride); + }).pipe( + Effect.provide( + layerThreadToolkit.pipe( + Layer.provideMerge(McpServer.McpServer.layer), + Layer.provide(NodeCrypto.layer), + Layer.provideMerge(layerPersistedThreads), + ), + ), + ), +); + it.effect("refuses act-as-caller tools to a client caller", () => Effect.gen(function* () { const server = yield* McpServer.McpServer; diff --git a/apps/server/src/mcp/toolkits/thread/handlers.ts b/apps/server/src/mcp/toolkits/thread/handlers.ts index 8ad9afa435193..a6973bfeb7000 100644 --- a/apps/server/src/mcp/toolkits/thread/handlers.ts +++ b/apps/server/src/mcp/toolkits/thread/handlers.ts @@ -351,6 +351,14 @@ export const layer = McpToolAccess.toLayer(ThreadToolkit, { case "mark_unread": command = { ...common, type: "thread.mark-unread" }; break; + case "disable_auto_settle": + case "enable_auto_settle": + command = { + ...common, + type: "thread.auto-settle.set", + enabled: input.action === "enable_auto_settle", + }; + break; default: command = { ...common, type: `thread.${input.action}` }; } diff --git a/apps/server/src/mcp/toolkits/thread/tools.ts b/apps/server/src/mcp/toolkits/thread/tools.ts index 4b566c44a69d1..9146a80183e7e 100644 --- a/apps/server/src/mcp/toolkits/thread/tools.ts +++ b/apps/server/src/mcp/toolkits/thread/tools.ts @@ -31,7 +31,7 @@ import * as McpInvocationContext from "../../McpInvocationContext.ts"; const ThreadOrganizeTool = Tool.make("t3_thread_organize", { description: - "Pin, snooze, settle, archive, or mark a thread unread. Omit `threadId` for this thread. `snooze` needs `snoozedUntil`. Settling this thread applies at turn end. A failed turn or queued message leaves it active.", + "Pin, snooze, settle, archive, or mark a thread unread. Omit `threadId` for this thread. `snooze` needs `snoozedUntil`. Settling this thread applies at turn end. A failed turn or queued message leaves it active. `disable_auto_settle` stops the thread settling itself, such as after its PR merges, until `enable_auto_settle`.", parameters: Schema.Struct({ environmentId: OrchestratorMcpEnvironmentTarget, threadId: Schema.optional(ThreadId), @@ -45,6 +45,8 @@ const ThreadOrganizeTool = Tool.make("t3_thread_organize", { "archive", "unarchive", "mark_unread", + "disable_auto_settle", + "enable_auto_settle", ]), snoozedUntil: Schema.optional(IsoDateTime), }), diff --git a/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts b/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts index c36dfc365af60..de4125e2c4517 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts @@ -62,6 +62,61 @@ const layerTest = Layer.mergeAll( ), ); +it.effect("a same-millisecond auto-settle disable rejects an in-flight settlement", () => + Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const projections = yield* ProjectionStore.ProjectionStoreV2; + const threadId = ThreadId.make("thread:auto-settle-disable-race"); + yield* orchestrator.dispatch({ + type: "thread.create", + commandId: CommandId.make("create-auto-settle-disable-race"), + threadId, + projectId: ProjectId.make("project:auto-settle-disable-race"), + title: "Follow-up work", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + createdBy: "user", + creationSource: "web", + }); + const snapshotAt = (yield* projections.getThread(threadId)).updatedAt; + yield* orchestrator.dispatch({ + type: "thread.auto-settle.set", + commandId: CommandId.make("disable-auto-settle-race"), + threadId, + enabled: false, + }); + const disabled = yield* projections.getThread(threadId); + assert.equal(DateTime.toEpochMillis(disabled.updatedAt), DateTime.toEpochMillis(snapshotAt)); + const settlement = yield* Effect.exit( + orchestrator.dispatch({ + type: "thread.auto-settle", + commandId: CommandId.make("in-flight-auto-settle-race"), + threadId, + snapshotAt, + }), + ); + assert.equal(settlement._tag, "Failure"); + assert.equal((yield* projections.getThread(threadId)).settledOverride, null); + + yield* orchestrator.dispatch({ + type: "thread.auto-settle.set", + commandId: CommandId.make("enable-auto-settle-race"), + threadId, + enabled: true, + }); + yield* orchestrator.dispatch({ + type: "thread.auto-settle", + commandId: CommandId.make("enabled-auto-settle-race"), + threadId, + snapshotAt, + }); + assert.equal((yield* projections.getThread(threadId)).settledOverride, "settled"); + }).pipe(Effect.provide(layerTest)), +); + it.effect( "records approval feedback and targets only its runtime request without dispatching a parent turn", () => diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 91acb881637c1..1ae58477d7b8b 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -12402,6 +12402,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio ); if ( thread.settledOverride !== null || + thread.autoSettleDisabledAt != null || DateTime.toEpochMillis(thread.updatedAt) > DateTime.toEpochMillis(command.snapshotAt) ) { return yield* new OrchestratorDispatchError({ diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index d5496ea8cfc57..894df0401f6d8 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { "claudeSystemAppendBytes": 5135, "codexAdditionalContextBytes": 6865, - "mcpToolListJsonBytes": 45907 + "mcpToolListJsonBytes": 46063 } diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index ce16b9df0374b..476ac42403cb9 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -184,7 +184,8 @@ finished background work or a pull request watch starts on its own does not. To keep one thread out of the settled shelf no matter how long it sits idle, open its menu, choose **Auto-settle behavior**, and pick **Disabled**. The current option is checked. Pick **Enabled** to return to the usual rules. Manual settle, snooze, and archive still work while it -is disabled. +is disabled. An agent can turn this off and on too, for example to keep a thread whose pull request +merged open while it finishes follow-up work. Change these rules in **Settings → Threads**. They continue to run when your apps are closed. On web and desktop, choose an environment at the diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson index dc79eb774df7e..e7996848f2c46 100644 --- a/evals/mcp-tool-use/cases/orchestration.ndjson +++ b/evals/mcp-tool-use/cases/orchestration.ndjson @@ -34,3 +34,5 @@ {"id": "two-children-same-model", "area": "orchestration", "kind": "bad", "pattern": "named-model rule read as a cap", "experience": "Asked for two Sol xhigh subagents, an Astra parent started one and refused the second, citing 'One child per named model' (#24967).", "rule": "Before delegate_task ... One child per named model. (intended: every named model gets a child; read as a cap of one child per model)", "privacy": "commit-safe", "setup": {"role": "top", "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-sonnet-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": "Claude", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "fastMode", "type": "boolean", "default": false}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-haiku-5-5", "label": "Claude Haiku 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "codex", "driverKind": "codex", "displayName": "Codex", "models": [{"id": "gpt-6.1-sol", "label": "GPT-6.1 Sol", "options": [{"id": "reasoningEffort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "serviceTier", "values": ["default", "priority"], "default": "default"}]}]}, {"providerInstanceId": "muse", "driverKind": "muse", "displayName": "Muse Code", "runtimeModes": ["approval-required", "full-access"], "models": [{"id": "muse-wonder-full", "label": "Muse Wonder Full", "options": [{"id": "reasoningEffort", "values": ["minimal", "low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "grok", "driverKind": "grok", "displayName": "Grok", "models": [{"id": "grok-4.7", "label": "Grok 4.7", "options": []}]}]}, "delegate_task": "default"}}, "prompt": "Launch two Sol xhigh subagents. Give each a trivial task that only replies OK.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "gpt-6.1-sol", "target.options.reasoningEffort": "xhigh"}}], "minCalls": {"delegate_task": 2}, "callsMustNot": [{"tool": "delegate_task", "args": {"target.model": "~muse"}}, {"tool": "delegate_task", "args": {"target.model": "~claude"}}]}, "notes": "From #24967. One child plus the parent doing the work fails, and so does a second model in place of Sol."} {"id": "worker-counts-per-model", "area": "orchestration", "kind": "bad", "pattern": "named-model rule read as a cap", "experience": "Asked for 20 Sol workers and 20 Luna workers, the agent started one worker per model (#25359).", "rule": "Before delegate_task ... One child per named model. (intended: every named model gets a child; read as a cap of one child per model)", "privacy": "commit-safe", "setup": {"role": "top", "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-sonnet-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": "Claude", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "fastMode", "type": "boolean", "default": false}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-haiku-5-5", "label": "Claude Haiku 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "codex", "driverKind": "codex", "displayName": "Codex", "models": [{"id": "gpt-6.1-sol", "label": "GPT-6.1 Sol", "options": [{"id": "reasoningEffort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "serviceTier", "values": ["default", "priority"], "default": "default"}]}]}, {"providerInstanceId": "muse", "driverKind": "muse", "displayName": "Muse Code", "runtimeModes": ["approval-required", "full-access"], "models": [{"id": "muse-wonder-full", "label": "Muse Wonder Full", "options": [{"id": "reasoningEffort", "values": ["minimal", "low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "grok", "driverKind": "grok", "displayName": "Grok", "models": [{"id": "grok-4.7", "label": "Grok 4.7", "options": []}]}]}, "delegate_task": "default"}, "world": {"files": {"tests/flaky/retry.test.ts": "it('retry is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/upload.test.ts": "it('upload is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/session.test.ts": "it('session is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/search.test.ts": "it('search is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/billing.test.ts": "it('billing is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/webhook.test.ts": "it('webhook is flaky', async () => { /* fails 1 in 5 runs */ });\n"}}}, "prompt": "Six tests in tests/flaky/ fail now and then. Start 3 Muse Wonder Full workers and 3 Grok workers, one test file each, to find and fix the flake.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "muse-wonder-full"}}, {"tool": "delegate_task", "args": {"target.model": "grok-4.7"}}], "minCalls": {"delegate_task": 6}}, "notes": "Scaled down from the real 20 Sol + 20 Luna ask. Muse and Grok need delegate_task from every parent, so a native Sol helper cannot pass or fail it."} {"id": "linked-thread-chip-reads-there", "area": "orchestration", "kind": "guard", "pattern": "thread chip from a linked machine read on this one", "experience": "A thread chip from a linked machine names its environmentId; an agent that reads it without passing that id reads this machine, where the thread does not exist.", "rule": "The thread chip names its environmentId; t3_thread_read takes it.", "privacy": "commit-safe", "setup": {"role": "top", "linkedEnvironments": true, "mocks": {"t3_environment_links": {"environmentId": "environment-laptop", "links": [{"environmentId": "environment-vps", "label": "Hetzner VPS", "status": "reachable", "access": "auto-accept-edits", "expiresAt": "2026-12-01T00:00:00.000Z", "lastError": null}], "notListed": "Not listed: not linked yet. Call t3_environment_link with a hint from the user's words."}, "t3_thread_read": {"thread": {"threadId": "thread-deploy-api", "title": "Deploy the API", "status": "idle"}, "items": [{"position": 1, "createdBy": "user", "text": "Deploy the API to the VPS and check the health endpoint."}, {"position": 2, "createdBy": "assistant", "text": "Deployed. /health returns 200, but /health/db times out after 5s: the API can't reach Postgres on 10.0.0.5:5432."}], "nextPosition": 3, "hasMore": false}}}, "prompt": "Pick up where [Deploy the API](t3-context://v1/thread/thread_deploy-api) left off. What is still failing?", "promptContext": {"records": [{"version": 1, "kind": "thread", "contextId": "thread_deploy-api", "label": "Deploy the API", "environmentId": "environment-vps", "threadId": "thread-deploy-api", "title": "Deploy the API"}]}, "expect": {"calls": [{"tool": "t3_thread_read", "args": {"threadId": "thread-deploy-api", "environmentId": "environment-vps"}}], "notCalls": ["t3_environment_link"]}, "source": {"request": "akj on #17130: attach a linked machine's thread without dragging"}} +{"id": "merged-thread-followup-disables-auto-settle", "area": "orchestration", "kind": "bad", "pattern": "t3_thread_organize unsettle used to hold a thread open for post-merge work", "experience": "After a thread's PR merged, an orchestrating agent kept waking it for production checks. Each run ended with the thread auto-settled again, and unsettle did not hold, so it settled 5 times in a day.", "rule": "t3_thread_organize: '`disable_auto_settle` stops the thread settling itself, such as after its PR merges, until `enable_auto_settle`.'", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Start a new thread to fix the CSV export timeout on large workspaces and have it open a PR."}, {"role": "assistant", "text": "Started [Fix CSV export timeout](t3-thread://v1/55555555-5555-4555-8555-555555555555). It opened https://github.com/acme/app/pull/418.", "toolCalls": [{"tool": "t3_thread_launch", "input": {"title": "Fix CSV export timeout", "message": "The CSV export times out on workspaces with more than 50k rows. Fix it and open a PR."}, "result": {"threadId": "55555555-5555-4555-8555-555555555555", "projectId": "22222222-2222-4222-8222-222222222222", "status": "starting", "link": "[Fix CSV export timeout](t3-thread://v1/55555555-5555-4555-8555-555555555555)"}}]}], "mocks": {"t3_thread_send": {"threadId": "55555555-5555-4555-8555-555555555555", "messageId": "msg-9", "runId": "run-9", "queued": false}, "t3_thread_organize": {"sequence": 57}, "t3_thread_read": {"thread": {"threadId": "55555555-5555-4555-8555-555555555555", "title": "Fix CSV export timeout", "link": "[Fix CSV export timeout](t3-thread://v1/55555555-5555-4555-8555-555555555555)", "status": "idle"}, "items": [{"position": 30, "createdBy": "assistant", "text": "PR https://github.com/acme/app/pull/418 merged. The export now streams rows in pages of 5,000."}], "nextPosition": 31, "hasMore": false}}, "shellMocks": [{"match": ".*", "output": ""}]}, "prompt": "418 merged. That thread still has to check the export in production after tonight's deploy, but it keeps settling itself every time one of its runs ends. Tell it to do the production check, and keep it from settling until that's done.", "expect": {"calls": [{"tool": "t3_thread_organize", "args": {"threadId": "55555555-5555-4555-8555-555555555555", "action": "disable_auto_settle"}}, {"tool": "t3_thread_send", "args": {"threadId": "55555555-5555-4555-8555-555555555555"}}]}, "source": {"issue": "https://github.com/pingdotgg/t3code/issues/26097"}, "notes": "Before the fix, t3_thread_organize had no per-thread auto-settle switch, so the agent could only unsettle, which the next run's activity overrides."} +{"id": "control-merged-thread-followup-leaves-auto-settle", "area": "orchestration", "kind": "control", "pattern": "plain follow-up to a merged thread", "experience": "Ordinary follow-up to a thread whose PR merged. Nothing asks to keep it open, so the agent should only send the message.", "rule": "Keeps disable_auto_settle from becoming a reflex on every post-merge message.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Start a new thread to fix the CSV export timeout on large workspaces and have it open a PR."}, {"role": "assistant", "text": "Started [Fix CSV export timeout](t3-thread://v1/55555555-5555-4555-8555-555555555555). It opened https://github.com/acme/app/pull/418.", "toolCalls": [{"tool": "t3_thread_launch", "input": {"title": "Fix CSV export timeout", "message": "The CSV export times out on workspaces with more than 50k rows. Fix it and open a PR."}, "result": {"threadId": "55555555-5555-4555-8555-555555555555", "projectId": "22222222-2222-4222-8222-222222222222", "status": "starting", "link": "[Fix CSV export timeout](t3-thread://v1/55555555-5555-4555-8555-555555555555)"}}]}], "mocks": {"t3_thread_send": {"threadId": "55555555-5555-4555-8555-555555555555", "messageId": "msg-9", "runId": "run-9", "queued": false}, "t3_thread_organize": {"sequence": 57}, "t3_thread_read": {"thread": {"threadId": "55555555-5555-4555-8555-555555555555", "title": "Fix CSV export timeout", "link": "[Fix CSV export timeout](t3-thread://v1/55555555-5555-4555-8555-555555555555)", "status": "idle"}, "items": [{"position": 30, "createdBy": "assistant", "text": "PR https://github.com/acme/app/pull/418 merged. The export now streams rows in pages of 5,000."}], "nextPosition": 31, "hasMore": false}}, "shellMocks": [{"match": ".*", "output": ""}]}, "prompt": "418 merged. Ask that thread to delete its feature branch.", "expect": {"calls": [{"tool": "t3_thread_send", "args": {"threadId": "55555555-5555-4555-8555-555555555555"}}], "callsMustNot": [{"tool": "t3_thread_organize", "args": {"action": "disable_auto_settle"}}]}, "source": {"issue": "https://github.com/pingdotgg/t3code/issues/26097"}}