refactor(task): align continuation ids with task_id

This commit is contained in:
YeonGyu-Kim
2026-04-16 23:13:44 +09:00
parent 56be458ede
commit d89e257d8a
29 changed files with 180 additions and 96 deletions
@@ -3,6 +3,8 @@ import type { ExecutorContext, ParentContext } from "./executor-types"
import { publishToolMetadata } from "../../features/tool-metadata-store"
import { formatDetailedError } from "./error-formatting"
import { getSessionTools } from "../../shared/session-tools-store"
import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract"
import { getTaskID } from "./task-id"
export async function executeBackgroundContinuation(
args: DelegateTaskArgs,
@@ -13,8 +15,13 @@ export async function executeBackgroundContinuation(
const { manager } = executorCtx
try {
const taskID = getTaskID(args)
if (!taskID) {
throw new Error("task_id is required to continue a background task")
}
const task = await manager.resume({
sessionId: args.session_id!,
sessionId: taskID,
prompt: args.prompt,
parentSessionID: parentContext.sessionID,
parentMessageID: parentContext.messageID,
@@ -31,6 +38,8 @@ export async function executeBackgroundContinuation(
load_skills: args.load_skills,
description: args.description,
run_in_background: args.run_in_background,
taskId: task.sessionID,
backgroundTaskId: task.id,
sessionId: task.sessionID,
command: args.command,
model: task.model ? { providerID: task.model.providerID, modelID: task.model.modelID } : undefined,
@@ -50,14 +59,17 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id
Do NOT call background_output now. Wait for <system-reminder> notification first.
<task_metadata>
session_id: ${task.sessionID}
${task.agent ? `subagent: ${task.agent}\n` : ""}</task_metadata>`
${buildTaskMetadataBlock({
sessionId: task.sessionID,
taskId: task.sessionID,
backgroundTaskId: task.id,
agent: task.agent,
})}`
} catch (error) {
return formatDetailedError(error, {
operation: "Continue background task",
args,
sessionID: args.session_id,
sessionID: getTaskID(args),
})
}
}
@@ -104,11 +104,14 @@ describeFn("executeBackgroundTask output/session metadata compatibility", () =>
//#then - output and metadata should include canonical session linkage
expectFn(result).toContain("<task_metadata>")
expectFn(result).toContain("session_id: ses_sub_123")
expectFn(result).toContain("task_id: bg_resolved")
expectFn(result).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")
expectFn(metadataCalls).toHaveLength(1)
expectFn(metadataCalls[0].metadata.sessionId).toBe("ses_sub_123")
expectFn(metadataCalls[0].metadata.taskId).toBe("ses_sub_123")
expectFn(metadataCalls[0].metadata.backgroundTaskId).toBe("bg_resolved")
})
testFn("captures late-resolved session id and emits synced metadata", async () => {
@@ -152,10 +155,12 @@ 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: bg_late")
expectFn(result).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")
expectFn(metadataCalls[0].metadata.taskId).toBe("ses_late_123")
expectFn(metadataCalls[0].metadata.backgroundTaskId).toBe("bg_late")
})
testFn("passes question-deny session permission when launching delegate task", async () => {
+10 -1
View File
@@ -10,6 +10,7 @@ import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission"
import { setSessionFallbackChain } from "../../hooks/model-fallback/hook"
import { stripAgentListSortPrefix } from "../../shared/agent-display-names"
import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract"
function continueSessionSetup(args: {
taskID: string
@@ -125,6 +126,8 @@ export async function executeBackgroundTask(
description: args.description,
run_in_background: args.run_in_background,
command: args.command,
...(sessionId ? { taskId: sessionId } : {}),
backgroundTaskId: task.id,
...(sessionId ? { sessionId } : {}),
...(categoryModel ? { model: { providerID: categoryModel.providerID, modelID: categoryModel.modelID } } : {}),
}
@@ -136,7 +139,13 @@ export async function executeBackgroundTask(
await publishToolMetadata(ctx, unstableMeta)
const taskMetadataBlock = sessionId
? `\n\n<task_metadata>\nsession_id: ${sessionId}\ntask_id: ${task.id}\nbackground_task_id: ${task.id}\n</task_metadata>`
? `\n\n${buildTaskMetadataBlock({
sessionId,
taskId: sessionId,
backgroundTaskId: task.id,
agent: task.agent,
category: args.category,
})}`
: ""
return `Background task launched.
+4 -2
View File
@@ -1,4 +1,5 @@
import type { DelegateTaskArgs } from "./types"
import { getTaskID } from "./task-id"
/**
* Context for error formatting.
@@ -35,8 +36,9 @@ export function formatDetailedError(error: unknown, ctx: ErrorContext): string {
lines.push(`- subagent_type: ${ctx.args.subagent_type ?? "(none)"}`)
lines.push(`- run_in_background: ${ctx.args.run_in_background}`)
lines.push(`- load_skills: [${ctx.args.load_skills?.join(", ") ?? ""}]`)
if (ctx.args.session_id) {
lines.push(`- session_id: ${ctx.args.session_id}`)
const taskID = getTaskID(ctx.args)
if (taskID) {
lines.push(`- task_id: ${taskID}`)
}
}
+21 -12
View File
@@ -12,6 +12,8 @@ import { syncContinuationDeps, type SyncContinuationDeps } from "./sync-continua
import { setSessionTools } from "../../shared/session-tools-store"
import { normalizeSDKResponse } from "../../shared"
import { buildTaskPrompt } from "./prompt-builder"
import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract"
import { getTaskID } from "./task-id"
export async function executeSyncContinuation(
args: DelegateTaskArgs,
@@ -21,7 +23,11 @@ export async function executeSyncContinuation(
): Promise<string> {
const { client, syncPollTimeoutMs, sisyphusAgentConfig } = executorCtx
const toastManager = getTaskToastManager()
const taskId = `resume_sync_${args.session_id!.slice(0, 8)}`
const continuationID = getTaskID(args)
if (!continuationID) {
throw new Error("task_id is required to continue a sync task")
}
const taskId = `resume_sync_${continuationID.slice(0, 8)}`
const startTime = new Date()
if (toastManager) {
@@ -42,7 +48,7 @@ export async function executeSyncContinuation(
try {
try {
const messagesResp = await client.session.messages({ path: { id: args.session_id! } })
const messagesResp = await client.session.messages({ path: { id: continuationID } })
const messages = normalizeSDKResponse(messagesResp, [] as SessionMessage[])
anchorMessageCount = messages.length
for (let i = messages.length - 1; i >= 0; i--) {
@@ -55,7 +61,7 @@ export async function executeSyncContinuation(
}
}
} catch {
const resumeMessageDir = getMessageDir(args.session_id!)
const resumeMessageDir = getMessageDir(continuationID)
const resumeMessage = resumeMessageDir ? findNearestMessageWithFields(resumeMessageDir) : null
resumeAgent = resumeMessage?.agent
resumeModel = resumeMessage?.model?.providerID && resumeMessage?.model?.modelID
@@ -71,7 +77,8 @@ export async function executeSyncContinuation(
load_skills: args.load_skills,
description: args.description,
run_in_background: args.run_in_background,
sessionId: args.session_id,
taskId: continuationID,
sessionId: continuationID,
sync: true,
command: args.command,
model: resumeModel,
@@ -88,10 +95,10 @@ export async function executeSyncContinuation(
question: false,
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
}
setSessionTools(args.session_id!, tools)
setSessionTools(continuationID, tools)
await promptWithModelSuggestionRetry(client, {
path: { id: args.session_id! },
path: { id: continuationID },
body: {
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
...(resumeModel !== undefined ? { model: resumeModel } : {}),
@@ -105,12 +112,12 @@ export async function executeSyncContinuation(
toastManager.removeTask(taskId)
}
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError)
return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}`
return `Failed to send continuation prompt: ${errorMessage}\n\nTask ID: ${continuationID}`
}
try {
const pollError = await deps.pollSyncSession(ctx, client, {
sessionID: args.session_id!,
sessionID: continuationID,
agentToUse: resumeAgent ?? "continue",
toastManager,
taskId,
@@ -120,7 +127,7 @@ export async function executeSyncContinuation(
return pollError
}
const result = await deps.fetchSyncResult(client, args.session_id!, anchorMessageCount)
const result = await deps.fetchSyncResult(client, continuationID, anchorMessageCount)
if (!result.ok) {
return result.error
}
@@ -133,9 +140,11 @@ export async function executeSyncContinuation(
${result.textContent || "(No text output)"}
<task_metadata>
session_id: ${args.session_id}
${resumeAgent ? `subagent: ${resumeAgent}\n` : ""}</task_metadata>`
${buildTaskMetadataBlock({
sessionId: continuationID,
taskId: continuationID,
agent: resumeAgent,
})}`
} finally {
if (toastManager) {
toastManager.removeTask(taskId)
+8 -3
View File
@@ -11,6 +11,7 @@ import { formatDetailedError } from "./error-formatting"
import { syncTaskDeps, type SyncTaskDeps } from "./sync-task-deps"
import { setSessionFallbackChain, clearSessionFallbackChain } from "../../hooks/model-fallback/hook"
import { retrySyncPromptWithFallbacks } from "./sync-task-fallback"
import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract"
export async function executeSyncTask(
args: DelegateTaskArgs,
@@ -122,6 +123,7 @@ export async function executeSyncTask(
load_skills: args.load_skills,
description: args.description,
run_in_background: args.run_in_background,
taskId: sessionID,
sessionId: sessionID,
sync: true,
spawnDepth: spawnContext.childDepth,
@@ -210,9 +212,12 @@ Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""}${mod
${result.textContent || "(No text output)"}
<task_metadata>
session_id: ${sessionID}
</task_metadata>`
${buildTaskMetadataBlock({
sessionId: sessionID,
taskId: sessionID,
agent: agentToUse,
category: args.category,
})}`
} finally {
if (toastManager && taskId !== undefined) {
toastManager.removeTask(taskId)
+5
View File
@@ -0,0 +1,5 @@
import type { DelegateTaskArgs } from "./types"
export function getTaskID(args: Pick<DelegateTaskArgs, "task_id" | "session_id">): string | undefined {
return args.task_id ?? args.session_id
}
@@ -41,6 +41,7 @@ function createDelegateTask(...args: Parameters<typeof import("./tools").createD
//#then
expect(description).toContain("subagent_type: Use specific agent directly")
expect(description).toContain("task_id: Existing task to continue")
expect(description).not.toContain("sisyphus")
expect(description).not.toContain("hephaestus")
expect(description).not.toContain("prometheus")
+12 -7
View File
@@ -84,13 +84,14 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini
${categoryList}
- subagent_type: Use specific agent directly (explore, librarian, oracle, metis, momus)
- run_in_background: REQUIRED. true=async (returns task_id), false=sync (waits). Use background=true ONLY for parallel exploration with 5+ independent queries.
- session_id: Existing Task session to continue (from previous task output). Continues agent with FULL CONTEXT PRESERVED - saves tokens, maintains continuity.
- task_id: Existing task to continue (from previous task output). Continues the same subagent session with FULL CONTEXT PRESERVED.
- session_id: Deprecated alias for task_id. Accepted for backward compatibility.
- command: The command that triggered this task (optional, for slash command tracking).
**WHEN TO USE session_id:**
- Task failed/incomplete → session_id with "fix: [specific issue]"
- Need follow-up on previous result → session_id with additional question
- Multi-turn conversation with same agent → always session_id instead of new task
**WHEN TO USE task_id:**
- Task failed/incomplete → task_id with "fix: [specific issue]"
- Need follow-up on previous result → task_id with additional question
- Multi-turn conversation with same agent → always task_id instead of new task
Prompts MUST be in English.`
@@ -103,11 +104,15 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini
run_in_background: tool.schema.boolean().describe("REQUIRED. true=async (returns task_id), false=sync (waits). Use false for task delegation, true ONLY for parallel exploration."),
category: tool.schema.string().optional().describe(`REQUIRED if subagent_type not provided. Do NOT provide both category and subagent_type.`),
subagent_type: tool.schema.string().optional().describe("REQUIRED if category not provided. Do NOT provide both category and subagent_type."),
session_id: tool.schema.string().optional().describe("Existing Task session to continue"),
task_id: tool.schema.string().optional().describe("Existing task to continue. Canonical resume identifier."),
session_id: tool.schema.string().optional().describe("Deprecated alias for task_id. Existing task to continue."),
command: tool.schema.string().optional().describe("The command that triggered this task"),
},
async execute(args: DelegateTaskArgs, toolContext) {
const ctx = toolContext as ToolContextWithMetadata
if (!args.task_id && args.session_id) {
args.task_id = args.session_id
}
if (args.category) {
if (args.subagent_type && args.subagent_type !== SISYPHUS_JUNIOR_AGENT) {
@@ -158,7 +163,7 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini
const parentContext = await resolveParentContext(ctx, options.client)
if (args.session_id) {
if (args.task_id || args.session_id) {
if (runInBackground) {
return executeBackgroundContinuation(args, ctx, options, parentContext)
}
+2
View File
@@ -14,6 +14,8 @@ export interface DelegateTaskArgs {
category?: string
subagent_type?: string
run_in_background: boolean
task_id?: string
/** @deprecated Use task_id instead. */
session_id?: string
command?: string
load_skills: string[]