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"
|
|
|
|
|
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
2026-04-05 15:30:36 +09:00
|
|
|
import { resolveCallID } from "./resolve-call-id"
|
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-02-20 00:02:17 +02:00
|
|
|
import { setSessionFallbackChain, clearSessionFallbackChain } from "../../hooks/model-fallback/hook"
|
2026-04-12 02:30:32 +09:00
|
|
|
import { retrySyncPromptWithFallbacks } from "./sync-task-fallback"
|
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-07 19:56:51 +09:00
|
|
|
// Depth/descendant guard. We must NOT silently fall back to childDepth: 1
|
|
|
|
|
// when the manager is unavailable or lacks the spawn methods, because that
|
|
|
|
|
// would let subagents recurse without bound. The only safe fallback is
|
|
|
|
|
// when the manager genuinely cannot enforce limits (legacy SDK), in which
|
|
|
|
|
// case we still record childDepth: 1 but log a warning so regressions are
|
|
|
|
|
// visible.
|
|
|
|
|
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). " +
|
|
|
|
|
"Depth and descendant limits cannot be enforced for this task. This indicates an old SDK or a misconfiguration.",
|
|
|
|
|
{ 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
|
|
|
|
|
subagentSessions.add(sessionID)
|
2026-02-19 04:41:00 +02:00
|
|
|
syncSubagentSessions.add(sessionID)
|
|
|
|
|
setSessionAgent(sessionID, agentToUse)
|
2026-02-20 00:02:17 +02:00
|
|
|
setSessionFallbackChain(sessionID, fallbackChain)
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-02-03 12:18:52 +09:00
|
|
|
if (args.category) {
|
|
|
|
|
SessionCategoryRegistry.register(sessionID, args.category)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 13:57:26 +09:00
|
|
|
if (onSyncSessionCreated) {
|
|
|
|
|
log("[task] Invoking onSyncSessionCreated callback", { sessionID, parentID: parentContext.sessionID })
|
|
|
|
|
await onSyncSessionCreated({
|
|
|
|
|
sessionID,
|
|
|
|
|
parentID: parentContext.sessionID,
|
|
|
|
|
title: args.description,
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
log("[task] onSyncSessionCreated callback failed", { error: String(err) })
|
|
|
|
|
})
|
|
|
|
|
await new Promise(r => setTimeout(r, 200))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const syncTaskMeta = {
|
|
|
|
|
title: args.description,
|
|
|
|
|
metadata: {
|
|
|
|
|
prompt: args.prompt,
|
|
|
|
|
agent: agentToUse,
|
|
|
|
|
category: args.category,
|
|
|
|
|
load_skills: args.load_skills,
|
|
|
|
|
description: args.description,
|
|
|
|
|
run_in_background: args.run_in_background,
|
|
|
|
|
sessionId: sessionID,
|
|
|
|
|
sync: true,
|
2026-03-11 17:46:04 +09:00
|
|
|
spawnDepth: spawnContext.childDepth,
|
2026-02-08 13:57:26 +09:00
|
|
|
command: args.command,
|
2026-03-04 16:35:06 +09:00
|
|
|
model: categoryModel ? { providerID: categoryModel.providerID, modelID: categoryModel.modelID } : undefined,
|
2026-02-08 13:57:26 +09:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
await ctx.metadata?.(syncTaskMeta)
|
2026-04-05 15:30:36 +09:00
|
|
|
const callID = resolveCallID(ctx)
|
|
|
|
|
if (callID) {
|
|
|
|
|
storeToolMetadata(ctx.sessionID, callID, syncTaskMeta)
|
2026-02-08 13:57:26 +09:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 02:30:32 +09:00
|
|
|
let effectiveCategoryModel = categoryModel
|
|
|
|
|
let promptError = await deps.sendSyncPrompt(client, {
|
2026-02-08 13:57:26 +09:00
|
|
|
sessionID,
|
|
|
|
|
agentToUse,
|
|
|
|
|
args,
|
|
|
|
|
systemContent,
|
2026-04-12 02:30:32 +09:00
|
|
|
categoryModel: effectiveCategoryModel,
|
2026-02-08 13:57:26 +09:00
|
|
|
toastManager,
|
|
|
|
|
taskId,
|
2026-03-28 09:34:58 -07:00
|
|
|
sisyphusAgentConfig: executorCtx.sisyphusAgentConfig,
|
2026-02-08 13:57:26 +09:00
|
|
|
})
|
|
|
|
|
if (promptError) {
|
2026-04-12 02:30:32 +09:00
|
|
|
const promptResult = await retrySyncPromptWithFallbacks({
|
|
|
|
|
sessionID,
|
|
|
|
|
initialError: promptError,
|
|
|
|
|
categoryModel: effectiveCategoryModel,
|
|
|
|
|
fallbackChain,
|
|
|
|
|
sendPrompt: async (fallbackModel) => {
|
|
|
|
|
return deps.sendSyncPrompt(client, {
|
|
|
|
|
sessionID,
|
|
|
|
|
agentToUse,
|
|
|
|
|
args,
|
|
|
|
|
systemContent,
|
|
|
|
|
categoryModel: fallbackModel,
|
|
|
|
|
toastManager,
|
|
|
|
|
taskId,
|
|
|
|
|
sisyphusAgentConfig: executorCtx.sisyphusAgentConfig,
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
promptError = promptResult.promptError
|
|
|
|
|
effectiveCategoryModel = promptResult.categoryModel
|
|
|
|
|
|
|
|
|
|
if (promptError) {
|
|
|
|
|
return promptError
|
|
|
|
|
}
|
2026-02-08 13:57:26 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 19:09:22 +09:00
|
|
|
try {
|
2026-02-10 22:54:30 +09:00
|
|
|
const pollError = await deps.pollSyncSession(ctx, client, {
|
2026-02-10 19:09:22 +09:00
|
|
|
sessionID,
|
|
|
|
|
agentToUse,
|
|
|
|
|
toastManager,
|
|
|
|
|
taskId,
|
2026-02-27 13:54:50 +08:00
|
|
|
}, syncPollTimeoutMs)
|
2026-02-10 19:09:22 +09:00
|
|
|
if (pollError) {
|
|
|
|
|
return pollError
|
|
|
|
|
}
|
2026-02-08 13:57:26 +09:00
|
|
|
|
2026-02-10 22:54:30 +09:00
|
|
|
const result = await deps.fetchSyncResult(client, sessionID)
|
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-03-15 12:05:42 +08:00
|
|
|
// 检测模型路由是否与父 session 不同,给用户可见的提示
|
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
|
|
|
|
|
const modelRoutingNote =
|
|
|
|
|
actualModelStr && parentModelStr && actualModelStr !== parentModelStr
|
|
|
|
|
? `\n⚠️ Model routing: parent used ${parentModelStr}, this subagent used ${actualModelStr} (via category: ${args.category ?? "unknown"})`
|
|
|
|
|
: actualModelStr
|
|
|
|
|
? `\nModel: ${actualModelStr}${args.category ? ` (category: ${args.category})` : ""}`
|
|
|
|
|
: ""
|
|
|
|
|
|
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)"}
|
|
|
|
|
|
|
|
|
|
<task_metadata>
|
|
|
|
|
session_id: ${sessionID}
|
|
|
|
|
</task_metadata>`
|
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-02-20 00:02:17 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|