{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../../src/runs/background/notify.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Completion notification delivery.\n *\n * Async result files call this notifier directly and are deleted only after\n * `sendMessage()` accepts the notification. The event bus remains an\n * observation channel, not a delivery acknowledgement.\n */\n\nimport type { ExtensionAPI } from \"@lpb-work/pi-coding-agent\";\nimport {\n\ttype ParallelHandoffReference,\n\tSUBAGENT_ASYNC_COMPLETE_EVENT,\n\tSUBAGENT_FOREGROUND_COMPLETE_EVENT,\n\ttype SubagentState,\n} from \"../../shared/types.ts\";\nimport { isUnexplainedProcessSignal } from \"../shared/process-signal.ts\";\nimport {\n\ttype CompletionBatchConfig,\n\ttype CompletionBatcher,\n\tcreateCompletionBatcher,\n\tresolveCompletionBatchConfig,\n} from \"./completion-batcher.ts\";\nimport { buildCompletionKey, markSeenWithTtl } from \"./completion-dedupe.ts\";\n\nexport interface SubagentNotifyDetails {\n\tagent: string;\n\tstatus: \"completed\" | \"failed\" | \"paused\" | \"stopped\";\n\tsource?: \"async\" | \"foreground\";\n\ttaskInfo?: string;\n\tresultPreview: string;\n\tdurationMs?: number;\n\tsessionLabel?: string;\n\tsessionValue?: string;\n\thandoffPath?: string;\n}\n\nexport interface CompletionNotification {\n\t[key: string]: unknown;\n\tid?: string | null;\n\tsource?: \"async\" | \"foreground\";\n\tagent?: string | null;\n\tsuccess?: boolean;\n\tsummary?: string;\n\texitCode?: number;\n\tstate?: string;\n\tprocessSignal?: string | null;\n\tinterrupted?: boolean;\n\ttimedOut?: boolean;\n\tstopped?: boolean;\n\tturnBudgetExceeded?: boolean;\n\tresults?: Array<{\n\t\tstatus?: string;\n\t\tsuccess?: boolean;\n\t\texitCode?: number | null;\n\t\tprocessSignal?: string | null;\n\t\tinterrupted?: boolean;\n\t\ttimedOut?: boolean;\n\t\tstopped?: boolean;\n\t\tturnBudgetExceeded?: boolean;\n\t}>;\n\ttimestamp?: number;\n\tdurationMs?: number;\n\tcwd?: string;\n\tsessionFile?: string;\n\tshareUrl?: string;\n\tgistUrl?: string;\n\tshareError?: string;\n\ttaskIndex?: number;\n\ttotalTasks?: number;\n\tsessionId?: string | null;\n\ttriggerTurn?: boolean;\n\t/** True when an acknowledged grouped intercom relay already delivered this run. */\n\tintercomDelivered?: boolean;\n\tparallelHandoff?: ParallelHandoffReference;\n}\n\ninterface NotifyTimerApi {\n\tsetTimeout(handler: () => void, delayMs: number): unknown;\n\tclearTimeout(handle: unknown): void;\n}\n\nexport interface RegisterSubagentNotifyOptions {\n\tbatchConfig?: CompletionBatchConfig;\n\ttimers?: NotifyTimerApi;\n\tnow?: () => number;\n}\n\nexport interface CompletionNotifier {\n\tdeliver(result: CompletionNotification): Promise<boolean>;\n\tdispose(): void;\n}\n\nfunction formatSessionLine(details: SubagentNotifyDetails): string | undefined {\n\tif (!details.sessionValue) return undefined;\n\treturn details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;\n}\n\nexport function formatSingleCompletion(details: SubagentNotifyDetails): string {\n\tconst sessionLine = formatSessionLine(details);\n\tconst taskKind = details.source === \"foreground\" ? \"Detached foreground task\" : \"Background task\";\n\treturn [\n\t\t`${taskKind} ${details.status}: **${details.agent}**${details.taskInfo ?? \"\"}`,\n\t\t\"\",\n\t\tdetails.resultPreview.trim() ? details.resultPreview : \"(no output)\",\n\t\tdetails.handoffPath ? \"\" : undefined,\n\t\tdetails.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,\n\t\tsessionLine ? \"\" : undefined,\n\t\tsessionLine,\n\t]\n\t\t.filter((line) => line !== undefined)\n\t\t.join(\"\\n\");\n}\n\nexport function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {\n\tconst lines = content.split(\"\\n\");\n\tconst match = (lines[0] ?? \"\").match(\n\t\t/^(Background task|Detached foreground task) (completed|failed|paused|stopped): \\*\\*(.+?)\\*\\*(?:\\s+(\\([^)]*\\)))?$/,\n\t);\n\tif (!match) return undefined;\n\tconst body = lines.slice(2);\n\tlet sessionIndex = -1;\n\tfor (let i = body.length - 1; i >= 1; i--) {\n\t\tif (body[i - 1]?.trim() === \"\" && /^(Session|Session file|Session share error):\\s+/.test(body[i]!)) {\n\t\t\tsessionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;\n\tconst handoffIndex = body.findIndex((line) => line.startsWith(\"Parallel handoff: \"));\n\tconst metadataIndexes = [sessionIndex, handoffIndex].filter((index) => index >= 0);\n\tconst firstMetadataIndex = metadataIndexes.length ? Math.min(...metadataIndexes) : body.length;\n\tconst resultEnd =\n\t\tfirstMetadataIndex > 0 && body[firstMetadataIndex - 1]?.trim() === \"\"\n\t\t\t? firstMetadataIndex - 1\n\t\t\t: firstMetadataIndex;\n\tconst resultPreview = body.slice(0, resultEnd).join(\"\\n\").trim() || \"(no output)\";\n\tconst handoffPath = handoffIndex >= 0 ? body[handoffIndex]!.slice(\"Parallel handoff: \".length).trim() : undefined;\n\tlet sessionLabel: string | undefined;\n\tlet sessionValue: string | undefined;\n\tif (sessionLine) {\n\t\tconst separator = sessionLine.indexOf(\":\");\n\t\tsessionLabel = sessionLine.slice(0, separator).toLowerCase();\n\t\tsessionValue = sessionLine.slice(separator + 1).trim();\n\t}\n\treturn {\n\t\tagent: match[3]!,\n\t\tstatus: match[2] as SubagentNotifyDetails[\"status\"],\n\t\t...(match[1] === \"Detached foreground task\" ? { source: \"foreground\" as const } : {}),\n\t\t...(match[4] ? { taskInfo: match[4] } : {}),\n\t\tresultPreview,\n\t\t...(handoffPath ? { handoffPath } : {}),\n\t\t...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),\n\t};\n}\n\nexport function formatGroupedCompletion(details: SubagentNotifyDetails[]): string {\n\tconst header = `Background tasks completed (${details.length}): ${details.map((d) => `**${d.agent}**${d.taskInfo ?? \"\"}`).join(\", \")}`;\n\tconst blocks: string[] = [header, \"\"];\n\tfor (let index = 0; index < details.length; index++) {\n\t\tconst detail = details[index];\n\t\tif (!detail) continue;\n\t\tconst sessionLine = formatSessionLine(detail);\n\t\tblocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? \"\"}`);\n\t\tblocks.push(detail.resultPreview.trim() ? detail.resultPreview : \"(no output)\");\n\t\tif (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);\n\t\tif (sessionLine) blocks.push(sessionLine);\n\t\tblocks.push(\"\");\n\t}\n\treturn blocks.join(\"\\n\").trimEnd();\n}\n\ninterface PendingCompletion {\n\tkey: string;\n\tdetails: SubagentNotifyDetails;\n\ttriggerTurn: boolean;\n\tresolve(accepted: boolean): void;\n}\n\nfunction sendCompletion(pi: Pick<ExtensionAPI, \"sendMessage\">, items: PendingCompletion[]): boolean {\n\tif (items.length === 0) return true;\n\tconst details = items.map((item) => item.details);\n\tconst content = details.length === 1 ? formatSingleCompletion(details[0]!) : formatGroupedCompletion(details);\n\tconst display = details.some((detail) => detail.source === \"foreground\" || detail.status !== \"completed\");\n\ttry {\n\t\tpi.sendMessage(\n\t\t\t{\n\t\t\t\tcustomType: \"subagent-notify\",\n\t\t\t\tcontent,\n\t\t\t\tdisplay,\n\t\t\t},\n\t\t\t{ triggerTurn: items.some((item) => item.triggerTurn) },\n\t\t);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction completionBatchKey(result: CompletionNotification): string {\n\tconst sessionId = typeof result.sessionId === \"string\" ? result.sessionId.trim() : \"\";\n\tif (sessionId) return `session:${sessionId}`;\n\tconst cwd = typeof result.cwd === \"string\" ? result.cwd.trim() : \"\";\n\treturn cwd ? `cwd:${cwd}` : \"unknown\";\n}\n\nexport function buildCompletionDetails(result: CompletionNotification): SubagentNotifyDetails {\n\tconst agent = result.agent ?? \"unknown\";\n\tconst summary = typeof result.summary === \"string\" ? result.summary : \"\";\n\tconst stopped =\n\t\tresult.stopped === true ||\n\t\tresult.state === \"stopped\" ||\n\t\t(result.success !== true && result.exitCode !== 0 && isUnexplainedProcessSignal(result)) ||\n\t\tresult.results?.some(\n\t\t\t(child) =>\n\t\t\t\tchild.stopped === true ||\n\t\t\t\tchild.status === \"stopped\" ||\n\t\t\t\t(child.success !== true && child.exitCode !== 0 && isUnexplainedProcessSignal(child)),\n\t\t) === true;\n\tconst paused =\n\t\t!stopped &&\n\t\t!result.success &&\n\t\t(result.exitCode === 0 || result.state === \"paused\" || summary.startsWith(\"Paused after interrupt.\"));\n\tconst status = stopped ? \"stopped\" : paused ? \"paused\" : result.success ? \"completed\" : \"failed\";\n\tconst taskInfo =\n\t\tresult.taskIndex !== undefined && result.totalTasks !== undefined\n\t\t\t? ` (${result.taskIndex + 1}/${result.totalTasks})`\n\t\t\t: undefined;\n\n\tconst parallelHandoff =\n\t\tresult.parallelHandoff && typeof result.parallelHandoff === \"object\"\n\t\t\t? (result.parallelHandoff as { path?: unknown })\n\t\t\t: undefined;\n\tconst handoffPath = typeof parallelHandoff?.path === \"string\" ? parallelHandoff.path : undefined;\n\tconst session = result.shareUrl\n\t\t? { label: \"Session\", value: result.shareUrl }\n\t\t: result.shareError\n\t\t\t? { label: \"Session share error\", value: result.shareError }\n\t\t\t: result.sessionFile\n\t\t\t\t? { label: \"Session file\", value: result.sessionFile }\n\t\t\t\t: undefined;\n\treturn {\n\t\tagent,\n\t\tstatus,\n\t\t...(result.source ? { source: result.source } : {}),\n\t\t...(taskInfo ? { taskInfo } : {}),\n\t\tresultPreview: summary,\n\t\t...(typeof result.durationMs === \"number\" ? { durationMs: result.durationMs } : {}),\n\t\t...(handoffPath ? { handoffPath } : {}),\n\t\t...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),\n\t};\n}\n\nexport default function registerSubagentNotify(\n\tpi: ExtensionAPI,\n\tstate: Pick<SubagentState, \"currentSessionId\">,\n\toptions: RegisterSubagentNotifyOptions = {},\n): CompletionNotifier {\n\tconst seen = new Map<string, number>();\n\tconst pending = new Map<string, Promise<boolean>>();\n\tconst ttlMs = 10 * 60 * 1000;\n\tconst now = options.now ?? Date.now;\n\tconst batchConfig = resolveCompletionBatchConfig(options.batchConfig);\n\tconst batchers = new Map<string, CompletionBatcher<PendingCompletion>>();\n\tlet disposed = false;\n\n\tconst settle = (items: PendingCompletion[], accepted: boolean) => {\n\t\tfor (const item of items) {\n\t\t\tpending.delete(item.key);\n\t\t\tif (accepted) markSeenWithTtl(seen, item.key, now(), ttlMs);\n\t\t\titem.resolve(accepted);\n\t\t}\n\t};\n\tconst emit = (items: PendingCompletion[]) => settle(items, sendCompletion(pi, items));\n\tconst getBatcher = (result: CompletionNotification) => {\n\t\tconst key = completionBatchKey(result);\n\t\tlet batcher = batchers.get(key);\n\t\tif (!batcher) {\n\t\t\tbatcher = createCompletionBatcher<PendingCompletion>({\n\t\t\t\tconfig: batchConfig,\n\t\t\t\temit,\n\t\t\t\t...(options.timers ? { timers: options.timers } : {}),\n\t\t\t\tnow,\n\t\t\t});\n\t\t\tbatchers.set(key, batcher);\n\t\t}\n\t\treturn batcher;\n\t};\n\n\tconst deliver = (result: CompletionNotification): Promise<boolean> => {\n\t\tif (disposed || typeof result.sessionId !== \"string\" || result.sessionId !== state.currentSessionId)\n\t\t\treturn Promise.resolve(false);\n\t\tif (result.intercomDelivered === true) return Promise.resolve(true);\n\t\tconst key = buildCompletionKey(result, \"notify\");\n\t\tconst seenAt = seen.get(key);\n\t\tif (seenAt !== undefined && now() - seenAt <= ttlMs) return Promise.resolve(true);\n\t\tif (seenAt !== undefined) seen.delete(key);\n\t\tconst inFlight = pending.get(key);\n\t\tif (inFlight) return inFlight;\n\t\tconst details = buildCompletionDetails(result);\n\t\tlet resolve!: (accepted: boolean) => void;\n\t\tconst completion = new Promise<boolean>((settleCompletion) => {\n\t\t\tresolve = settleCompletion;\n\t\t});\n\t\tpending.set(key, completion);\n\t\tconst item: PendingCompletion = {\n\t\t\tkey,\n\t\t\tdetails,\n\t\t\ttriggerTurn: result.triggerTurn !== false,\n\t\t\tresolve,\n\t\t};\n\t\tif (details.source === \"foreground\") {\n\t\t\temit([item]);\n\t\t\treturn completion;\n\t\t}\n\t\tconst batcher = getBatcher(result);\n\t\tif (details.status !== \"completed\") {\n\t\t\tbatcher.flush();\n\t\t\temit([item]);\n\t\t\treturn completion;\n\t\t}\n\t\tbatcher.push(item);\n\t\treturn completion;\n\t};\n\n\tconst unsubscribeAsync = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, (data) => {\n\t\tvoid deliver(data as CompletionNotification);\n\t});\n\tconst unsubscribeForeground = pi.events.on(SUBAGENT_FOREGROUND_COMPLETE_EVENT, (data) => {\n\t\tvoid deliver(data as CompletionNotification);\n\t});\n\n\treturn {\n\t\tdeliver,\n\t\tdispose() {\n\t\t\tif (disposed) return;\n\t\t\tdisposed = true;\n\t\t\tfor (const batcher of batchers.values()) settle(batcher.dispose(), false);\n\t\t\tbatchers.clear();\n\t\t\tfor (const unsubscribe of [unsubscribeAsync, unsubscribeForeground]) {\n\t\t\t\ttry {\n\t\t\t\t\tunsubscribe?.();\n\t\t\t\t} catch {\n\t\t\t\t\t// The runtime is already shutting down; pending records stay on disk.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n}\n"]}