diff --git a/src/tools/background-task/create-background-output.metadata.test.ts b/src/tools/background-task/create-background-output.metadata.test.ts index d763b961a..0555d3748 100644 --- a/src/tools/background-task/create-background-output.metadata.test.ts +++ b/src/tools/background-task/create-background-output.metadata.test.ts @@ -65,4 +65,46 @@ describe("createBackgroundOutput metadata", () => { clearPendingStore() }) + + test("explains when a session id is passed as the background task id", async () => { + // #given + const task: BackgroundTask = { + id: "bg-real-task", + sessionId: "ses-child-task", + parentSessionId: "main-1", + parentMessageId: "msg-1", + description: "background task", + prompt: "do work", + agent: "test-agent", + status: "completed", + } + const manager: BackgroundOutputManager = { + getTask: id => (id === task.id ? task : undefined), + } + const client: BackgroundOutputClient = { + session: { + messages: async () => ({ data: [] }), + }, + } + const tool = createBackgroundOutput(manager, client) + const context = { + sessionID: "test-session", + messageID: "test-message", + agent: "test-agent", + directory: projectDir, + worktree: projectDir, + abort: new AbortController().signal, + metadata: () => {}, + ask: async () => {}, + callID: "call-1", + } satisfies ToolContextWithCallID + + // #when + const output = await tool.execute({ task_id: "ses-child-task" }, context) + + // #then + expect(output).toContain("background_output expects a background task ID") + expect(output).toContain("bg_") + expect(output).toContain('session_read(session_id="ses-child-task")') + }) }) diff --git a/src/tools/background-task/create-background-output.ts b/src/tools/background-task/create-background-output.ts index e4ea3f8ca..45c13afbf 100644 --- a/src/tools/background-task/create-background-output.ts +++ b/src/tools/background-task/create-background-output.ts @@ -36,6 +36,22 @@ function appendTimeoutNote(output: string, timeoutMs: number): string { return `${output}\n\n> **Timed out waiting** after ${timeoutMs}ms. Task is still running; showing latest available output.` } +function isSessionId(value: string): boolean { + return /^ses[_-]/.test(value) +} + +function formatTaskNotFoundMessage(taskId: string): string { + if (!isSessionId(taskId)) { + return `Task not found: ${taskId}` + } + + return `Task not found: ${taskId} + +background_output expects a background task ID such as \`bg_...\`, not a session ID. +Use the \`background_task_id\` / \`Background Task ID\` from the task launch output or completion notification. +To inspect this session directly, use \`session_read(session_id="${taskId}")\`, \`session_info\`, or \`session_search\`.` +} + export function createBackgroundOutput(manager: BackgroundOutputManager, client: BackgroundOutputClient): ToolDefinition { return tool({ description: BACKGROUND_OUTPUT_DESCRIPTION, @@ -60,7 +76,7 @@ export function createBackgroundOutput(manager: BackgroundOutputManager, client: const ctx = toolContext as ToolContextWithMetadata const task = manager.getTask(args.task_id) if (!task) { - return `Task not found: ${args.task_id}` + return formatTaskNotFoundMessage(args.task_id) } const meta = { diff --git a/src/tools/delegate-task/background-continuation.test.ts b/src/tools/delegate-task/background-continuation.test.ts index bbb03a414..f19b5e073 100644 --- a/src/tools/delegate-task/background-continuation.test.ts +++ b/src/tools/delegate-task/background-continuation.test.ts @@ -45,6 +45,9 @@ describe("executeBackgroundContinuation - subagent metadata", () => { expect(result).toContain("") expect(result).toContain("subagent: oracle") expect(result).toContain("session_id: ses_resumed_123") + expect(result).toContain("background_task_id: bg_task_001") + expect(result).not.toContain("task_id: ses_resumed_123") + expect(result).toContain("Background Task ID: bg_task_001") }) test("omits subagent from task_metadata when task agent is undefined", async () => { diff --git a/src/tools/delegate-task/background-continuation.ts b/src/tools/delegate-task/background-continuation.ts index 890a87ebf..b090170ee 100644 --- a/src/tools/delegate-task/background-continuation.ts +++ b/src/tools/delegate-task/background-continuation.ts @@ -60,7 +60,7 @@ export async function executeBackgroundContinuation( return `Background task continued. -Task ID: ${backgroundTaskId} +Background Task ID: ${backgroundTaskId} Description: ${task.description} Agent: ${task.agent} Status: ${task.status} @@ -72,7 +72,6 @@ Do NOT call background_output now. Wait for notification first ${buildTaskMetadataBlock({ sessionId, - taskId: sessionId, backgroundTaskId, agent: task.agent, category: task.category, diff --git a/src/tools/delegate-task/background-task.test.ts b/src/tools/delegate-task/background-task.test.ts index 659090e1f..c1ee61b46 100644 --- a/src/tools/delegate-task/background-task.test.ts +++ b/src/tools/delegate-task/background-task.test.ts @@ -104,7 +104,7 @@ describeFn("executeBackgroundTask output/session metadata compatibility", () => //#then - output and metadata should include canonical session linkage expectFn(result).toContain("") expectFn(result).toContain("session_id: ses_sub_123") - expectFn(result).toContain("task_id: ses_sub_123") + expectFn(result).not.toContain("task_id: ses_sub_123") expectFn(result).toContain("background_task_id: bg_resolved") expectFn(result).toContain("subagent: explore") expectFn(result).toContain("Background Task ID: bg_resolved") @@ -114,6 +114,49 @@ describeFn("executeBackgroundTask output/session metadata compatibility", () => expectFn(metadataCalls[0].metadata.backgroundTaskId).toBe("bg_resolved") }) + testFn("keeps continuation taskId out of visible background metadata", async () => { + //#given - launched background task with both a background id and session id + const metadataCalls: Array<{ metadata: Record }> = [] + const manager = { + launch: async () => ({ + id: "bg_visible_contract", + sessionId: "ses_visible_contract", + description: "Visible contract", + agent: "explore", + status: "running", + }), + getTask: () => ({ sessionId: "ses_visible_contract" }), + } + + const result = await executeBackgroundTask( + { + description: "Visible contract", + prompt: "check", + run_in_background: true, + load_skills: [], + }, + { + sessionID: "ses_parent", + callID: "call_visible_contract", + metadata: async (value: { metadata: Record }) => metadataCalls.push(value), + abort: new AbortController().signal, + }, + { manager }, + { sessionID: "ses_parent", messageID: "msg_visible_contract" }, + "explore", + undefined, + undefined, + undefined, + ) + + //#then - machine metadata keeps OpenCode compatibility, visible text avoids the overloaded task_id label + expectFn(result).toContain("session_id: ses_visible_contract") + expectFn(result).toContain("background_task_id: bg_visible_contract") + expectFn(result).not.toContain("task_id: ses_visible_contract") + expectFn(metadataCalls[0].metadata.taskId).toBe("ses_visible_contract") + expectFn(metadataCalls[0].metadata.backgroundTaskId).toBe("bg_visible_contract") + }) + testFn("captures late-resolved session id and emits synced metadata", async () => { //#given - background task session id appears after launch via manager polling const metadataCalls: any[] = [] @@ -155,7 +198,7 @@ describeFn("executeBackgroundTask output/session metadata compatibility", () => //#then - late session id still propagates to task metadata contract expectFn(result).toContain("session_id: ses_late_123") - expectFn(result).toContain("task_id: ses_late_123") + expectFn(result).not.toContain("task_id: ses_late_123") expectFn(result).toContain("background_task_id: bg_late") expectFn(metadataCalls).toHaveLength(1) expectFn(metadataCalls[0].metadata.sessionId).toBe("ses_late_123") diff --git a/src/tools/delegate-task/background-task.ts b/src/tools/delegate-task/background-task.ts index 8de84bf68..767bab764 100644 --- a/src/tools/delegate-task/background-task.ts +++ b/src/tools/delegate-task/background-task.ts @@ -192,7 +192,6 @@ export async function executeBackgroundTask( const taskMetadataBlock = sessionId ? `\n\n${buildTaskMetadataBlock({ sessionId, - taskId: sessionId, backgroundTaskId: task.id, agent: task.agent, category: args.category, diff --git a/src/tools/delegate-task/unstable-agent-task.ts b/src/tools/delegate-task/unstable-agent-task.ts index f6eff2a8a..0f9c026dd 100644 --- a/src/tools/delegate-task/unstable-agent-task.ts +++ b/src/tools/delegate-task/unstable-agent-task.ts @@ -89,7 +89,6 @@ export async function executeUnstableAgentTask( const taskMetadataBlock = buildTaskMetadataBlock({ sessionId: sessionID, - taskId: sessionID, backgroundTaskId: task.id, agent: agentToUse, category: args.category,