2026-02-08 13:57:26 +09:00
|
|
|
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
2026-03-18 14:21:27 +01:00
|
|
|
import type { DelegateTaskArgs, ToolContextWithMetadata, DelegatedModelConfig } from "./types"
|
2026-02-08 13:57:26 +09:00
|
|
|
import type { ExecutorContext, ParentContext } from "./executor-types"
|
|
|
|
|
import { getTaskToastManager } from "../../features/task-toast-manager"
|
2026-04-16 13:52:20 +09:00
|
|
|
import { publishToolMetadata } from "../../features/tool-metadata-store"
|
2026-02-19 04:41:00 +02:00
|
|
|
import { subagentSessions, syncSubagentSessions, setSessionAgent } from "../../features/claude-code-session-state"
|
2026-02-08 18:03:15 +09:00
|
|
|
import { log } from "../../shared/logger"
|
2026-02-03 12:18:52 +09:00
|
|
|
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
|
2026-02-08 13:57:26 +09:00
|
|
|
import { formatDuration } from "./time-formatter"
|
|
|
|
|
import { formatDetailedError } from "./error-formatting"
|
2026-02-10 22:54:30 +09:00
|
|
|
import { syncTaskDeps, type SyncTaskDeps } from "./sync-task-deps"
|
2026-04-28 15:28:20 +09:00
|
|
|
import { getNextSyncFallbackModel, retrySyncPromptWithFallbacks } from "./sync-task-fallback"
|
2026-04-16 23:13:44 +09:00
|
|
|
import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract"
|
2026-04-17 14:42:38 +09:00
|
|
|
import { resolveMetadataModel } from "./resolve-metadata-model"
|
2026-04-28 15:28:20 +09:00
|
|
|
import { shouldRetryError } from "../../shared/model-error-classifier"
|
|
|
|
|
import type { ModelFallbackState } from "../../hooks/model-fallback/hook"
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-05-10 14:55:06 +09:00
|
|
|
function shouldAttemptPollErrorRecovery(pollError: string): boolean {
|
2026-05-11 08:46:14 +09:00
|
|
|
const trimmed = pollError.trim()
|
|
|
|
|
|
|
|
|
|
if (trimmed.length === 0) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/\bMessageAbortedError\b/u.test(trimmed)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/\bDOMException\b/u.test(trimmed) && /\bAbortError\b/u.test(trimmed)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/\bAbortError\b/u.test(trimmed) && !/\bTask aborted\b/u.test(trimmed)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 09:01:17 +09:00
|
|
|
if (/^the operation was aborted\.?$/iu.test(trimmed)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 08:46:14 +09:00
|
|
|
return false
|
2026-05-10 14:55:06 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 13:57:26 +09:00
|
|
|
export async function executeSyncTask(
|
|
|
|
|
args: DelegateTaskArgs,
|
|
|
|
|
ctx: ToolContextWithMetadata,
|
|
|
|
|
executorCtx: ExecutorContext,
|
|
|
|
|
parentContext: ParentContext,
|
|
|
|
|
agentToUse: string,
|
2026-03-18 14:21:27 +01:00
|
|
|
categoryModel: DelegatedModelConfig | undefined,
|
2026-02-08 13:57:26 +09:00
|
|
|
systemContent: string | undefined,
|
2026-02-10 22:54:30 +09:00
|
|
|
modelInfo?: ModelFallbackInfo,
|
2026-02-20 00:02:17 +02:00
|
|
|
fallbackChain?: import("../../shared/model-requirements").FallbackEntry[],
|
2026-02-10 22:54:30 +09:00
|
|
|
deps: SyncTaskDeps = syncTaskDeps
|
2026-02-08 13:57:26 +09:00
|
|
|
): Promise<string> {
|
2026-03-11 17:46:04 +09:00
|
|
|
const { manager, client, directory, onSyncSessionCreated, syncPollTimeoutMs } = executorCtx
|
2026-02-08 13:57:26 +09:00
|
|
|
const toastManager = getTaskToastManager()
|
|
|
|
|
let taskId: string | undefined
|
|
|
|
|
let syncSessionID: string | undefined
|
2026-03-11 18:44:20 +09:00
|
|
|
let spawnReservation:
|
|
|
|
|
| Awaited<ReturnType<ExecutorContext["manager"]["reserveSubagentSpawn"]>>
|
|
|
|
|
| undefined
|
2026-02-08 13:57:26 +09:00
|
|
|
|
|
|
|
|
try {
|
2026-03-11 18:44:20 +09:00
|
|
|
if (typeof manager?.reserveSubagentSpawn === "function") {
|
|
|
|
|
spawnReservation = await manager.reserveSubagentSpawn(parentContext.sessionID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 19:09:01 +09:00
|
|
|
// Only default to childDepth: 1 for legacy managers that cannot enforce spawn depth.
|
2026-04-07 19:56:51 +09:00
|
|
|
let spawnContext: { rootSessionID: string; parentDepth: number; childDepth: number }
|
|
|
|
|
if (spawnReservation?.spawnContext) {
|
|
|
|
|
spawnContext = spawnReservation.spawnContext
|
|
|
|
|
} else if (typeof manager?.assertCanSpawn === "function") {
|
|
|
|
|
spawnContext = await manager.assertCanSpawn(parentContext.sessionID)
|
|
|
|
|
} else {
|
|
|
|
|
log(
|
|
|
|
|
"[task] WARNING: BackgroundManager has no spawn enforcement methods (reserveSubagentSpawn / assertCanSpawn). " +
|
2026-04-16 23:02:27 +09:00
|
|
|
"Depth limits cannot be enforced for this task. This indicates an old SDK or a misconfiguration.",
|
2026-04-07 19:56:51 +09:00
|
|
|
{ parentSessionID: parentContext.sessionID }
|
|
|
|
|
)
|
|
|
|
|
spawnContext = {
|
|
|
|
|
rootSessionID: parentContext.sessionID,
|
|
|
|
|
parentDepth: 0,
|
|
|
|
|
childDepth: 1,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 17:46:04 +09:00
|
|
|
|
2026-02-10 22:54:30 +09:00
|
|
|
const createSessionResult = await deps.createSyncSession(client, {
|
2026-02-08 13:57:26 +09:00
|
|
|
parentSessionID: parentContext.sessionID,
|
|
|
|
|
agentToUse,
|
|
|
|
|
description: args.description,
|
|
|
|
|
defaultDirectory: directory,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!createSessionResult.ok) {
|
2026-03-11 18:44:20 +09:00
|
|
|
spawnReservation?.rollback()
|
2026-02-08 13:57:26 +09:00
|
|
|
return createSessionResult.error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sessionID = createSessionResult.sessionID
|
2026-03-11 18:44:20 +09:00
|
|
|
spawnReservation?.commit()
|
2026-02-08 13:57:26 +09:00
|
|
|
syncSessionID = sessionID
|
|
|
|
|
|
2026-04-28 19:09:01 +09:00
|
|
|
const registerSyncSession = async (newSessionID: string): Promise<void> => {
|
|
|
|
|
syncSessionID = newSessionID
|
|
|
|
|
subagentSessions.add(newSessionID)
|
|
|
|
|
syncSubagentSessions.add(newSessionID)
|
|
|
|
|
setSessionAgent(newSessionID, agentToUse)
|
|
|
|
|
executorCtx.modelFallbackControllerAccessor?.setSessionFallbackChain(newSessionID, fallbackChain)
|
2026-02-03 12:18:52 +09:00
|
|
|
|
2026-04-28 19:09:01 +09:00
|
|
|
if (args.category) {
|
|
|
|
|
SessionCategoryRegistry.register(newSessionID, args.category)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (onSyncSessionCreated) {
|
|
|
|
|
log("[task] Invoking onSyncSessionCreated callback", { sessionID: newSessionID, parentID: parentContext.sessionID })
|
|
|
|
|
try {
|
|
|
|
|
await onSyncSessionCreated({
|
|
|
|
|
sessionID: newSessionID,
|
|
|
|
|
parentID: parentContext.sessionID,
|
|
|
|
|
title: args.description,
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log("[task] onSyncSessionCreated callback failed", { error: String(error) })
|
|
|
|
|
}
|
|
|
|
|
await new Promise(r => setTimeout(r, 200))
|
2026-04-20 15:16:48 +09:00
|
|
|
}
|
2026-02-08 13:57:26 +09:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 19:09:01 +09:00
|
|
|
const publishSyncMetadata = async (
|
|
|
|
|
currentSessionID: string,
|
|
|
|
|
currentModel: DelegatedModelConfig | undefined,
|
|
|
|
|
currentTaskId: string,
|
|
|
|
|
spawnDepth: number,
|
|
|
|
|
): Promise<void> => {
|
|
|
|
|
await publishToolMetadata(ctx, {
|
|
|
|
|
title: args.description,
|
|
|
|
|
metadata: {
|
|
|
|
|
prompt: args.prompt,
|
|
|
|
|
agent: agentToUse,
|
|
|
|
|
category: args.category,
|
|
|
|
|
...(args.requested_subagent_type !== undefined ? { requested_subagent_type: args.requested_subagent_type } : {}),
|
|
|
|
|
load_skills: args.load_skills,
|
|
|
|
|
description: args.description,
|
|
|
|
|
run_in_background: args.run_in_background,
|
|
|
|
|
taskId: currentSessionID,
|
|
|
|
|
sessionId: currentSessionID,
|
|
|
|
|
sync: true,
|
|
|
|
|
spawnDepth,
|
|
|
|
|
command: args.command,
|
|
|
|
|
model: resolveMetadataModel(currentModel, parentContext.model),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await registerSyncSession(sessionID)
|
|
|
|
|
|
2026-02-08 13:57:26 +09:00
|
|
|
taskId = `sync_${sessionID.slice(0, 8)}`
|
|
|
|
|
const startTime = new Date()
|
|
|
|
|
|
|
|
|
|
if (toastManager) {
|
|
|
|
|
toastManager.addTask({
|
|
|
|
|
id: taskId,
|
2026-02-19 04:41:00 +02:00
|
|
|
sessionID,
|
2026-02-08 13:57:26 +09:00
|
|
|
description: args.description,
|
|
|
|
|
agent: agentToUse,
|
|
|
|
|
isBackground: false,
|
|
|
|
|
category: args.category,
|
|
|
|
|
skills: args.load_skills,
|
|
|
|
|
modelInfo,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-28 19:09:01 +09:00
|
|
|
await publishSyncMetadata(sessionID, categoryModel, taskId, spawnContext.childDepth)
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-04-20 15:16:48 +09:00
|
|
|
const syncPromptInput = {
|
2026-02-08 13:57:26 +09:00
|
|
|
sessionID,
|
|
|
|
|
agentToUse,
|
|
|
|
|
args,
|
|
|
|
|
systemContent,
|
2026-05-12 18:14:17 +09:00
|
|
|
directory: createSessionResult.parentDirectory,
|
2026-02-08 13:57:26 +09:00
|
|
|
toastManager,
|
|
|
|
|
taskId,
|
2026-03-28 09:34:58 -07:00
|
|
|
sisyphusAgentConfig: executorCtx.sisyphusAgentConfig,
|
2026-04-20 15:16:48 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let effectiveCategoryModel = categoryModel
|
2026-04-28 15:28:20 +09:00
|
|
|
let fallbackState: ModelFallbackState | undefined = effectiveCategoryModel && fallbackChain?.length
|
|
|
|
|
? {
|
|
|
|
|
providerID: effectiveCategoryModel.providerID,
|
|
|
|
|
modelID: effectiveCategoryModel.modelID,
|
|
|
|
|
fallbackChain,
|
|
|
|
|
attemptCount: 0,
|
|
|
|
|
pending: true,
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
let activeSessionID = sessionID
|
2026-04-12 02:30:32 +09:00
|
|
|
|
2026-04-28 15:28:20 +09:00
|
|
|
const cleanupRetrySession = (currentSessionID: string): void => {
|
|
|
|
|
subagentSessions.delete(currentSessionID)
|
|
|
|
|
syncSubagentSessions.delete(currentSessionID)
|
|
|
|
|
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(currentSessionID)
|
|
|
|
|
SessionCategoryRegistry.remove(currentSessionID)
|
2026-02-08 13:57:26 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:09:22 +09:00
|
|
|
try {
|
2026-04-28 15:28:20 +09:00
|
|
|
while (true) {
|
|
|
|
|
let promptError = await deps.sendSyncPrompt(client, {
|
|
|
|
|
...syncPromptInput,
|
|
|
|
|
sessionID: activeSessionID,
|
|
|
|
|
categoryModel: effectiveCategoryModel,
|
|
|
|
|
})
|
|
|
|
|
if (promptError) {
|
|
|
|
|
const promptResult = await retrySyncPromptWithFallbacks({
|
|
|
|
|
sessionID: activeSessionID,
|
|
|
|
|
initialError: promptError,
|
|
|
|
|
categoryModel: effectiveCategoryModel,
|
|
|
|
|
fallbackChain,
|
|
|
|
|
sendPrompt: async (fallbackModel) => {
|
|
|
|
|
return deps.sendSyncPrompt(client, {
|
|
|
|
|
...syncPromptInput,
|
|
|
|
|
sessionID: activeSessionID,
|
|
|
|
|
categoryModel: fallbackModel,
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
promptError = promptResult.promptError
|
|
|
|
|
effectiveCategoryModel = promptResult.categoryModel
|
|
|
|
|
fallbackState = promptResult.fallbackState ?? fallbackState
|
|
|
|
|
|
|
|
|
|
if (promptError) {
|
|
|
|
|
return promptError
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-04-28 15:28:20 +09:00
|
|
|
const pollError = await deps.pollSyncSession(ctx, client, {
|
|
|
|
|
sessionID: activeSessionID,
|
|
|
|
|
agentToUse,
|
|
|
|
|
toastManager,
|
|
|
|
|
taskId,
|
|
|
|
|
}, syncPollTimeoutMs)
|
|
|
|
|
if (pollError) {
|
2026-05-10 14:55:06 +09:00
|
|
|
if (shouldAttemptPollErrorRecovery(pollError)) {
|
2026-05-11 09:20:14 +09:00
|
|
|
const recoveredResult = await deps.fetchSyncResult(client, activeSessionID, undefined, {
|
|
|
|
|
strictAbortRecovery: true,
|
|
|
|
|
})
|
2026-05-10 14:55:06 +09:00
|
|
|
if (recoveredResult.ok) {
|
|
|
|
|
const duration = formatDuration(startTime)
|
|
|
|
|
|
|
|
|
|
const actualModelStr = effectiveCategoryModel
|
|
|
|
|
? `${effectiveCategoryModel.providerID}/${effectiveCategoryModel.modelID}`
|
|
|
|
|
: undefined
|
|
|
|
|
const parentModelStr = parentContext.model
|
|
|
|
|
? `${parentContext.model.providerID}/${parentContext.model.modelID}`
|
|
|
|
|
: undefined
|
|
|
|
|
let modelRoutingNote = ""
|
|
|
|
|
if (actualModelStr && parentModelStr && actualModelStr !== parentModelStr) {
|
|
|
|
|
modelRoutingNote = `\n⚠️ Model fallback used: requested ${parentModelStr}, executed ${actualModelStr}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `Task completed in ${duration}.\n\n---\n\n${recoveredResult.textContent || "(No text output)"}${modelRoutingNote}\n\n${buildTaskMetadataBlock({
|
|
|
|
|
sessionId: activeSessionID,
|
|
|
|
|
taskId: activeSessionID,
|
|
|
|
|
agent: agentToUse,
|
|
|
|
|
category: args.category,
|
|
|
|
|
})}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 15:28:20 +09:00
|
|
|
const nextFallbackModel = shouldRetryError({ message: pollError })
|
|
|
|
|
? getNextSyncFallbackModel(activeSessionID, fallbackState)
|
|
|
|
|
: null
|
|
|
|
|
if (!nextFallbackModel) {
|
|
|
|
|
return pollError
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanupRetrySession(activeSessionID)
|
|
|
|
|
|
|
|
|
|
const retrySessionResult = await deps.createSyncSession(client, {
|
|
|
|
|
parentSessionID: parentContext.sessionID,
|
|
|
|
|
agentToUse,
|
|
|
|
|
description: args.description,
|
|
|
|
|
defaultDirectory: directory,
|
|
|
|
|
})
|
|
|
|
|
if (!retrySessionResult.ok) {
|
|
|
|
|
return retrySessionResult.error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
activeSessionID = retrySessionResult.sessionID
|
|
|
|
|
effectiveCategoryModel = nextFallbackModel
|
2026-04-28 19:09:01 +09:00
|
|
|
await registerSyncSession(activeSessionID)
|
|
|
|
|
if (toastManager && taskId) {
|
|
|
|
|
toastManager.addTask({
|
|
|
|
|
id: taskId,
|
|
|
|
|
sessionID: activeSessionID,
|
|
|
|
|
description: args.description,
|
|
|
|
|
agent: agentToUse,
|
|
|
|
|
isBackground: false,
|
|
|
|
|
category: args.category,
|
|
|
|
|
skills: args.load_skills,
|
|
|
|
|
modelInfo,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (taskId) {
|
|
|
|
|
await publishSyncMetadata(activeSessionID, effectiveCategoryModel, taskId, spawnContext.childDepth)
|
|
|
|
|
}
|
2026-04-28 15:28:20 +09:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await deps.fetchSyncResult(client, activeSessionID)
|
2026-02-10 19:09:22 +09:00
|
|
|
if (!result.ok) {
|
|
|
|
|
return result.error
|
|
|
|
|
}
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-02-10 19:09:22 +09:00
|
|
|
const duration = formatDuration(startTime)
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-04-12 02:30:32 +09:00
|
|
|
const actualModelStr = effectiveCategoryModel
|
|
|
|
|
? `${effectiveCategoryModel.providerID}/${effectiveCategoryModel.modelID}`
|
2026-03-15 12:05:42 +08:00
|
|
|
: undefined
|
|
|
|
|
const parentModelStr = parentContext.model
|
|
|
|
|
? `${parentContext.model.providerID}/${parentContext.model.modelID}`
|
|
|
|
|
: undefined
|
2026-04-20 15:16:48 +09:00
|
|
|
let modelRoutingNote = ""
|
|
|
|
|
if (actualModelStr && parentModelStr && actualModelStr !== parentModelStr) {
|
|
|
|
|
modelRoutingNote = `\n⚠️ Model routing: parent used ${parentModelStr}, this subagent used ${actualModelStr} (via category: ${args.category ?? "unknown"})`
|
|
|
|
|
} else if (actualModelStr) {
|
|
|
|
|
modelRoutingNote = `\nModel: ${actualModelStr}${args.category ? ` (category: ${args.category})` : ""}`
|
|
|
|
|
}
|
2026-03-15 12:05:42 +08:00
|
|
|
|
2026-04-28 19:09:01 +09:00
|
|
|
await publishSyncMetadata(activeSessionID, effectiveCategoryModel, taskId!, spawnContext.childDepth)
|
2026-04-28 15:28:20 +09:00
|
|
|
|
2026-02-10 19:09:22 +09:00
|
|
|
return `Task completed in ${duration}.
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-03-15 12:05:42 +08:00
|
|
|
Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""}${modelRoutingNote}
|
2026-02-08 13:57:26 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
${result.textContent || "(No text output)"}
|
|
|
|
|
|
2026-04-16 23:13:44 +09:00
|
|
|
${buildTaskMetadataBlock({
|
2026-04-28 15:28:20 +09:00
|
|
|
sessionId: activeSessionID,
|
|
|
|
|
taskId: activeSessionID,
|
2026-04-16 23:13:44 +09:00
|
|
|
agent: agentToUse,
|
|
|
|
|
category: args.category,
|
|
|
|
|
})}`
|
2026-04-28 15:28:20 +09:00
|
|
|
}
|
2026-02-10 19:09:22 +09:00
|
|
|
} finally {
|
|
|
|
|
if (toastManager && taskId !== undefined) {
|
|
|
|
|
toastManager.removeTask(taskId)
|
|
|
|
|
}
|
2026-02-08 13:57:26 +09:00
|
|
|
}
|
2026-02-10 19:09:22 +09:00
|
|
|
} catch (error) {
|
2026-03-11 18:44:20 +09:00
|
|
|
spawnReservation?.rollback()
|
2026-02-08 13:57:26 +09:00
|
|
|
return formatDetailedError(error, {
|
|
|
|
|
operation: "Execute task",
|
|
|
|
|
args,
|
|
|
|
|
sessionID: syncSessionID,
|
|
|
|
|
agent: agentToUse,
|
|
|
|
|
category: args.category,
|
|
|
|
|
})
|
2026-02-11 00:43:43 +09:00
|
|
|
} finally {
|
|
|
|
|
if (syncSessionID) {
|
|
|
|
|
subagentSessions.delete(syncSessionID)
|
2026-02-19 04:41:00 +02:00
|
|
|
syncSubagentSessions.delete(syncSessionID)
|
2026-04-18 02:35:46 +09:00
|
|
|
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(syncSessionID)
|
2026-02-10 00:25:47 +09:00
|
|
|
SessionCategoryRegistry.remove(syncSessionID)
|
2026-02-11 00:43:43 +09:00
|
|
|
}
|
2026-02-08 13:57:26 +09:00
|
|
|
}
|
|
|
|
|
}
|