From 43b5cf1980a75d457afe5c6dd3ddb5d282775d8e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 01:10:45 -0700 Subject: [PATCH] Revert "feat(server): thread tools return a ready t3-thread link (#24784)" This reverts commit 003b64dfa536e545079565d36ae0c2a5dba73b34. --- .../src/mcp/OrchestratorMcpService.test.ts | 18 +++++++++--------- apps/server/src/mcp/OrchestratorMcpService.ts | 6 ------ .../src/mcp/toolkits/orchestrator/tools.ts | 4 ++-- .../src/mcp/toolkits/project/handlers.test.ts | 2 -- .../src/mcp/toolkits/project/handlers.ts | 2 -- apps/server/src/mcp/toolkits/project/tools.ts | 3 +-- .../server/src/mcp/toolkits/thread/handlers.ts | 9 +-------- apps/server/src/mcp/toolkits/thread/tools.ts | 1 - .../orchestrator-mcp-server.md | 3 +-- packages/contracts/src/orchestratorMcp.ts | 5 ----- packages/shared/src/threadLinks.test.ts | 8 -------- 11 files changed, 14 insertions(+), 47 deletions(-) diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index 6ce02cbb32e2..703efc84beab 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -90,7 +90,7 @@ describe("OrchestratorMcpService", () => { ], } as unknown as OrchestrationV2ThreadProjection; const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [ { id: childRunId, @@ -224,7 +224,7 @@ describe("OrchestratorMcpService", () => { ], } as unknown as OrchestrationV2ThreadProjection; const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [ { id: RunId.make("run:mcp-restart-child"), @@ -425,7 +425,7 @@ describe("OrchestratorMcpService", () => { ], } as unknown as OrchestrationV2ThreadProjection; const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [], contextTransfers: [], messages: [], @@ -509,7 +509,7 @@ describe("OrchestratorMcpService", () => { }) as unknown as OrchestrationV2ThreadProjection; // A finished run whose binding to the follow-up was lost. const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: childState === "no runs" ? [] @@ -612,7 +612,7 @@ describe("OrchestratorMcpService", () => { ], } as unknown as OrchestrationV2ThreadProjection; const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [{ id: childRunId, status: "running" }], contextTransfers: [], messages: [], @@ -692,7 +692,7 @@ describe("OrchestratorMcpService", () => { ], } as unknown as OrchestrationV2ThreadProjection; const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [{ id: childRunId, status: "running" }], contextTransfers: [], messages: [], @@ -778,7 +778,7 @@ describe("OrchestratorMcpService", () => { ], } as unknown as OrchestrationV2ThreadProjection; const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [{ id: childRunId, status: "running" }], contextTransfers: [], messages: [], @@ -873,7 +873,7 @@ describe("OrchestratorMcpService", () => { [ childThreadId, { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [{ id: RunId.make("run:mcp-cancel-grandchild-child"), status: "running" }], contextTransfers: [], messages: [], @@ -1101,7 +1101,7 @@ describe("OrchestratorMcpService provider resolution", () => { }) as unknown as OrchestrationV2ThreadProjection; const childProjection = { - thread: { id: childThreadId, title: "Child" }, + thread: { id: childThreadId }, runs: [], contextTransfers: [], messages: [], diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index 0df296db5bc1..24cbe633ca47 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -68,7 +68,6 @@ import { } from "@t3tools/contracts"; import { getProviderOptionCurrentValue } from "@t3tools/shared/model"; import { runRanAfter } from "@t3tools/shared/orchestrationV2ThreadError"; -import { formatThreadLink } from "@t3tools/shared/threadLinks"; import { normalizeThreadPullRequestKey, visibleThreadPullRequests, @@ -857,7 +856,6 @@ function listItemFromShell( return { threadId: shell.id, title: shell.title, - link: formatThreadLink(shell.id, shell.title), createdBy: shell.createdBy, creationSource: shell.creationSource, status: shell.activityRunStatus ?? shell.status, @@ -890,7 +888,6 @@ function threadDetail( threadId: projection.thread.id, projectId: projection.thread.projectId, title: projection.thread.title, - link: formatThreadLink(projection.thread.id, projection.thread.title), createdBy: projection.thread.createdBy, creationSource: projection.thread.creationSource, status: active?.status ?? latest?.status ?? "idle", @@ -1601,7 +1598,6 @@ const make = Effect.gen(function* () { const result = { taskId: task.id, childThreadId: task.childThreadId, - link: formatThreadLink(task.childThreadId, childControls.thread.title), childRunId: childRun?.id ?? null, childNodeId: task.id, status, @@ -2657,7 +2653,6 @@ const make = Effect.gen(function* () { ); return { threadId, - link: formatThreadLink(threadId, projection.thread.title), runId: run?.id ?? null, status: run?.status ?? "idle", title: projection.thread.title, @@ -2849,7 +2844,6 @@ const make = Effect.gen(function* () { ); return { threadId: input.threadId, - link: formatThreadLink(input.threadId, target.thread.title), messageId, runId: result.run.id, ...(result.run.delegatedTaskId === undefined ? {} : { taskId: result.run.delegatedTaskId }), diff --git a/apps/server/src/mcp/toolkits/orchestrator/tools.ts b/apps/server/src/mcp/toolkits/orchestrator/tools.ts index c193a0afb159..0aa0b430b7c7 100644 --- a/apps/server/src/mcp/toolkits/orchestrator/tools.ts +++ b/apps/server/src/mcp/toolkits/orchestrator/tools.ts @@ -214,7 +214,7 @@ const CreateThreadsTool = Tool.make("create_threads", { const ThreadListTool = Tool.make("t3_thread_list", { description: - "List T3 threads in a project, newest first. Omit projectId for the calling thread's project. Filter by durable run status, title, or settled state (settled=true lists threads the user or auto-settlement moved out of the active list), or snoozed state, and paginate with the returned cursor. A snoozed thread wakes early only when it asks for something or fails after snoozing; completed runs and background wakes keep it snoozed. pullRequests lists every pull request linked to each thread; linkedPullRequest is a legacy single link and can be null while pullRequests still has entries. Mention a thread with its `link`.", + "List T3 threads in a project, newest first. Omit projectId for the calling thread's project. Filter by durable run status, title, or settled state (settled=true lists threads the user or auto-settlement moved out of the active list), or snoozed state, and paginate with the returned cursor. A snoozed thread wakes early only when it asks for something or fails after snoozing; completed runs and background wakes keep it snoozed. pullRequests lists every pull request linked to each thread; linkedPullRequest is a legacy single link and can be null while pullRequests still has entries. To link a thread for the user, write `[title](t3-thread://v1/)` with the threadId exactly as returned, not URL-encoded; T3 Code shows the thread's current title.", parameters: OrchestratorMcpThreadListInput, success: OrchestratorMcpThreadListResult, failure: OrchestratorMcpFailure, @@ -228,7 +228,7 @@ const ThreadListTool = Tool.make("t3_thread_list", { const ThreadReadTool = Tool.make("t3_thread_read", { description: - "Read durable state and a paginated timeline from any T3 thread in this environment. The default messages view returns user messages, assistant messages, and proposed plans; activity returns all summarized timeline items. Reading an untruncated terminal assistant result from this parent thread's direct app-owned child acknowledges that child's automatic completion delivery. Continue with afterPosition=nextPosition. Recover long item text with itemId and textOffset=nextTextOffset until nextTextOffset is null; offsets count UTF-16 code units. The thread also reports its snooze state and, in pullRequests, every linked pull request; linkedPullRequest is a legacy single link and can be null while pullRequests still has entries. Mention a thread with its `link`.", + "Read durable state and a paginated timeline from any T3 thread in this environment. The default messages view returns user messages, assistant messages, and proposed plans; activity returns all summarized timeline items. Reading an untruncated terminal assistant result from this parent thread's direct app-owned child acknowledges that child's automatic completion delivery. Continue with afterPosition=nextPosition. Recover long item text with itemId and textOffset=nextTextOffset until nextTextOffset is null; offsets count UTF-16 code units. The thread also reports its snooze state and, in pullRequests, every linked pull request; linkedPullRequest is a legacy single link and can be null while pullRequests still has entries. To link a thread for the user, write `[title](t3-thread://v1/)` with the threadId exactly as returned, not URL-encoded; T3 Code shows the thread's current title.", parameters: OrchestratorMcpThreadReadInput, success: OrchestratorMcpThreadReadResult, failure: OrchestratorMcpFailure, diff --git a/apps/server/src/mcp/toolkits/project/handlers.test.ts b/apps/server/src/mcp/toolkits/project/handlers.test.ts index ccc4443d0b7c..4c22393bf476 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.test.ts @@ -81,7 +81,6 @@ const clientLaunchHarness = (input: { thread: { id: launch.threadId, projectId: launch.projectId, - title: launch.title, modelSelection: launch.modelSelection, }, runs: [], @@ -160,7 +159,6 @@ it.effect( expect(belowCeiling.result).toMatchObject({ projectId: belowCeiling.projectId, modelSelection: belowCeiling.modelSelection, - link: `[Fix](t3-thread://v1/${belowCeiling.launched[0]?.threadId})`, }); expect(belowCeiling.launched[0]?.runtimeMode).toBe("approval-required"); expect(belowCeiling.launched[0]?.initialMessage?.senderThreadId).toBeUndefined(); diff --git a/apps/server/src/mcp/toolkits/project/handlers.ts b/apps/server/src/mcp/toolkits/project/handlers.ts index aaf37468f8cd..9522c8d28fec 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.ts @@ -11,7 +11,6 @@ import * as Repositories from "../../../sourceControl/SourceControlRepositorySer import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; import * as ServerSettings from "../../../serverSettings.ts"; import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; -import { formatThreadLink } from "@t3tools/shared/threadLinks"; import { resolveRuntimeMode } from "../../OrchestratorMcpService.ts"; import * as McpToolAccess from "../../McpToolAccess.ts"; import { newCommandId, readCaller, resolveProjectId, unavailable } from "../../threadAccess.ts"; @@ -198,7 +197,6 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { const run = result.projection.runs.find((run) => run.userMessageId === messageId); return { threadId: thread.id, - link: formatThreadLink(thread.id, thread.title), projectId: thread.projectId, modelSelection: thread.modelSelection, runId: run?.id ?? null, diff --git a/apps/server/src/mcp/toolkits/project/tools.ts b/apps/server/src/mcp/toolkits/project/tools.ts index fbe73215113c..2114565d8201 100644 --- a/apps/server/src/mcp/toolkits/project/tools.ts +++ b/apps/server/src/mcp/toolkits/project/tools.ts @@ -104,7 +104,7 @@ const ProjectCloneTool = Tool.make("t3_project_clone", { const ThreadLaunchTool = Tool.make("t3_thread_launch", { ...shared, description: - 'Create an ordinary TOP-LEVEL thread with an explicit workspace binding before its agent starts. Use this when the user requests independent work, a new thread, or a PR stack in its own worktree; use delegate_task for child subagents. Set workspaceStrategy to {type:"worktree",baseRef:"parent-branch",branch:"new-branch",startFromOrigin:false} for a new worktree based on local commits, or {type:"existing_worktree",worktreePath:"/absolute/path",branch:"existing-branch"} to use an existing checkout. To fetch and start from the base branch\'s remote, set startFromOrigin:true. Omitted workspaceStrategy means the project root, NOT the caller\'s worktree. Omit projectId/modelSelection/modes to inherit those settings from the calling thread; a caller outside a T3 thread must pass projectId and gets the project\'s default model. Set scratch:true instead of projectId for a thread without a project: it runs in a fresh folder of its own, outside any repository. Put the task in message. Do not ask the agent to create its own worktree via shell: that does not update the thread binding. Each call creates a new launch with no retry key; retain threadId and use t3_thread_read/t3_thread_wait to follow preparation. Mention a thread with its `link`. After errors or lost responses, inspect t3_thread_list before retrying. Attachments must be pending uploads. The new thread may not run with broader runtime or interaction modes than the caller: the calling T3 thread\'s own modes, or the permission mode an outside agent was approved with.', + 'Create an ordinary TOP-LEVEL thread with an explicit workspace binding before its agent starts. Use this when the user requests independent work, a new thread, or a PR stack in its own worktree; use delegate_task for child subagents. Set workspaceStrategy to {type:"worktree",baseRef:"parent-branch",branch:"new-branch",startFromOrigin:false} for a new worktree based on local commits, or {type:"existing_worktree",worktreePath:"/absolute/path",branch:"existing-branch"} to use an existing checkout. To fetch and start from the base branch\'s remote, set startFromOrigin:true. Omitted workspaceStrategy means the project root, NOT the caller\'s worktree. Omit projectId/modelSelection/modes to inherit those settings from the calling thread; a caller outside a T3 thread must pass projectId and gets the project\'s default model. Set scratch:true instead of projectId for a thread without a project: it runs in a fresh folder of its own, outside any repository. Put the task in message. Do not ask the agent to create its own worktree via shell: that does not update the thread binding. Each call creates a new launch with no retry key; retain threadId and use t3_thread_read/t3_thread_wait to follow preparation. To link a thread for the user, write `[title](t3-thread://v1/)` with the threadId exactly as returned, not URL-encoded; T3 Code shows the thread\'s current title. After errors or lost responses, inspect t3_thread_list before retrying. Attachments must be pending uploads. The new thread may not run with broader runtime or interaction modes than the caller: the calling T3 thread\'s own modes, or the permission mode an outside agent was approved with.', parameters: Schema.Struct({ projectId: Schema.optional(ProjectId), scratch: Schema.optional( @@ -133,7 +133,6 @@ const ThreadLaunchTool = Tool.make("t3_thread_launch", { }), success: Schema.Struct({ threadId: ThreadId, - link: Schema.optional(Schema.String), projectId: ProjectId, modelSelection: ModelSelection, runId: Schema.NullOr(RunId), diff --git a/apps/server/src/mcp/toolkits/thread/handlers.ts b/apps/server/src/mcp/toolkits/thread/handlers.ts index 8a15d1f15656..1a841542eac9 100644 --- a/apps/server/src/mcp/toolkits/thread/handlers.ts +++ b/apps/server/src/mcp/toolkits/thread/handlers.ts @@ -9,7 +9,6 @@ import { } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import { modelSelectionCommandType } from "@t3tools/shared/model"; -import { formatThreadLink } from "@t3tools/shared/threadLinks"; import * as McpToolAccess from "../../McpToolAccess.ts"; import { @@ -106,15 +105,9 @@ export const layer = McpToolAccess.toLayer(ThreadToolkit, { Effect.gen(function* () { const { caller } = yield* readCaller(); const threadSearch = yield* ThreadSearch.ThreadSearch; - const { matches } = yield* threadSearch + return yield* threadSearch .searchThreads({ ...input, projectId: input.projectId ?? caller?.projectId }) .pipe(Effect.mapError(unavailable)); - return { - matches: matches.map((match) => ({ - ...match, - link: formatThreadLink(match.threadId, match.title), - })), - }; }), ), t3_thread_fork: writesThread((input) => diff --git a/apps/server/src/mcp/toolkits/thread/tools.ts b/apps/server/src/mcp/toolkits/thread/tools.ts index 5e015368fd28..c23f032522eb 100644 --- a/apps/server/src/mcp/toolkits/thread/tools.ts +++ b/apps/server/src/mcp/toolkits/thread/tools.ts @@ -254,7 +254,6 @@ const ThreadSearchTool = Tool.make("t3_thread_search", { threadId: OrchestrationThreadSearchMatch.fields.threadId, projectId: OrchestrationThreadSearchMatch.fields.projectId, title: Schema.String, - link: Schema.optional(Schema.String), source: Schema.Literals(["title", "user", "assistant"]), snippet: Schema.optional(OrchestrationThreadSearchMatch.fields.snippet), messageCreatedAt: Schema.optional(OrchestrationThreadSearchMatch.fields.messageCreatedAt), diff --git a/docs/orchestration-v2/orchestrator-mcp-server.md b/docs/orchestration-v2/orchestrator-mcp-server.md index dc4af856b8c3..4a53d19b0087 100644 --- a/docs/orchestration-v2/orchestrator-mcp-server.md +++ b/docs/orchestration-v2/orchestrator-mcp-server.md @@ -391,8 +391,7 @@ and `creationSource: "mcp"`; provider output uses `creationSource: "provider"`. Actor and ingress are separate so agent-authored user-role messages remain distinguishable from human-authored messages. -Agents mention another thread as `[title](t3-thread://v1/)`. Thread -results carry this ready-made as `link`, so agents never format the id. The +Agents mention another thread as `[title](t3-thread://v1/)`. The link carries only the id. Clients first resolve it in the message's environment, then use a unique match in another enabled environment. Ambiguous matches stay unresolved. Web and desktop show the resolved thread’s current title. Mobile diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts index 49924b068678..e5ab1902dce2 100644 --- a/packages/contracts/src/orchestratorMcp.ts +++ b/packages/contracts/src/orchestratorMcp.ts @@ -195,7 +195,6 @@ const OrchestratorMcpRunningTaskNext = Schema.Literal(ORCHESTRATOR_MCP_RUNNING_T export const OrchestratorMcpDelegateTaskResult = Schema.Struct({ taskId: NodeId, childThreadId: ThreadId, - link: Schema.optional(Schema.String), childRunId: Schema.NullOr(RunId), childNodeId: NodeId, status: OrchestratorMcpDelegatedTaskStatus, @@ -280,7 +279,6 @@ export type OrchestratorMcpCreatedThreadStatus = typeof OrchestratorMcpCreatedTh export const OrchestratorMcpCreatedThread = Schema.Struct({ threadId: ThreadId, - link: Schema.optional(Schema.String), runId: Schema.NullOr(RunId), status: OrchestratorMcpCreatedThreadStatus, title: Schema.String, @@ -335,7 +333,6 @@ export type OrchestratorMcpThreadPullRequest = typeof OrchestratorMcpThreadPullR export const OrchestratorMcpThreadListItem = Schema.Struct({ threadId: ThreadId, title: Schema.String, - link: Schema.optional(Schema.String), createdBy: OrchestrationV2Actor, creationSource: OrchestrationV2CreationSource, status: OrchestratorMcpThreadStatus, @@ -403,7 +400,6 @@ export const OrchestratorMcpThreadDetail = Schema.Struct({ threadId: ThreadId, projectId: ProjectId, title: Schema.String, - link: Schema.optional(Schema.String), createdBy: OrchestrationV2Actor, creationSource: OrchestrationV2CreationSource, status: OrchestratorMcpThreadStatus, @@ -485,7 +481,6 @@ export type OrchestratorMcpThreadSendInput = typeof OrchestratorMcpThreadSendInp export const OrchestratorMcpThreadSendResult = Schema.Struct({ threadId: ThreadId, - link: Schema.optional(Schema.String), messageId: MessageId, runId: RunId, taskId: Schema.optional(NodeId), diff --git a/packages/shared/src/threadLinks.test.ts b/packages/shared/src/threadLinks.test.ts index a6c8eebc7e65..ef27422dedf8 100644 --- a/packages/shared/src/threadLinks.test.ts +++ b/packages/shared/src/threadLinks.test.ts @@ -98,14 +98,6 @@ describe("thread links", () => { ); }); - it("writes the thread id verbatim so the link opens that exact thread", () => { - const link = formatThreadLink("thread:delegated-task:mcp%3A1", "Fix"); - expect(link).toBe("[Fix](t3-thread://v1/thread:delegated-task:mcp%3A1)"); - expect(parseThreadLinkHref(link.slice(link.indexOf("](") + 2, -1))).toBe( - "thread:delegated-task:mcp%3A1", - ); - }); - it("formats a label that would otherwise break the Markdown link", () => { expect(formatThreadLink("t1", "Fix [ci] \\ build")).toBe("[Fix ci build](t3-thread://v1/t1)"); expect(formatThreadLink("t1", " ] ")).toBe("[t1](t3-thread://v1/t1)");