|
|
|
|
@@ -92,6 +92,7 @@ export class BackgroundManager {
|
|
|
|
|
private completionTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
|
|
|
private idleDeferralTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
|
|
|
private notificationQueueByParent: Map<string, Promise<void>> = new Map()
|
|
|
|
|
private enableParentSessionNotifications: boolean
|
|
|
|
|
readonly taskHistory = new TaskHistory()
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
@@ -101,6 +102,7 @@ export class BackgroundManager {
|
|
|
|
|
tmuxConfig?: TmuxConfig
|
|
|
|
|
onSubagentSessionCreated?: OnSubagentSessionCreated
|
|
|
|
|
onShutdown?: () => void
|
|
|
|
|
enableParentSessionNotifications?: boolean
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
this.tasks = new Map()
|
|
|
|
|
@@ -113,6 +115,7 @@ export class BackgroundManager {
|
|
|
|
|
this.tmuxEnabled = options?.tmuxConfig?.enabled ?? false
|
|
|
|
|
this.onSubagentSessionCreated = options?.onSubagentSessionCreated
|
|
|
|
|
this.onShutdown = options?.onShutdown
|
|
|
|
|
this.enableParentSessionNotifications = options?.enableParentSessionNotifications ?? true
|
|
|
|
|
this.registerProcessCleanup()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1203,19 +1206,22 @@ export class BackgroundManager {
|
|
|
|
|
allComplete = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED"
|
|
|
|
|
const errorInfo = task.error ? `\n**Error:** ${task.error}` : ""
|
|
|
|
|
|
|
|
|
|
let notification: string
|
|
|
|
|
let completedTasks: BackgroundTask[] = []
|
|
|
|
|
if (allComplete) {
|
|
|
|
|
completedTasks = Array.from(this.tasks.values())
|
|
|
|
|
const completedTasks = allComplete
|
|
|
|
|
? Array.from(this.tasks.values())
|
|
|
|
|
.filter(t => t.parentSessionID === task.parentSessionID && t.status !== "running" && t.status !== "pending")
|
|
|
|
|
const completedTasksText = completedTasks
|
|
|
|
|
.map(t => `- \`${t.id}\`: ${t.description}`)
|
|
|
|
|
.join("\n")
|
|
|
|
|
: []
|
|
|
|
|
|
|
|
|
|
notification = `<system-reminder>
|
|
|
|
|
if (this.enableParentSessionNotifications) {
|
|
|
|
|
const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED"
|
|
|
|
|
const errorInfo = task.error ? `\n**Error:** ${task.error}` : ""
|
|
|
|
|
|
|
|
|
|
let notification: string
|
|
|
|
|
if (allComplete) {
|
|
|
|
|
const completedTasksText = completedTasks
|
|
|
|
|
.map(t => `- \`${t.id}\`: ${t.description}`)
|
|
|
|
|
.join("\n")
|
|
|
|
|
|
|
|
|
|
notification = `<system-reminder>
|
|
|
|
|
[ALL BACKGROUND TASKS COMPLETE]
|
|
|
|
|
|
|
|
|
|
**Completed:**
|
|
|
|
|
@@ -1223,9 +1229,9 @@ ${completedTasksText || `- \`${task.id}\`: ${task.description}`}
|
|
|
|
|
|
|
|
|
|
Use \`background_output(task_id="<id>")\` to retrieve each result.
|
|
|
|
|
</system-reminder>`
|
|
|
|
|
} else {
|
|
|
|
|
// Individual completion - silent notification
|
|
|
|
|
notification = `<system-reminder>
|
|
|
|
|
} else {
|
|
|
|
|
// Individual completion - silent notification
|
|
|
|
|
notification = `<system-reminder>
|
|
|
|
|
[BACKGROUND TASK ${statusText}]
|
|
|
|
|
**ID:** \`${task.id}\`
|
|
|
|
|
**Description:** ${task.description}
|
|
|
|
|
@@ -1236,70 +1242,76 @@ Do NOT poll - continue productive work.
|
|
|
|
|
|
|
|
|
|
Use \`background_output(task_id="${task.id}")\` to retrieve this result when ready.
|
|
|
|
|
</system-reminder>`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let agent: string | undefined = task.parentAgent
|
|
|
|
|
let model: { providerID: string; modelID: string } | undefined
|
|
|
|
|
let agent: string | undefined = task.parentAgent
|
|
|
|
|
let model: { providerID: string; modelID: string } | undefined
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } })
|
|
|
|
|
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
|
|
|
|
|
info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string }
|
|
|
|
|
}>)
|
|
|
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
|
|
|
const info = messages[i].info
|
|
|
|
|
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
|
|
|
|
agent = info.agent ?? task.parentAgent
|
|
|
|
|
model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined)
|
|
|
|
|
break
|
|
|
|
|
try {
|
|
|
|
|
const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } })
|
|
|
|
|
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
|
|
|
|
|
info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string }
|
|
|
|
|
}>)
|
|
|
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
|
|
|
const info = messages[i].info
|
|
|
|
|
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
|
|
|
|
agent = info.agent ?? task.parentAgent
|
|
|
|
|
model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (this.isAbortedSessionError(error)) {
|
|
|
|
|
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
parentSessionID: task.parentSessionID,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const messageDir = getMessageDir(task.parentSessionID)
|
|
|
|
|
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
|
|
|
|
agent = currentMessage?.agent ?? task.parentAgent
|
|
|
|
|
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
|
|
|
|
|
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log("[background-agent] notifyParentSession context:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
resolvedAgent: agent,
|
|
|
|
|
resolvedModel: model,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.client.session.promptAsync({
|
|
|
|
|
path: { id: task.parentSessionID },
|
|
|
|
|
body: {
|
|
|
|
|
noReply: !allComplete,
|
|
|
|
|
...(agent !== undefined ? { agent } : {}),
|
|
|
|
|
...(model !== undefined ? { model } : {}),
|
|
|
|
|
...(task.parentTools ? { tools: task.parentTools } : {}),
|
|
|
|
|
parts: [{ type: "text", text: notification }],
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
log("[background-agent] Sent notification to parent session:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
allComplete,
|
|
|
|
|
noReply: !allComplete,
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (this.isAbortedSessionError(error)) {
|
|
|
|
|
log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
parentSessionID: task.parentSessionID,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
log("[background-agent] Failed to send notification:", error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (this.isAbortedSessionError(error)) {
|
|
|
|
|
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
parentSessionID: task.parentSessionID,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const messageDir = getMessageDir(task.parentSessionID)
|
|
|
|
|
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
|
|
|
|
agent = currentMessage?.agent ?? task.parentAgent
|
|
|
|
|
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
|
|
|
|
|
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log("[background-agent] notifyParentSession context:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
resolvedAgent: agent,
|
|
|
|
|
resolvedModel: model,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.client.session.promptAsync({
|
|
|
|
|
path: { id: task.parentSessionID },
|
|
|
|
|
body: {
|
|
|
|
|
noReply: !allComplete,
|
|
|
|
|
...(agent !== undefined ? { agent } : {}),
|
|
|
|
|
...(model !== undefined ? { model } : {}),
|
|
|
|
|
...(task.parentTools ? { tools: task.parentTools } : {}),
|
|
|
|
|
parts: [{ type: "text", text: notification }],
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
log("[background-agent] Sent notification to parent session:", {
|
|
|
|
|
} else {
|
|
|
|
|
log("[background-agent] Parent session notifications disabled, skipping prompt injection:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
allComplete,
|
|
|
|
|
noReply: !allComplete,
|
|
|
|
|
parentSessionID: task.parentSessionID,
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (this.isAbortedSessionError(error)) {
|
|
|
|
|
log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", {
|
|
|
|
|
taskId: task.id,
|
|
|
|
|
parentSessionID: task.parentSessionID,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
log("[background-agent] Failed to send notification:", error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (allComplete) {
|
|
|
|
|
|