diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index 0309a51715d7..d73ae8e8640c 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -562,6 +562,16 @@ function taskPrompt(input: OrchestratorMcpDelegateTaskInput): string { : `Act as the ${input.role} sub-agent for this task.\n\n${input.task}`; } +function threadSettlement( + thread: Pick, +): Pick { + const settled = thread.settledOverride === "settled"; + return { + settled, + settledAt: settled && thread.settledAt !== null ? DateTime.formatIso(thread.settledAt) : null, + }; +} + function listItemFromShell(shell: OrchestrationV2ThreadShell): OrchestratorMcpThreadListItem { return { threadId: shell.id, @@ -575,6 +585,7 @@ function listItemFromShell(shell: OrchestrationV2ThreadShell): OrchestratorMcpTh runtimeMode: shell.runtimeMode, interactionMode: shell.interactionMode, linkedPullRequest: shell.linkedPullRequest ?? null, + ...threadSettlement(shell), parentThreadId: shell.lineage.parentThreadId, relationshipToParent: shell.lineage.relationshipToParent, itemCount: shell.visibleItemCount, @@ -621,6 +632,7 @@ function threadDetail( (request) => request.status === "pending", ).length, archived: projection.thread.archivedAt !== null, + ...threadSettlement(projection.thread), createdAt: DateTime.formatIso(projection.thread.createdAt), updatedAt: DateTime.formatIso(projection.thread.updatedAt), }; @@ -1721,6 +1733,10 @@ const make = Effect.gen(function* () { (thread) => statuses === null || statuses.has(thread.activityRunStatus ?? thread.status), ) + .filter( + (thread) => + input.settled === undefined || threadSettlement(thread).settled === input.settled, + ) .filter( (thread) => titleContains === undefined || diff --git a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts index 3e67bba6e8b6..19a59d490573 100644 --- a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts @@ -1960,6 +1960,50 @@ describe("orchestrator MCP toolkit", () => { ).toMatchObject({ title: "Metadata-managed thread", linkedPullRequest: linked.linkedPullRequest, + settled: false, + settledAt: null, + }); + expect(metadataRead.thread).toMatchObject({ settled: false, settledAt: null }); + + yield* orchestrator.dispatch({ + type: "thread.settle", + commandId: CommandId.make("command:mcp-empty:settle"), + threadId: emptyThread.threadId, + settledAt: DateTime.makeUnsafe("2026-01-01T00:00:00.000Z"), + }); + const settledReadCall = yield* invoke("t3_thread_read", { + threadId: emptyThread.threadId, + }); + const settledRead = yield* decodeThreadReadResult( + settledReadCall.structuredContent, + ).pipe(Effect.orDie); + expect(settledRead.thread).toMatchObject({ + settled: true, + settledAt: "2026-01-01T00:00:00.000Z", + }); + const settledListCall = yield* invoke("t3_thread_list", { settled: true, limit: 100 }); + const settledList = yield* decodeThreadListResult( + settledListCall.structuredContent, + ).pipe(Effect.orDie); + expect(settledList.threads.map((thread) => thread.threadId)).toEqual([ + emptyThread.threadId, + ]); + expect(settledList.threads[0]).toMatchObject({ + settled: true, + settledAt: "2026-01-01T00:00:00.000Z", + }); + const activeListCall = yield* invoke("t3_thread_list", { settled: false, limit: 100 }); + const activeList = yield* decodeThreadListResult(activeListCall.structuredContent).pipe( + Effect.orDie, + ); + expect( + activeList.threads.some((thread) => thread.threadId === emptyThread.threadId), + ).toBe(false); + yield* orchestrator.dispatch({ + type: "thread.unsettle", + commandId: CommandId.make("command:mcp-empty:unsettle"), + threadId: emptyThread.threadId, + reason: "user", }); const unlinkedCall = yield* invoke("t3_thread_update", { diff --git a/apps/server/src/mcp/toolkits/orchestrator/tools.ts b/apps/server/src/mcp/toolkits/orchestrator/tools.ts index fed4daed8833..2f169c611de3 100644 --- a/apps/server/src/mcp/toolkits/orchestrator/tools.ts +++ b/apps/server/src/mcp/toolkits/orchestrator/tools.ts @@ -156,7 +156,7 @@ export const CreateThreadsTool = Tool.make("create_threads", { const ThreadListTool = Tool.make("t3_thread_list", { description: - "List T3 threads in the calling thread's project, newest first. Filter by durable run status or title and paginate with the returned cursor. Threads from other projects are never exposed.", + "List T3 threads in the calling thread's project, newest first. Filter by durable run status, title, or settled state (settled=true lists threads the user or auto-settlement moved out of the active list) and paginate with the returned cursor. Threads from other projects are never exposed.", parameters: OrchestratorMcpThreadListInput, success: OrchestratorMcpThreadListResult, failure: OrchestratorMcpFailure, diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts index 19480711491a..985f03cc951b 100644 --- a/packages/contracts/src/orchestratorMcp.ts +++ b/packages/contracts/src/orchestratorMcp.ts @@ -284,6 +284,7 @@ export const OrchestratorMcpThreadListInput = Schema.Struct({ Schema.Array(OrchestratorMcpThreadStatus).check(Schema.isMaxLength(10)), ), titleContains: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(256))), + settled: Schema.optional(Schema.Boolean), includeSubagents: Schema.optional(Schema.Boolean), cursor: Schema.optional(NonNegativeInt), limit: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(100))), @@ -302,6 +303,8 @@ export const OrchestratorMcpThreadListItem = Schema.Struct({ runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, linkedPullRequest: Schema.NullOr(ThreadLinkedPullRequest), + settled: Schema.Boolean, + settledAt: Schema.NullOr(IsoDateTime), parentThreadId: Schema.NullOr(ThreadId), relationshipToParent: Schema.NullOr(Schema.Literals(["fork", "subagent"])), itemCount: NonNegativeInt, @@ -354,6 +357,8 @@ export const OrchestratorMcpThreadDetail = Schema.Struct({ itemCount: NonNegativeInt, pendingRequestCount: NonNegativeInt, archived: Schema.Boolean, + settled: Schema.Boolean, + settledAt: Schema.NullOr(IsoDateTime), createdAt: IsoDateTime, updatedAt: IsoDateTime, });