{"version":3,"file":"herdr-status.d.ts","sourceRoot":"","sources":["../../../src/integrations/herdr-status.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,uBAAuB;IACvC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC9E,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACxC,MAAM,EAAE,uBAAuB,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,QAAQ,CAAC,cAAc,CAAC,CAAC;IACzC,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QACR,WAAW,EAAE,OAAO,WAAW,CAAC;QAChC,aAAa,EAAE,OAAO,aAAa,CAAC;KACpC,CAAC;CACF;AAED,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,cAAc,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IAChF,YAAY,IAAI,IAAI,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CAChB;AAoCD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAsQ9F","sourcesContent":["import {\n\tSUBAGENT_ASYNC_COMPLETE_EVENT,\n\tSUBAGENT_ASYNC_STARTED_EVENT,\n\tSUBAGENT_CONTROL_EVENT,\n} from \"../shared/types.ts\";\n\nconst DEFAULT_SOURCE = \"pi-subagents:herdr\";\nconst DEFAULT_TTL_MS = 120_000;\nconst DEFAULT_REFRESH_MS = 45_000;\n\nlet metadataReportSeq = Date.now() * 1000;\n\nfunction nextMetadataReportSeq(): number {\n\tmetadataReportSeq = Math.max(metadataReportSeq + 1, Date.now() * 1000);\n\treturn metadataReportSeq;\n}\n\nexport interface HerdrStatusBridgeEvents {\n\ton(event: string, handler: (data: unknown) => void): (() => void) | undefined;\n\temit(event: string, data: unknown): void;\n}\n\nexport interface HerdrStatusRun {\n\tid: string;\n\tagent?: string;\n\tagents?: string[];\n\tneedsAttention?: boolean;\n\tattentionLabel?: string;\n}\n\nexport interface HerdrStatusBridgeOptions {\n\tevents: HerdrStatusBridgeEvents;\n\tenv?: Record<string, string | undefined>;\n\t/** Current authoritative active-run projection, used before TTL refresh. */\n\tgetRuns?: () => Iterable<HerdrStatusRun>;\n\trunHerdr: (args: readonly string[]) => void | Promise<void>;\n\tttlMs?: number;\n\trefreshMs?: number;\n\ttimers?: {\n\t\tsetInterval: typeof setInterval;\n\t\tclearInterval: typeof clearInterval;\n\t};\n}\n\nexport interface HerdrStatusBridge {\n\t/**\n\t * Binds the pane owner. Only the root interactive session may publish pane\n\t * metadata: headless parents (print/json), non-UI harnesses, and child\n\t * runtimes must never fight the pane's lifecycle authority over display\n\t * state. Also re-syncs runs that survived a reload/resume.\n\t */\n\tsessionStarted(input: { hasUI: boolean; runs: Iterable<HerdrStatusRun> }): void;\n\tagentStarted(): void;\n\tflush(): Promise<void>;\n\tdispose(): void;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction startedRun(data: unknown): HerdrStatusRun | undefined {\n\tif (!isRecord(data) || typeof data.id !== \"string\" || !data.id) return undefined;\n\treturn {\n\t\tid: data.id,\n\t\t...(typeof data.agent === \"string\" ? { agent: data.agent } : {}),\n\t\t...(Array.isArray(data.agents) && data.agents.every((agent) => typeof agent === \"string\")\n\t\t\t? { agents: data.agents as string[] }\n\t\t\t: {}),\n\t};\n}\n\nfunction completedRunId(data: unknown): string | undefined {\n\tif (!isRecord(data)) return undefined;\n\tconst id = typeof data.runId === \"string\" ? data.runId : data.id;\n\treturn typeof id === \"string\" && id.length > 0 ? id : undefined;\n}\n\nfunction attentionNotice(data: unknown): { runId: string; label: string } | undefined {\n\tif (!isRecord(data) || data.source !== \"async\" || !isRecord(data.event)) return undefined;\n\tif (data.event.type !== \"needs_attention\" || typeof data.event.runId !== \"string\" || !data.event.runId)\n\t\treturn undefined;\n\tconst label =\n\t\ttypeof data.noticeText === \"string\" && data.noticeText\n\t\t\t? data.noticeText\n\t\t\t: typeof data.event.message === \"string\" && data.event.message\n\t\t\t\t? data.event.message\n\t\t\t\t: \"subagent needs attention\";\n\treturn { runId: data.event.runId, label };\n}\n\nexport function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): HerdrStatusBridge {\n\tconst env = options.env ?? process.env;\n\tconst paneId = env.HERDR_PANE_ID;\n\tconst enabled = env.HERDR_ENV === \"1\" && typeof paneId === \"string\" && paneId.length > 0;\n\tconst runHerdr = options.runHerdr;\n\tconst ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;\n\tconst refreshMs = options.refreshMs ?? DEFAULT_REFRESH_MS;\n\tconst timers = options.timers ?? { setInterval, clearInterval };\n\tconst runs = new Map<string, HerdrStatusRun>();\n\tconst attentionLabels = new Map<string, string>();\n\tconst acknowledgedAttention = new Set<string>();\n\tconst subscriptions: Array<() => void> = [];\n\tlet rootSession = false;\n\tlet published = false;\n\tlet busyRaised = false;\n\tlet busyLabel: string | undefined;\n\tlet blockedRaised = false;\n\tlet blockedLabel: string | undefined;\n\tlet disposed = false;\n\tlet pendingReport: readonly string[] | undefined;\n\tlet draining = false;\n\tlet drainPromise = Promise.resolve();\n\tlet refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n\tconst label = (): string => {\n\t\tconst agents = [\n\t\t\t...new Set(\n\t\t\t\t[...runs.values()].flatMap((run) => (run.agents?.length ? run.agents : run.agent ? [run.agent] : [])),\n\t\t\t),\n\t\t];\n\t\tconst who = agents.length > 0 ? ` (${agents.slice(0, 3).join(\", \")}${agents.length > 3 ? \", …\" : \"\"})` : \"\";\n\t\treturn `⏳ ${runs.size} subagent${runs.size === 1 ? \"\" : \"s\"}${who}`;\n\t};\n\n\tconst enqueue = (args: readonly string[]): void => {\n\t\tpendingReport = args;\n\t\tif (draining) return;\n\t\tdraining = true;\n\t\tdrainPromise = (async () => {\n\t\t\twhile (pendingReport) {\n\t\t\t\tconst next = pendingReport;\n\t\t\t\tpendingReport = undefined;\n\t\t\t\ttry {\n\t\t\t\t\tawait runHerdr(next);\n\t\t\t\t} catch {\n\t\t\t\t\t// Herdr integration is best effort; a later state transition or TTL\n\t\t\t\t\t// refresh retries with the newest desired snapshot.\n\t\t\t\t}\n\t\t\t}\n\t\t})().finally(() => {\n\t\t\tdraining = false;\n\t\t});\n\t};\n\n\tconst publish = (): void => {\n\t\tif (!enabled || !rootSession || disposed || !paneId) return;\n\t\tif (runs.size === 0 && !published) return;\n\t\tconst seq = String(nextMetadataReportSeq());\n\t\tif (runs.size === 0) {\n\t\t\tpublished = false;\n\t\t\tenqueue([\n\t\t\t\t\"pane\",\n\t\t\t\t\"report-metadata\",\n\t\t\t\tpaneId,\n\t\t\t\t\"--source\",\n\t\t\t\tDEFAULT_SOURCE,\n\t\t\t\t\"--agent\",\n\t\t\t\t\"pi\",\n\t\t\t\t\"--applies-to-source\",\n\t\t\t\t\"herdr:pi\",\n\t\t\t\t\"--clear-state-labels\",\n\t\t\t\t\"--clear-token\",\n\t\t\t\t\"summary\",\n\t\t\t\t\"--seq\",\n\t\t\t\tseq,\n\t\t\t]);\n\t\t\treturn;\n\t\t}\n\t\tconst text = label();\n\t\tpublished = true;\n\t\tenqueue([\n\t\t\t\"pane\",\n\t\t\t\"report-metadata\",\n\t\t\tpaneId,\n\t\t\t\"--source\",\n\t\t\tDEFAULT_SOURCE,\n\t\t\t\"--agent\",\n\t\t\t\"pi\",\n\t\t\t\"--applies-to-source\",\n\t\t\t\"herdr:pi\",\n\t\t\t\"--state-label\",\n\t\t\t`idle=${text}`,\n\t\t\t\"--state-label\",\n\t\t\t`done=${text}`,\n\t\t\t\"--state-label\",\n\t\t\t`working=${text}`,\n\t\t\t\"--token\",\n\t\t\t`summary=${text}`,\n\t\t\t\"--ttl-ms\",\n\t\t\tString(ttlMs),\n\t\t\t\"--seq\",\n\t\t\tseq,\n\t\t]);\n\t};\n\n\tconst syncRefreshTimer = (): void => {\n\t\tif (runs.size > 0 && refreshMs > 0 && !refreshTimer) {\n\t\t\trefreshTimer = timers.setInterval(() => refresh(), refreshMs);\n\t\t\trefreshTimer.unref?.();\n\t\t} else if ((runs.size === 0 || refreshMs <= 0) && refreshTimer) {\n\t\t\ttimers.clearInterval(refreshTimer);\n\t\t\trefreshTimer = undefined;\n\t\t}\n\t};\n\n\tconst syncBusy = (): void => {\n\t\tif (!enabled || !rootSession || disposed) return;\n\t\tif (runs.size > 0) {\n\t\t\tconst text = label();\n\t\t\tif (busyRaised && busyLabel === text) return;\n\t\t\tif (busyRaised) options.events.emit(\"herdr:busy\", { active: false });\n\t\t\tbusyRaised = true;\n\t\t\tbusyLabel = text;\n\t\t\toptions.events.emit(\"herdr:busy\", { active: true, label: text });\n\t\t\treturn;\n\t\t}\n\t\tif (busyRaised) {\n\t\t\tbusyRaised = false;\n\t\t\tbusyLabel = undefined;\n\t\t\toptions.events.emit(\"herdr:busy\", { active: false });\n\t\t}\n\t};\n\n\tconst syncBlocked = (): void => {\n\t\tif (!enabled || !rootSession || disposed) return;\n\t\tconst nextLabel = [...attentionLabels.values()].at(-1);\n\t\tif (nextLabel !== undefined) {\n\t\t\tif (blockedRaised && blockedLabel === nextLabel) return;\n\t\t\t// Herdr's sibling overlay contract is counted. Lower before changing\n\t\t\t// the active label so this bridge continues to own exactly one count.\n\t\t\tif (blockedRaised) options.events.emit(\"herdr:blocked\", { active: false });\n\t\t\tblockedRaised = true;\n\t\t\tblockedLabel = nextLabel;\n\t\t\toptions.events.emit(\"herdr:blocked\", { active: true, label: nextLabel });\n\t\t\treturn;\n\t\t}\n\t\tif (blockedRaised) {\n\t\t\tblockedRaised = false;\n\t\t\tblockedLabel = undefined;\n\t\t\toptions.events.emit(\"herdr:blocked\", { active: false });\n\t\t}\n\t};\n\n\tconst clearAttention = (): void => {\n\t\tattentionLabels.clear();\n\t\tsyncBlocked();\n\t};\n\n\tconst raiseAttention = (runId: string, labelText: string): void => {\n\t\tif (!rootSession || attentionLabels.has(runId)) return;\n\t\tacknowledgedAttention.delete(runId);\n\t\tattentionLabels.set(runId, labelText);\n\t\tsyncBlocked();\n\t};\n\n\tconst replaceRuns = (nextRuns: Iterable<HerdrStatusRun>): void => {\n\t\tconst nextAttention = new Map<string, string>();\n\t\tconst activeIds = new Set<string>();\n\t\truns.clear();\n\t\tfor (const run of nextRuns) {\n\t\t\tif (!run || typeof run.id !== \"string\" || !run.id) continue;\n\t\t\tactiveIds.add(run.id);\n\t\t\truns.set(run.id, { ...run });\n\t\t\tif (!run.needsAttention) {\n\t\t\t\tacknowledgedAttention.delete(run.id);\n\t\t\t} else if (!acknowledgedAttention.has(run.id)) {\n\t\t\t\tnextAttention.set(run.id, run.attentionLabel || attentionLabels.get(run.id) || \"subagent needs attention\");\n\t\t\t}\n\t\t}\n\t\tfor (const id of acknowledgedAttention) {\n\t\t\tif (!activeIds.has(id)) acknowledgedAttention.delete(id);\n\t\t}\n\t\tattentionLabels.clear();\n\t\tfor (const [id, labelText] of nextAttention) attentionLabels.set(id, labelText);\n\t\tsyncBusy();\n\t\tsyncRefreshTimer();\n\t\tsyncBlocked();\n\t\tpublish();\n\t};\n\n\tconst refresh = (): void => {\n\t\tif (!options.getRuns) {\n\t\t\tpublish();\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\treplaceRuns(options.getRuns());\n\t\t} catch {\n\t\t\t// Keep the last known active projection and retry on the next refresh.\n\t\t\tpublish();\n\t\t}\n\t};\n\n\tconst subscribe = (event: string, handler: (data: unknown) => void): void => {\n\t\tconst unsubscribe = options.events.on(event, handler);\n\t\tif (typeof unsubscribe === \"function\") subscriptions.push(unsubscribe);\n\t};\n\n\tif (enabled) {\n\t\tsubscribe(SUBAGENT_ASYNC_STARTED_EVENT, (data) => {\n\t\t\tif (!rootSession) return;\n\t\t\tconst run = startedRun(data);\n\t\t\tif (!run) return;\n\t\t\tacknowledgedAttention.delete(run.id);\n\t\t\truns.set(run.id, run);\n\t\t\tsyncBusy();\n\t\t\tsyncRefreshTimer();\n\t\t\tpublish();\n\t\t});\n\t\tsubscribe(SUBAGENT_ASYNC_COMPLETE_EVENT, (data) => {\n\t\t\tif (!rootSession) return;\n\t\t\tconst id = completedRunId(data);\n\t\t\tif (!id || !runs.delete(id)) return;\n\t\t\tacknowledgedAttention.delete(id);\n\t\t\tif (attentionLabels.delete(id)) syncBlocked();\n\t\t\tsyncBusy();\n\t\t\tsyncRefreshTimer();\n\t\t\tpublish();\n\t\t});\n\t\tsubscribe(SUBAGENT_CONTROL_EVENT, (data) => {\n\t\t\tif (!rootSession) return;\n\t\t\tconst notice = attentionNotice(data);\n\t\t\tif (!notice || !runs.has(notice.runId)) return;\n\t\t\traiseAttention(notice.runId, notice.label);\n\t\t});\n\t}\n\n\treturn {\n\t\tagentStarted() {\n\t\t\tfor (const id of attentionLabels.keys()) acknowledgedAttention.add(id);\n\t\t\tclearAttention();\n\t\t},\n\t\tsessionStarted({ hasUI, runs: restoredRuns }) {\n\t\t\tif (!enabled || disposed || hasUI !== true) return;\n\t\t\trootSession = true;\n\t\t\treplaceRuns(restoredRuns);\n\t\t},\n\t\tasync flush() {\n\t\t\twhile (draining || pendingReport) await drainPromise;\n\t\t},\n\t\tdispose() {\n\t\t\tif (disposed) return;\n\t\t\tclearAttention();\n\t\t\tacknowledgedAttention.clear();\n\t\t\truns.clear();\n\t\t\tsyncBusy();\n\t\t\tsyncRefreshTimer();\n\t\t\tpublish();\n\t\t\tfor (const unsubscribe of subscriptions) unsubscribe();\n\t\t\tdisposed = true;\n\t\t},\n\t};\n}\n"]}