@agimon-ai/doompi-team 0.0.1-alpha.65 → 0.0.1-alpha.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/src/adapters/asyncJobTracker.cjs +1 -1
  2. package/dist/src/adapters/asyncJobTracker.cjs.map +1 -1
  3. package/dist/src/adapters/asyncJobTracker.mjs +1 -1
  4. package/dist/src/adapters/asyncJobTracker.mjs.map +1 -1
  5. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.cjs +1 -1
  6. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.cjs.map +1 -1
  7. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.d.cts.map +1 -1
  8. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.d.mts.map +1 -1
  9. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.mjs +6 -6
  10. package/dist/src/adapters/pi/extensions/subagentPromptRuntime.mjs.map +1 -1
  11. package/dist/src/adapters/pi/tui/fleetStatus.cjs +1 -1
  12. package/dist/src/adapters/pi/tui/fleetStatus.cjs.map +1 -1
  13. package/dist/src/adapters/pi/tui/fleetStatus.mjs +1 -1
  14. package/dist/src/adapters/pi/tui/fleetStatus.mjs.map +1 -1
  15. package/dist/src/adapters/pi/tui/register.cjs +1 -1
  16. package/dist/src/adapters/pi/tui/register.cjs.map +1 -1
  17. package/dist/src/adapters/pi/tui/register.mjs +1 -1
  18. package/dist/src/adapters/pi/tui/register.mjs.map +1 -1
  19. package/dist/src/adapters/process/childTranscript.cjs +2 -2
  20. package/dist/src/adapters/process/childTranscript.cjs.map +1 -1
  21. package/dist/src/adapters/process/childTranscript.mjs +3 -3
  22. package/dist/src/adapters/process/childTranscript.mjs.map +1 -1
  23. package/dist/src/adapters/runs/background/asyncExecution.cjs.map +1 -1
  24. package/dist/src/adapters/runs/background/asyncExecution.mjs.map +1 -1
  25. package/dist/src/adapters/runs/background/runnerExecution.cjs +1 -1
  26. package/dist/src/adapters/runs/background/runnerExecution.cjs.map +1 -1
  27. package/dist/src/adapters/runs/background/runnerExecution.mjs +1 -1
  28. package/dist/src/adapters/runs/background/runnerExecution.mjs.map +1 -1
  29. package/dist/src/services/webSubagentRuns.cjs +1 -1
  30. package/dist/src/services/webSubagentRuns.cjs.map +1 -1
  31. package/dist/src/services/webSubagentRuns.mjs +1 -1
  32. package/dist/src/services/webSubagentRuns.mjs.map +1 -1
  33. package/dist/src/types/webSubagents.cjs.map +1 -1
  34. package/dist/src/types/webSubagents.d.cts +1 -0
  35. package/dist/src/types/webSubagents.d.cts.map +1 -1
  36. package/dist/src/types/webSubagents.d.mts +1 -0
  37. package/dist/src/types/webSubagents.d.mts.map +1 -1
  38. package/dist/src/types/webSubagents.mjs.map +1 -1
  39. package/package.json +11 -9
  40. package/src/types/webSubagents.ts +1 -0
  41. package/src/web/components/AgentCatalogDrawer.tsx +10 -12
  42. package/src/web/components/AgentThreadPanel.tsx +8 -8
  43. package/src/web/components/AgentsActivitySection.tsx +5 -5
  44. package/src/web/components/LaunchAgentDialog.tsx +11 -13
  45. package/src/web/components/SubagentsPanel.tsx +16 -16
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./filesystem/paths.cjs"),n=require("./runs/background/statusWriter.cjs"),r=require("./statusReader.cjs");let i=require("node:fs");i=e.__toESM(i,1);let a=require("node:path");a=e.__toESM(a,1);const o=new Set([`complete`,`completed`,`failed`,`paused`,`stopped`]);function s(e,t){let n=e.list().map(e=>e.runId).filter(e=>e===t||e.startsWith(t)),r=n.find(e=>e===t);if(r)return r;if(n.length===1)return n[0];throw n.length===0?Error(`No current-session run found for '${t}'.`):Error(`Multiple current-session runs match '${t}'. Use a longer id.`)}const c=`__unscoped__`;function l(e){return a.join(t.currentRunsDir(),e,n.STATUS_FILE_NAME)}function u(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function d(e){return u(e)===`ENOENT`}const f=new Set([`starting`,`working`,`tool`,`waiting_for_reply`,`needs_attention`,`finalizing`,`active_long_running`]);function p(e){return typeof e==`string`&&f.has(e)?e:void 0}var m=class{scheduler;constructor(e){this.scheduler=e}pollIntervalMs=250;retentionMs=1e4;now(){return Date.now()}readFile(e){try{return i.readFileSync(e,`utf-8`)}catch(e){if(d(e))return;throw e}}async readFileAsync(e){try{return await i.promises.readFile(e,`utf-8`)}catch(e){if(d(e))return;throw e}}sessions=new Map;listeners=new Map;unregisterPoll;session(e){let t=this.sessions.get(e);return t||(t={jobs:new Map,terminalAt:new Map,handedOff:new Set},this.sessions.set(e,t)),t}invalidate(e){for(let t of this.listeners.get(e)??[])t()}forSession(e){if(!e.trim())throw Error(`Pi session identity is required to track subagent runs.`);return{track:t=>this.trackInSession(e,t),untrack:t=>this.untrackInSession(e,t),list:()=>this.listInSession(e),get:t=>this.getInSession(e,t),reset:()=>this.resetSession(e)}}parseStatus(e,t){if(t===void 0)return;let n=r.parseAsyncRunStatus(t,l(e));if(n.kind!==`ok`)return;let i=n.status;return{agent:i.agent,status:i.state,startedAt:i.startedAt,updatedAt:i.lastUpdate,error:i.error,activityState:p(i.activityState),attentionReason:i.attentionReason,runtime:i.runtime,tokens:i.tokens,currentTool:i.currentTool,toolCount:i.toolCount}}readStatus(e){return this.parseStatus(e,this.readFile(l(e)))}async readStatusAsync(e){return this.parseStatus(e,await this.readFileAsync(l(e)))}applyStatus(e,t,n){let r=this.sessions.get(e);if(!r)return!1;let i=r.jobs.get(t);if(n===void 0)return!i&&(r.jobs.set(t,{runId:t,status:void 0}),!0);let a={runId:t,...n};r.jobs.set(t,a);let s=i?.status!==void 0&&o.has(i.status),c=a.status!==void 0&&o.has(a.status);return c&&!s&&r.terminalAt.set(t,this.now()),c||(r.terminalAt.delete(t),r.handedOff.delete(t)),!i||i.status!==a.status||i.updatedAt!==a.updatedAt||i.tokens!==a.tokens||i.currentTool!==a.currentTool||i.toolCount!==a.toolCount}refresh(e,t){let n=this.applyStatus(e,t,this.readStatus(t));return n&&this.invalidate(e),n}async refreshAsync(e,t){let n=this.applyStatus(e,t,await this.readStatusAsync(t));return n&&this.invalidate(e),n}async run(){let e=!1,t=this.now();for(let[n,r]of this.sessions){for(let t of r.jobs.keys())await this.refreshAsync(n,t)&&(e=!0);for(let[i,a]of r.terminalAt)!r.handedOff.has(i)||t-a<=this.retentionMs||(r.jobs.delete(i),r.terminalAt.delete(i),r.handedOff.delete(i),this.invalidate(n),e=!0);r.jobs.size===0&&this.sessions.get(n)===r&&this.sessions.delete(n)}return e}track(e){this.trackInSession(c,e)}untrack(e){this.untrackInSession(c,e)}acknowledgeHandoff(e,t){this.acknowledgeHandoffInSession(e,t)}listBackgroundWork(e){let t=this.sessions.get(e);return t?[...t.jobs.values()].filter(e=>e.status===void 0||!o.has(e.status)||!t.handedOff.has(e.runId)):[]}subscribe(e,t){if(!e.trim())throw Error(`Pi session identity is required to subscribe to subagent runs.`);return this.subscribeInSession(e,t)}list(){return this.listInSession(c)}get(e){return this.getInSession(c,e)}reset(){this.resetSession(c)}trackInSession(e,t){this.session(e),this.refresh(e,t)}untrackInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.delete(t)??!1;n?.terminalAt.delete(t),n?.handedOff.delete(t),n?.jobs.size===0&&this.sessions.delete(e),r&&this.invalidate(e)}acknowledgeHandoffInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.get(t)?.status;n&&r&&o.has(r)&&!n.handedOff.has(t)&&(n.handedOff.add(t),this.invalidate(e))}subscribeInSession(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>{n.delete(t),n.size===0&&this.listeners.delete(e)}}listInSession(e){return[...this.sessions.get(e)?.jobs.values()??[]]}getInSession(e,t){return this.sessions.get(e)?.jobs.get(t)}resetSession(e){let t=(this.sessions.get(e)?.jobs.size??0)>0;this.sessions.delete(e),t&&this.invalidate(e)}start(){this.stop(),this.unregisterPoll=this.scheduler.register({id:`async-job-tracker`,intervalMs:this.pollIntervalMs,run:()=>this.run()})}stop(){this.unregisterPoll?.(),this.unregisterPoll=void 0,this.sessions.clear(),this.listeners.clear()}};exports.AsyncJobTracker=m,exports.TERMINAL_ASYNC_JOB_STATES=o,exports.resolveTrackedRunId=s;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./filesystem/paths.cjs"),n=require("./runs/background/statusWriter.cjs"),r=require("./statusReader.cjs");let i=require("node:fs");i=e.__toESM(i,1);let a=require("node:path");a=e.__toESM(a,1);const o=new Set([`complete`,`completed`,`failed`,`paused`,`stopped`]);function s(e,t){let n=e.list().map(e=>e.runId).filter(e=>e===t||e.startsWith(t)),r=n.find(e=>e===t);if(r)return r;if(n.length===1)return n[0];throw n.length===0?Error(`No current-session run found for '${t}'.`):Error(`Multiple current-session runs match '${t}'. Use a longer id.`)}const c=`__unscoped__`;function l(e){return a.join(t.currentRunsDir(),e,n.STATUS_FILE_NAME)}function u(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function d(e){return u(e)===`ENOENT`}const f=new Set([`starting`,`working`,`tool`,`waiting_for_reply`,`needs_attention`,`finalizing`,`active_long_running`]);function p(e){return typeof e==`string`&&f.has(e)?e:void 0}var m=class{scheduler;constructor(e){this.scheduler=e}pollIntervalMs=250;retentionMs=1e4;now(){return Date.now()}readFile(e){try{return i.readFileSync(e,`utf-8`)}catch(e){if(d(e))return;throw e}}async readFileAsync(e){try{return await i.promises.readFile(e,`utf-8`)}catch(e){if(d(e))return;throw e}}sessions=new Map;listeners=new Map;unregisterPoll;session(e){let t=this.sessions.get(e);return t||(t={jobs:new Map,terminalAt:new Map,handedOff:new Set},this.sessions.set(e,t)),t}invalidate(e){for(let t of this.listeners.get(e)??[])t()}forSession(e){if(!e.trim())throw Error(`Pi session identity is required to track subagent runs.`);return{track:t=>this.trackInSession(e,t),untrack:t=>this.untrackInSession(e,t),list:()=>this.listInSession(e),get:t=>this.getInSession(e,t),reset:()=>this.resetSession(e)}}parseStatus(e,t){if(t===void 0)return;let n=r.parseAsyncRunStatus(t,l(e));if(n.kind!==`ok`)return;let i=n.status;return{agent:i.agent,status:i.state,startedAt:i.startedAt,updatedAt:i.lastUpdate,error:i.error,activityState:p(i.activityState),attentionReason:i.attentionReason,runtime:i.runtime,tokens:i.tokens,cost:i.cost,currentTool:i.currentTool,toolCount:i.toolCount}}readStatus(e){return this.parseStatus(e,this.readFile(l(e)))}async readStatusAsync(e){return this.parseStatus(e,await this.readFileAsync(l(e)))}applyStatus(e,t,n){let r=this.sessions.get(e);if(!r)return!1;let i=r.jobs.get(t);if(n===void 0)return!i&&(r.jobs.set(t,{runId:t,status:void 0}),!0);let a={runId:t,...n};r.jobs.set(t,a);let s=i?.status!==void 0&&o.has(i.status),c=a.status!==void 0&&o.has(a.status);return c&&!s&&r.terminalAt.set(t,this.now()),c||(r.terminalAt.delete(t),r.handedOff.delete(t)),!i||i.status!==a.status||i.updatedAt!==a.updatedAt||i.tokens!==a.tokens||i.cost!==a.cost||i.currentTool!==a.currentTool||i.toolCount!==a.toolCount}refresh(e,t){let n=this.applyStatus(e,t,this.readStatus(t));return n&&this.invalidate(e),n}async refreshAsync(e,t){let n=this.applyStatus(e,t,await this.readStatusAsync(t));return n&&this.invalidate(e),n}async run(){let e=!1,t=this.now();for(let[n,r]of this.sessions){for(let t of r.jobs.keys())await this.refreshAsync(n,t)&&(e=!0);for(let[i,a]of r.terminalAt)!r.handedOff.has(i)||t-a<=this.retentionMs||(r.jobs.delete(i),r.terminalAt.delete(i),r.handedOff.delete(i),this.invalidate(n),e=!0);r.jobs.size===0&&this.sessions.get(n)===r&&this.sessions.delete(n)}return e}track(e){this.trackInSession(c,e)}untrack(e){this.untrackInSession(c,e)}acknowledgeHandoff(e,t){this.acknowledgeHandoffInSession(e,t)}listBackgroundWork(e){let t=this.sessions.get(e);return t?[...t.jobs.values()].filter(e=>e.status===void 0||!o.has(e.status)||!t.handedOff.has(e.runId)):[]}subscribe(e,t){if(!e.trim())throw Error(`Pi session identity is required to subscribe to subagent runs.`);return this.subscribeInSession(e,t)}list(){return this.listInSession(c)}get(e){return this.getInSession(c,e)}reset(){this.resetSession(c)}trackInSession(e,t){this.session(e),this.refresh(e,t)}untrackInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.delete(t)??!1;n?.terminalAt.delete(t),n?.handedOff.delete(t),n?.jobs.size===0&&this.sessions.delete(e),r&&this.invalidate(e)}acknowledgeHandoffInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.get(t)?.status;n&&r&&o.has(r)&&!n.handedOff.has(t)&&(n.handedOff.add(t),this.invalidate(e))}subscribeInSession(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>{n.delete(t),n.size===0&&this.listeners.delete(e)}}listInSession(e){return[...this.sessions.get(e)?.jobs.values()??[]]}getInSession(e,t){return this.sessions.get(e)?.jobs.get(t)}resetSession(e){let t=(this.sessions.get(e)?.jobs.size??0)>0;this.sessions.delete(e),t&&this.invalidate(e)}start(){this.stop(),this.unregisterPoll=this.scheduler.register({id:`async-job-tracker`,intervalMs:this.pollIntervalMs,run:()=>this.run()})}stop(){this.unregisterPoll?.(),this.unregisterPoll=void 0,this.sessions.clear(),this.listeners.clear()}};exports.AsyncJobTracker=m,exports.TERMINAL_ASYNC_JOB_STATES=o,exports.resolveTrackedRunId=s;
2
2
  //# sourceMappingURL=asyncJobTracker.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"asyncJobTracker.cjs","names":["path","currentRunsDir","STATUS_FILE_NAME","fs","parseAsyncRunStatus"],"sources":["../../../src/adapters/asyncJobTracker.ts"],"sourcesContent":["/**\n * Live, in-memory registry of the async runs this session currently cares\n * about, refreshed from `status.json` on a `PollScheduler` tick.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/asyncJobTracker.ts` is 590 lines\n * doing four different jobs: this registry, TUI widget rendering\n * (`renderWidget`/`widgetRenderKey`/`ctx.ui.requestRender`), forwarding\n * control events and steering notices by byte-range-tailing each run's\n * `events.jsonl`, and projecting nested/parallel-group run trees. Only the\n * registry is ported. The other three have no consumer in this package yet:\n * TUI rendering is `G8` (slash commands + TUI), and nested/chain/parallel run\n * modes are `G7`+ (this package is currently single-run async-only - see\n * `spawnHandshake.ts`'s header). Porting rendering or event-tailing logic\n * with nothing to render to or forward to would be untested, unreachable\n * code; it is deferred to whichever later port actually needs it, not\n * dropped silently - this note is where to look for it.\n *\n * DESIGN PATTERNS:\n * - The in-memory map is a pure CACHE, never authoritative on its own:\n * `status.json` (owned by `CoalescedStatusWriter` elsewhere) is the only\n * source of truth. `track()` reads it synchronously immediately, so a\n * caller that already knows a run's id sees *something* right away rather\n * than waiting out the first poll\n * - Restart survivability follows from the point above rather than a second\n * persistence layer: this package has not built session-scoped run\n * ownership yet (which ids \"belong\" to a restarted session is not\n * something this module can determine on its own - see the AVOID entry),\n * so recovery is the caller re-`track()`-ing whatever ids it still cares\n * about from ITS OWN durable record. The very next poll tick repopulates\n * that job's status straight from disk. There is no separate store to keep\n * in sync, and therefore no window where memory could diverge from disk\n * that a restart would not immediately close\n * - Registered with `PollScheduler`, not a private timer: this polls\n * potentially many jobs' status files on an unbounded, periodic cadence\n * with no low-latency requirement - the exact shape `PollScheduler` exists\n * for. Contrast `completionBatcher.ts`/`spawnHandshake.ts`, which own\n * their own timers because they are short-lived, single-shot, and need\n * sub-200ms responsiveness `PollScheduler`'s floor cannot give them; this\n * registry needs neither\n * - Retention-then-eviction happens on the SAME poll tick as the status\n * refresh, rather than the predecessor's per-job `setTimeout`\n * (`scheduleCleanup`): a terminal job is dropped only after its result was\n * safely handed off and `retentionMs` elapsed. Failed delivery therefore\n * keeps the tracker entry available for `ResultWatcher`'s later retry\n *\n * AVOID:\n * - Scanning `currentRunsDir()` to guess which run ids \"belong\" to this session on\n * startup. This package has no session-scoped run ownership concept yet;\n * inventing one here to answer \"what should I restore\" would be a guess,\n * not a fact this module has standing to assert. Restoration is the\n * caller's job (see the design note above)\n * - Growing `terminalStates` independently of `staleRunReconciler.ts`'s own\n * (unexported) copy; both must treat the same state strings as terminal,\n * or a job could be evicted here while the reconciler still thinks it is\n * in flight, or vice versa\n *\n * `activityState`/`attentionReason` (added for `subagentWait.ts`):\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and a `reason` (e.g. `'missing-deliverable'`), but returns it rather than\n * writing it anywhere - it has no standing to touch `status.json` itself (see\n * that module's header). Something upstream of this tracker - the run's own\n * runner main loop, not yet built - is responsible for folding that result\n * into `status.json` through `CoalescedStatusWriter`. This tracker's job is\n * only to read whatever ends up there, the same as every other field: it does\n * not compute attention, detect it, or decide when a run needs it.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { parseAsyncRunStatus } from './statusReader';\nimport { STATUS_FILE_NAME } from './runs/background/statusWriter';\nimport { currentRunsDir } from './filesystem/paths';\nimport type { PollSchedulerContract } from './pollScheduler';\nimport type { ActivityState } from '../types';\n\n/**\n * States that mean \"already finished\"; matches `staleRunReconciler.ts`'s\n * own (unexported) set. Exported so a caller outside this module (the slash\n * layer's poll-until-terminal helper) can recognize the same terminal set\n * `TrackedAsyncJob.status` uses, rather than declaring a third copy of this\n * literal.\n */\nexport const TERMINAL_ASYNC_JOB_STATES = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\n/** Minimum time a handed-off terminal job stays queryable before eviction. */\nconst DEFAULT_RETENTION_MS = 10_000;\n/** How often tracked jobs' status is refreshed from disk. */\nconst DEFAULT_POLL_INTERVAL_MS = 250;\n\nconst POLL_SUBSCRIBER_ID = 'async-job-tracker';\n\n/**\n * The minimum shape this module reads. A run's actual `status.json` carries\n * far more (this package has no shared status type yet - see\n * `staleRunReconciler.ts`'s `ReconcilableStatus` for the same caveat);\n * everything else is simply not read here.\n */\nexport interface TrackedAsyncJob {\n runId: string;\n /** Named agent executing this run. Absent until the first readable status. */\n agent?: string;\n /** `status.json`'s `state` field. `undefined` until the first successful read. */\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n /** `status.json`'s `activityState` field, e.g. `'needs_attention'`. See the module doc. */\n activityState?: ActivityState;\n /** `status.json`'s `reason` field, present alongside `activityState` (e.g. `'missing-deliverable'`). */\n attentionReason?: string;\n /** Which runtime is executing this run. Absent means the default `pi` path. */\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nexport type TrackedAsyncJobsContract = {\n /**\n * Start tracking `runId`: reads its current status synchronously (so a\n * caller sees something immediately) and adds it to the poll rotation.\n * Calling this again for an id already tracked simply re-reads it now,\n * the same idempotent-refresh idiom as `CoalescedStatusWriter.open()`.\n */\n track(runId: string): void;\n /** Stop tracking `runId` immediately, without waiting out its retention window. */\n untrack(runId: string): void;\n /** Every tracked job, including terminal jobs still awaiting a safe handoff. */\n list(): TrackedAsyncJob[];\n get(runId: string): TrackedAsyncJob | undefined;\n /** Drop every tracked job. */\n reset(): void;\n};\n\nexport type AsyncJobTrackerContract = TrackedAsyncJobsContract & {\n /** Return the isolated run collection owned by one Pi session context. */\n forSession(sessionId: string): TrackedAsyncJobsContract;\n /** Register with `PollScheduler`. Call once; a second call is a reset, same as `ResultWatcher.start()`. */\n start(): void;\n /** Unregister from `PollScheduler` and drop every tracked job. */\n stop(): void;\n};\n\n/** Resolve an exact id or unique prefix only inside one Pi session's run collection. */\nexport function resolveTrackedRunId(jobs: TrackedAsyncJobsContract, id: string): string {\n const matches = jobs\n .list()\n .map((job) => job.runId)\n .filter((runId) => runId === id || runId.startsWith(id));\n const exact = matches.find((runId) => runId === id);\n if (exact) return exact;\n if (matches.length === 1) return matches[0]!;\n if (matches.length === 0) throw new Error(`No current-session run found for '${id}'.`);\n throw new Error(`Multiple current-session runs match '${id}'. Use a longer id.`);\n}\n\ninterface SessionJobs {\n jobs: Map<string, TrackedAsyncJob>;\n terminalAt: Map<string, number>;\n handedOff: Set<string>;\n}\n\nconst UNSCOPED_SESSION = '__unscoped__';\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\ninterface ReadStatusResult {\n agent: string;\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n activityState?: ActivityState;\n attentionReason?: string;\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nconst VALID_ACTIVITY_STATES: ReadonlySet<string> = new Set<ActivityState>([\n 'starting',\n 'working',\n 'tool',\n 'waiting_for_reply',\n 'needs_attention',\n 'finalizing',\n 'active_long_running',\n]);\n\nfunction asActivityState(value: unknown): ActivityState | undefined {\n return typeof value === 'string' && VALID_ACTIVITY_STATES.has(value) ? (value as ActivityState) : undefined;\n}\n\nexport class AsyncJobTracker implements AsyncJobTrackerContract {\n constructor(private readonly scheduler: PollSchedulerContract) {}\n\n /**\n * Runtime tuning seams for tests, kept out of the dependency constructor.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly retentionMs: number = DEFAULT_RETENTION_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected async readFileAsync(filePath: string): Promise<string | undefined> {\n try {\n return await fs.promises.readFile(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n private readonly sessions = new Map<string, SessionJobs>();\n private readonly listeners = new Map<string, Set<() => void>>();\n private unregisterPoll: (() => void) | undefined;\n\n private session(sessionId: string): SessionJobs {\n let session = this.sessions.get(sessionId);\n if (!session) {\n session = { jobs: new Map(), terminalAt: new Map(), handedOff: new Set() };\n this.sessions.set(sessionId, session);\n }\n return session;\n }\n\n private invalidate(sessionId: string): void {\n for (const listener of this.listeners.get(sessionId) ?? []) listener();\n }\n\n forSession(sessionId: string): TrackedAsyncJobsContract {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to track subagent runs.');\n return {\n track: (runId) => this.trackInSession(sessionId, runId),\n untrack: (runId) => this.untrackInSession(sessionId, runId),\n list: () => this.listInSession(sessionId),\n get: (runId) => this.getInSession(sessionId, runId),\n reset: () => this.resetSession(sessionId),\n };\n }\n\n private parseStatus(runId: string, raw: string | undefined): ReadStatusResult | undefined {\n if (raw === undefined) return undefined;\n const result = parseAsyncRunStatus(raw, statusPathFor(runId));\n if (result.kind !== 'ok') return undefined;\n const status = result.status;\n return {\n agent: status.agent,\n status: status.state,\n startedAt: status.startedAt,\n updatedAt: status.lastUpdate,\n error: status.error,\n activityState: asActivityState(status.activityState),\n attentionReason: status.attentionReason,\n runtime: status.runtime,\n tokens: status.tokens,\n currentTool: status.currentTool,\n toolCount: status.toolCount,\n };\n }\n\n private readStatus(runId: string): ReadStatusResult | undefined {\n return this.parseStatus(runId, this.readFile(statusPathFor(runId)));\n }\n\n private async readStatusAsync(runId: string): Promise<ReadStatusResult | undefined> {\n return this.parseStatus(runId, await this.readFileAsync(statusPathFor(runId)));\n }\n\n private applyStatus(sessionId: string, runId: string, read: ReadStatusResult | undefined): boolean {\n // Teardown (`stop()`, `reset()`) can land between two of the tick's awaits.\n // A late refresh must observe that and drop its result, never recreate the\n // session it belonged to: resurrecting it here is what made `stop()` not stop.\n const session = this.sessions.get(sessionId);\n if (!session) return false;\n const existing = session.jobs.get(runId);\n if (read === undefined) {\n // A run may not have created its file yet, and a torn read must not blank\n // a previously known status for one polling pass.\n if (existing) return false;\n session.jobs.set(runId, { runId, status: undefined });\n return true;\n }\n\n const job: TrackedAsyncJob = { runId, ...read };\n session.jobs.set(runId, job);\n const wasTerminal = existing?.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(existing.status);\n const isTerminal = job.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(job.status);\n if (isTerminal && !wasTerminal) session.terminalAt.set(runId, this.now());\n if (!isTerminal) {\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n }\n return (\n !existing ||\n existing.status !== job.status ||\n existing.updatedAt !== job.updatedAt ||\n existing.tokens !== job.tokens ||\n existing.currentTool !== job.currentTool ||\n existing.toolCount !== job.toolCount\n );\n }\n\n private refresh(sessionId: string, runId: string): boolean {\n const changed = this.applyStatus(sessionId, runId, this.readStatus(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n private async refreshAsync(sessionId: string, runId: string): Promise<boolean> {\n const changed = this.applyStatus(sessionId, runId, await this.readStatusAsync(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n /** One scheduler tick: asynchronously refresh every tracked status, then evict expired terminal jobs. */\n protected async run(): Promise<boolean> {\n let changed = false;\n const now = this.now();\n for (const [sessionId, session] of this.sessions) {\n for (const runId of session.jobs.keys()) {\n if (await this.refreshAsync(sessionId, runId)) changed = true;\n }\n for (const [runId, terminatedAt] of session.terminalAt) {\n if (!session.handedOff.has(runId) || now - terminatedAt <= this.retentionMs) continue;\n session.jobs.delete(runId);\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n this.invalidate(sessionId);\n changed = true;\n }\n // Only drop the entry this pass actually walked: a `track()` that landed\n // during an await above may have installed a fresh one under the same id.\n if (session.jobs.size === 0 && this.sessions.get(sessionId) === session) this.sessions.delete(sessionId);\n }\n\n return changed;\n }\n\n track(runId: string): void {\n this.trackInSession(UNSCOPED_SESSION, runId);\n }\n\n untrack(runId: string): void {\n this.untrackInSession(UNSCOPED_SESSION, runId);\n }\n\n /** Mark one session's terminal result as safely handed off. */\n acknowledgeHandoff(sessionId: string, runId: string): void {\n this.acknowledgeHandoffInSession(sessionId, runId);\n }\n\n /** Work that still suppresses same-session idle continuation. */\n listBackgroundWork(sessionId: string): TrackedAsyncJob[] {\n const session = this.sessions.get(sessionId);\n if (!session) return [];\n return [...session.jobs.values()].filter(\n (job) =>\n job.status === undefined || !TERMINAL_ASYNC_JOB_STATES.has(job.status) || !session.handedOff.has(job.runId),\n );\n }\n\n /** Subscribe to invalidations for one exact Pi session. */\n subscribe(sessionId: string, listener: () => void): () => void {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to subscribe to subagent runs.');\n return this.subscribeInSession(sessionId, listener);\n }\n\n list(): TrackedAsyncJob[] {\n return this.listInSession(UNSCOPED_SESSION);\n }\n\n get(runId: string): TrackedAsyncJob | undefined {\n return this.getInSession(UNSCOPED_SESSION, runId);\n }\n\n reset(): void {\n this.resetSession(UNSCOPED_SESSION);\n }\n\n private trackInSession(sessionId: string, runId: string): void {\n // `applyStatus` no longer creates sessions (see its comment), so tracking is\n // the one path that does.\n this.session(sessionId);\n this.refresh(sessionId, runId);\n }\n\n private untrackInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const deleted = session?.jobs.delete(runId) ?? false;\n session?.terminalAt.delete(runId);\n session?.handedOff.delete(runId);\n if (session?.jobs.size === 0) this.sessions.delete(sessionId);\n if (deleted) this.invalidate(sessionId);\n }\n\n private acknowledgeHandoffInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const status = session?.jobs.get(runId)?.status;\n if (!session || !status || !TERMINAL_ASYNC_JOB_STATES.has(status) || session.handedOff.has(runId)) return;\n session.handedOff.add(runId);\n this.invalidate(sessionId);\n }\n\n private subscribeInSession(sessionId: string, listener: () => void): () => void {\n const listeners = this.listeners.get(sessionId) ?? new Set<() => void>();\n listeners.add(listener);\n this.listeners.set(sessionId, listeners);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) this.listeners.delete(sessionId);\n };\n }\n\n private listInSession(sessionId: string): TrackedAsyncJob[] {\n return [...(this.sessions.get(sessionId)?.jobs.values() ?? [])];\n }\n\n private getInSession(sessionId: string, runId: string): TrackedAsyncJob | undefined {\n return this.sessions.get(sessionId)?.jobs.get(runId);\n }\n\n private resetSession(sessionId: string): void {\n const hadJobs = (this.sessions.get(sessionId)?.jobs.size ?? 0) > 0;\n this.sessions.delete(sessionId);\n if (hadJobs) this.invalidate(sessionId);\n }\n\n start(): void {\n this.stop();\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n }\n\n stop(): void {\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.sessions.clear();\n this.listeners.clear();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"asyncJobTracker.cjs","names":["path","currentRunsDir","STATUS_FILE_NAME","fs","parseAsyncRunStatus"],"sources":["../../../src/adapters/asyncJobTracker.ts"],"sourcesContent":["/**\n * Live, in-memory registry of the async runs this session currently cares\n * about, refreshed from `status.json` on a `PollScheduler` tick.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/asyncJobTracker.ts` is 590 lines\n * doing four different jobs: this registry, TUI widget rendering\n * (`renderWidget`/`widgetRenderKey`/`ctx.ui.requestRender`), forwarding\n * control events and steering notices by byte-range-tailing each run's\n * `events.jsonl`, and projecting nested/parallel-group run trees. Only the\n * registry is ported. The other three have no consumer in this package yet:\n * TUI rendering is `G8` (slash commands + TUI), and nested/chain/parallel run\n * modes are `G7`+ (this package is currently single-run async-only - see\n * `spawnHandshake.ts`'s header). Porting rendering or event-tailing logic\n * with nothing to render to or forward to would be untested, unreachable\n * code; it is deferred to whichever later port actually needs it, not\n * dropped silently - this note is where to look for it.\n *\n * DESIGN PATTERNS:\n * - The in-memory map is a pure CACHE, never authoritative on its own:\n * `status.json` (owned by `CoalescedStatusWriter` elsewhere) is the only\n * source of truth. `track()` reads it synchronously immediately, so a\n * caller that already knows a run's id sees *something* right away rather\n * than waiting out the first poll\n * - Restart survivability follows from the point above rather than a second\n * persistence layer: this package has not built session-scoped run\n * ownership yet (which ids \"belong\" to a restarted session is not\n * something this module can determine on its own - see the AVOID entry),\n * so recovery is the caller re-`track()`-ing whatever ids it still cares\n * about from ITS OWN durable record. The very next poll tick repopulates\n * that job's status straight from disk. There is no separate store to keep\n * in sync, and therefore no window where memory could diverge from disk\n * that a restart would not immediately close\n * - Registered with `PollScheduler`, not a private timer: this polls\n * potentially many jobs' status files on an unbounded, periodic cadence\n * with no low-latency requirement - the exact shape `PollScheduler` exists\n * for. Contrast `completionBatcher.ts`/`spawnHandshake.ts`, which own\n * their own timers because they are short-lived, single-shot, and need\n * sub-200ms responsiveness `PollScheduler`'s floor cannot give them; this\n * registry needs neither\n * - Retention-then-eviction happens on the SAME poll tick as the status\n * refresh, rather than the predecessor's per-job `setTimeout`\n * (`scheduleCleanup`): a terminal job is dropped only after its result was\n * safely handed off and `retentionMs` elapsed. Failed delivery therefore\n * keeps the tracker entry available for `ResultWatcher`'s later retry\n *\n * AVOID:\n * - Scanning `currentRunsDir()` to guess which run ids \"belong\" to this session on\n * startup. This package has no session-scoped run ownership concept yet;\n * inventing one here to answer \"what should I restore\" would be a guess,\n * not a fact this module has standing to assert. Restoration is the\n * caller's job (see the design note above)\n * - Growing `terminalStates` independently of `staleRunReconciler.ts`'s own\n * (unexported) copy; both must treat the same state strings as terminal,\n * or a job could be evicted here while the reconciler still thinks it is\n * in flight, or vice versa\n *\n * `activityState`/`attentionReason` (added for `subagentWait.ts`):\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and a `reason` (e.g. `'missing-deliverable'`), but returns it rather than\n * writing it anywhere - it has no standing to touch `status.json` itself (see\n * that module's header). Something upstream of this tracker - the run's own\n * runner main loop, not yet built - is responsible for folding that result\n * into `status.json` through `CoalescedStatusWriter`. This tracker's job is\n * only to read whatever ends up there, the same as every other field: it does\n * not compute attention, detect it, or decide when a run needs it.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { parseAsyncRunStatus } from './statusReader';\nimport { STATUS_FILE_NAME } from './runs/background/statusWriter';\nimport { currentRunsDir } from './filesystem/paths';\nimport type { PollSchedulerContract } from './pollScheduler';\nimport type { ActivityState } from '../types';\n\n/**\n * States that mean \"already finished\"; matches `staleRunReconciler.ts`'s\n * own (unexported) set. Exported so a caller outside this module (the slash\n * layer's poll-until-terminal helper) can recognize the same terminal set\n * `TrackedAsyncJob.status` uses, rather than declaring a third copy of this\n * literal.\n */\nexport const TERMINAL_ASYNC_JOB_STATES = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\n/** Minimum time a handed-off terminal job stays queryable before eviction. */\nconst DEFAULT_RETENTION_MS = 10_000;\n/** How often tracked jobs' status is refreshed from disk. */\nconst DEFAULT_POLL_INTERVAL_MS = 250;\n\nconst POLL_SUBSCRIBER_ID = 'async-job-tracker';\n\n/**\n * The minimum shape this module reads. A run's actual `status.json` carries\n * far more (this package has no shared status type yet - see\n * `staleRunReconciler.ts`'s `ReconcilableStatus` for the same caveat);\n * everything else is simply not read here.\n */\nexport interface TrackedAsyncJob {\n runId: string;\n /** Named agent executing this run. Absent until the first readable status. */\n agent?: string;\n /** `status.json`'s `state` field. `undefined` until the first successful read. */\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n /** `status.json`'s `activityState` field, e.g. `'needs_attention'`. See the module doc. */\n activityState?: ActivityState;\n /** `status.json`'s `reason` field, present alongside `activityState` (e.g. `'missing-deliverable'`). */\n attentionReason?: string;\n /** Which runtime is executing this run. Absent means the default `pi` path. */\n runtime?: string;\n tokens?: number;\n cost?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nexport type TrackedAsyncJobsContract = {\n /**\n * Start tracking `runId`: reads its current status synchronously (so a\n * caller sees something immediately) and adds it to the poll rotation.\n * Calling this again for an id already tracked simply re-reads it now,\n * the same idempotent-refresh idiom as `CoalescedStatusWriter.open()`.\n */\n track(runId: string): void;\n /** Stop tracking `runId` immediately, without waiting out its retention window. */\n untrack(runId: string): void;\n /** Every tracked job, including terminal jobs still awaiting a safe handoff. */\n list(): TrackedAsyncJob[];\n get(runId: string): TrackedAsyncJob | undefined;\n /** Drop every tracked job. */\n reset(): void;\n};\n\nexport type AsyncJobTrackerContract = TrackedAsyncJobsContract & {\n /** Return the isolated run collection owned by one Pi session context. */\n forSession(sessionId: string): TrackedAsyncJobsContract;\n /** Register with `PollScheduler`. Call once; a second call is a reset, same as `ResultWatcher.start()`. */\n start(): void;\n /** Unregister from `PollScheduler` and drop every tracked job. */\n stop(): void;\n};\n\n/** Resolve an exact id or unique prefix only inside one Pi session's run collection. */\nexport function resolveTrackedRunId(jobs: TrackedAsyncJobsContract, id: string): string {\n const matches = jobs\n .list()\n .map((job) => job.runId)\n .filter((runId) => runId === id || runId.startsWith(id));\n const exact = matches.find((runId) => runId === id);\n if (exact) return exact;\n if (matches.length === 1) return matches[0]!;\n if (matches.length === 0) throw new Error(`No current-session run found for '${id}'.`);\n throw new Error(`Multiple current-session runs match '${id}'. Use a longer id.`);\n}\n\ninterface SessionJobs {\n jobs: Map<string, TrackedAsyncJob>;\n terminalAt: Map<string, number>;\n handedOff: Set<string>;\n}\n\nconst UNSCOPED_SESSION = '__unscoped__';\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\ninterface ReadStatusResult {\n agent: string;\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n activityState?: ActivityState;\n attentionReason?: string;\n runtime?: string;\n tokens?: number;\n cost?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nconst VALID_ACTIVITY_STATES: ReadonlySet<string> = new Set<ActivityState>([\n 'starting',\n 'working',\n 'tool',\n 'waiting_for_reply',\n 'needs_attention',\n 'finalizing',\n 'active_long_running',\n]);\n\nfunction asActivityState(value: unknown): ActivityState | undefined {\n return typeof value === 'string' && VALID_ACTIVITY_STATES.has(value) ? (value as ActivityState) : undefined;\n}\n\nexport class AsyncJobTracker implements AsyncJobTrackerContract {\n constructor(private readonly scheduler: PollSchedulerContract) {}\n\n /**\n * Runtime tuning seams for tests, kept out of the dependency constructor.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly retentionMs: number = DEFAULT_RETENTION_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected async readFileAsync(filePath: string): Promise<string | undefined> {\n try {\n return await fs.promises.readFile(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n private readonly sessions = new Map<string, SessionJobs>();\n private readonly listeners = new Map<string, Set<() => void>>();\n private unregisterPoll: (() => void) | undefined;\n\n private session(sessionId: string): SessionJobs {\n let session = this.sessions.get(sessionId);\n if (!session) {\n session = { jobs: new Map(), terminalAt: new Map(), handedOff: new Set() };\n this.sessions.set(sessionId, session);\n }\n return session;\n }\n\n private invalidate(sessionId: string): void {\n for (const listener of this.listeners.get(sessionId) ?? []) listener();\n }\n\n forSession(sessionId: string): TrackedAsyncJobsContract {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to track subagent runs.');\n return {\n track: (runId) => this.trackInSession(sessionId, runId),\n untrack: (runId) => this.untrackInSession(sessionId, runId),\n list: () => this.listInSession(sessionId),\n get: (runId) => this.getInSession(sessionId, runId),\n reset: () => this.resetSession(sessionId),\n };\n }\n\n private parseStatus(runId: string, raw: string | undefined): ReadStatusResult | undefined {\n if (raw === undefined) return undefined;\n const result = parseAsyncRunStatus(raw, statusPathFor(runId));\n if (result.kind !== 'ok') return undefined;\n const status = result.status;\n return {\n agent: status.agent,\n status: status.state,\n startedAt: status.startedAt,\n updatedAt: status.lastUpdate,\n error: status.error,\n activityState: asActivityState(status.activityState),\n attentionReason: status.attentionReason,\n runtime: status.runtime,\n tokens: status.tokens,\n cost: status.cost,\n currentTool: status.currentTool,\n toolCount: status.toolCount,\n };\n }\n\n private readStatus(runId: string): ReadStatusResult | undefined {\n return this.parseStatus(runId, this.readFile(statusPathFor(runId)));\n }\n\n private async readStatusAsync(runId: string): Promise<ReadStatusResult | undefined> {\n return this.parseStatus(runId, await this.readFileAsync(statusPathFor(runId)));\n }\n\n private applyStatus(sessionId: string, runId: string, read: ReadStatusResult | undefined): boolean {\n // Teardown (`stop()`, `reset()`) can land between two of the tick's awaits.\n // A late refresh must observe that and drop its result, never recreate the\n // session it belonged to: resurrecting it here is what made `stop()` not stop.\n const session = this.sessions.get(sessionId);\n if (!session) return false;\n const existing = session.jobs.get(runId);\n if (read === undefined) {\n // A run may not have created its file yet, and a torn read must not blank\n // a previously known status for one polling pass.\n if (existing) return false;\n session.jobs.set(runId, { runId, status: undefined });\n return true;\n }\n\n const job: TrackedAsyncJob = { runId, ...read };\n session.jobs.set(runId, job);\n const wasTerminal = existing?.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(existing.status);\n const isTerminal = job.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(job.status);\n if (isTerminal && !wasTerminal) session.terminalAt.set(runId, this.now());\n if (!isTerminal) {\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n }\n return (\n !existing ||\n existing.status !== job.status ||\n existing.updatedAt !== job.updatedAt ||\n existing.tokens !== job.tokens ||\n existing.cost !== job.cost ||\n existing.currentTool !== job.currentTool ||\n existing.toolCount !== job.toolCount\n );\n }\n\n private refresh(sessionId: string, runId: string): boolean {\n const changed = this.applyStatus(sessionId, runId, this.readStatus(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n private async refreshAsync(sessionId: string, runId: string): Promise<boolean> {\n const changed = this.applyStatus(sessionId, runId, await this.readStatusAsync(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n /** One scheduler tick: asynchronously refresh every tracked status, then evict expired terminal jobs. */\n protected async run(): Promise<boolean> {\n let changed = false;\n const now = this.now();\n for (const [sessionId, session] of this.sessions) {\n for (const runId of session.jobs.keys()) {\n if (await this.refreshAsync(sessionId, runId)) changed = true;\n }\n for (const [runId, terminatedAt] of session.terminalAt) {\n if (!session.handedOff.has(runId) || now - terminatedAt <= this.retentionMs) continue;\n session.jobs.delete(runId);\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n this.invalidate(sessionId);\n changed = true;\n }\n // Only drop the entry this pass actually walked: a `track()` that landed\n // during an await above may have installed a fresh one under the same id.\n if (session.jobs.size === 0 && this.sessions.get(sessionId) === session) this.sessions.delete(sessionId);\n }\n\n return changed;\n }\n\n track(runId: string): void {\n this.trackInSession(UNSCOPED_SESSION, runId);\n }\n\n untrack(runId: string): void {\n this.untrackInSession(UNSCOPED_SESSION, runId);\n }\n\n /** Mark one session's terminal result as safely handed off. */\n acknowledgeHandoff(sessionId: string, runId: string): void {\n this.acknowledgeHandoffInSession(sessionId, runId);\n }\n\n /** Work that still suppresses same-session idle continuation. */\n listBackgroundWork(sessionId: string): TrackedAsyncJob[] {\n const session = this.sessions.get(sessionId);\n if (!session) return [];\n return [...session.jobs.values()].filter(\n (job) =>\n job.status === undefined || !TERMINAL_ASYNC_JOB_STATES.has(job.status) || !session.handedOff.has(job.runId),\n );\n }\n\n /** Subscribe to invalidations for one exact Pi session. */\n subscribe(sessionId: string, listener: () => void): () => void {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to subscribe to subagent runs.');\n return this.subscribeInSession(sessionId, listener);\n }\n\n list(): TrackedAsyncJob[] {\n return this.listInSession(UNSCOPED_SESSION);\n }\n\n get(runId: string): TrackedAsyncJob | undefined {\n return this.getInSession(UNSCOPED_SESSION, runId);\n }\n\n reset(): void {\n this.resetSession(UNSCOPED_SESSION);\n }\n\n private trackInSession(sessionId: string, runId: string): void {\n // `applyStatus` no longer creates sessions (see its comment), so tracking is\n // the one path that does.\n this.session(sessionId);\n this.refresh(sessionId, runId);\n }\n\n private untrackInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const deleted = session?.jobs.delete(runId) ?? false;\n session?.terminalAt.delete(runId);\n session?.handedOff.delete(runId);\n if (session?.jobs.size === 0) this.sessions.delete(sessionId);\n if (deleted) this.invalidate(sessionId);\n }\n\n private acknowledgeHandoffInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const status = session?.jobs.get(runId)?.status;\n if (!session || !status || !TERMINAL_ASYNC_JOB_STATES.has(status) || session.handedOff.has(runId)) return;\n session.handedOff.add(runId);\n this.invalidate(sessionId);\n }\n\n private subscribeInSession(sessionId: string, listener: () => void): () => void {\n const listeners = this.listeners.get(sessionId) ?? new Set<() => void>();\n listeners.add(listener);\n this.listeners.set(sessionId, listeners);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) this.listeners.delete(sessionId);\n };\n }\n\n private listInSession(sessionId: string): TrackedAsyncJob[] {\n return [...(this.sessions.get(sessionId)?.jobs.values() ?? [])];\n }\n\n private getInSession(sessionId: string, runId: string): TrackedAsyncJob | undefined {\n return this.sessions.get(sessionId)?.jobs.get(runId);\n }\n\n private resetSession(sessionId: string): void {\n const hadJobs = (this.sessions.get(sessionId)?.jobs.size ?? 0) > 0;\n this.sessions.delete(sessionId);\n if (hadJobs) this.invalidate(sessionId);\n }\n\n start(): void {\n this.stop();\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n }\n\n stop(): void {\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.sessions.clear();\n this.listeners.clear();\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{currentRunsDir as t,init_paths as n}from"./filesystem/paths.mjs";import{STATUS_FILE_NAME as r,init_statusWriter as i}from"./runs/background/statusWriter.mjs";import{init_statusReader as a,parseAsyncRunStatus as o}from"./statusReader.mjs";import*as s from"node:fs";import*as c from"node:path";function l(e,t){let n=e.list().map(e=>e.runId).filter(e=>e===t||e.startsWith(t)),r=n.find(e=>e===t);if(r)return r;if(n.length===1)return n[0];throw n.length===0?Error(`No current-session run found for '${t}'.`):Error(`Multiple current-session runs match '${t}'. Use a longer id.`)}function u(e){return c.join(t(),e,r)}function d(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function f(e){return d(e)===`ENOENT`}function p(e){return typeof e==`string`&&y.has(e)?e:void 0}var m,h,g,_,v,y,b,x=e((()=>{a(),i(),n(),m=new Set([`complete`,`completed`,`failed`,`paused`,`stopped`]),h=1e4,g=250,_=`async-job-tracker`,v=`__unscoped__`,y=new Set([`starting`,`working`,`tool`,`waiting_for_reply`,`needs_attention`,`finalizing`,`active_long_running`]),b=class{scheduler;constructor(e){this.scheduler=e}pollIntervalMs=g;retentionMs=h;now(){return Date.now()}readFile(e){try{return s.readFileSync(e,`utf-8`)}catch(e){if(f(e))return;throw e}}async readFileAsync(e){try{return await s.promises.readFile(e,`utf-8`)}catch(e){if(f(e))return;throw e}}sessions=new Map;listeners=new Map;unregisterPoll;session(e){let t=this.sessions.get(e);return t||(t={jobs:new Map,terminalAt:new Map,handedOff:new Set},this.sessions.set(e,t)),t}invalidate(e){for(let t of this.listeners.get(e)??[])t()}forSession(e){if(!e.trim())throw Error(`Pi session identity is required to track subagent runs.`);return{track:t=>this.trackInSession(e,t),untrack:t=>this.untrackInSession(e,t),list:()=>this.listInSession(e),get:t=>this.getInSession(e,t),reset:()=>this.resetSession(e)}}parseStatus(e,t){if(t===void 0)return;let n=o(t,u(e));if(n.kind!==`ok`)return;let r=n.status;return{agent:r.agent,status:r.state,startedAt:r.startedAt,updatedAt:r.lastUpdate,error:r.error,activityState:p(r.activityState),attentionReason:r.attentionReason,runtime:r.runtime,tokens:r.tokens,currentTool:r.currentTool,toolCount:r.toolCount}}readStatus(e){return this.parseStatus(e,this.readFile(u(e)))}async readStatusAsync(e){return this.parseStatus(e,await this.readFileAsync(u(e)))}applyStatus(e,t,n){let r=this.sessions.get(e);if(!r)return!1;let i=r.jobs.get(t);if(n===void 0)return!i&&(r.jobs.set(t,{runId:t,status:void 0}),!0);let a={runId:t,...n};r.jobs.set(t,a);let o=i?.status!==void 0&&m.has(i.status),s=a.status!==void 0&&m.has(a.status);return s&&!o&&r.terminalAt.set(t,this.now()),s||(r.terminalAt.delete(t),r.handedOff.delete(t)),!i||i.status!==a.status||i.updatedAt!==a.updatedAt||i.tokens!==a.tokens||i.currentTool!==a.currentTool||i.toolCount!==a.toolCount}refresh(e,t){let n=this.applyStatus(e,t,this.readStatus(t));return n&&this.invalidate(e),n}async refreshAsync(e,t){let n=this.applyStatus(e,t,await this.readStatusAsync(t));return n&&this.invalidate(e),n}async run(){let e=!1,t=this.now();for(let[n,r]of this.sessions){for(let t of r.jobs.keys())await this.refreshAsync(n,t)&&(e=!0);for(let[i,a]of r.terminalAt)!r.handedOff.has(i)||t-a<=this.retentionMs||(r.jobs.delete(i),r.terminalAt.delete(i),r.handedOff.delete(i),this.invalidate(n),e=!0);r.jobs.size===0&&this.sessions.get(n)===r&&this.sessions.delete(n)}return e}track(e){this.trackInSession(v,e)}untrack(e){this.untrackInSession(v,e)}acknowledgeHandoff(e,t){this.acknowledgeHandoffInSession(e,t)}listBackgroundWork(e){let t=this.sessions.get(e);return t?[...t.jobs.values()].filter(e=>e.status===void 0||!m.has(e.status)||!t.handedOff.has(e.runId)):[]}subscribe(e,t){if(!e.trim())throw Error(`Pi session identity is required to subscribe to subagent runs.`);return this.subscribeInSession(e,t)}list(){return this.listInSession(v)}get(e){return this.getInSession(v,e)}reset(){this.resetSession(v)}trackInSession(e,t){this.session(e),this.refresh(e,t)}untrackInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.delete(t)??!1;n?.terminalAt.delete(t),n?.handedOff.delete(t),n?.jobs.size===0&&this.sessions.delete(e),r&&this.invalidate(e)}acknowledgeHandoffInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.get(t)?.status;n&&r&&m.has(r)&&!n.handedOff.has(t)&&(n.handedOff.add(t),this.invalidate(e))}subscribeInSession(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>{n.delete(t),n.size===0&&this.listeners.delete(e)}}listInSession(e){return[...this.sessions.get(e)?.jobs.values()??[]]}getInSession(e,t){return this.sessions.get(e)?.jobs.get(t)}resetSession(e){let t=(this.sessions.get(e)?.jobs.size??0)>0;this.sessions.delete(e),t&&this.invalidate(e)}start(){this.stop(),this.unregisterPoll=this.scheduler.register({id:_,intervalMs:this.pollIntervalMs,run:()=>this.run()})}stop(){this.unregisterPoll?.(),this.unregisterPoll=void 0,this.sessions.clear(),this.listeners.clear()}}}));x();export{b as AsyncJobTracker,m as TERMINAL_ASYNC_JOB_STATES,x as init_asyncJobTracker,l as resolveTrackedRunId};
1
+ import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{currentRunsDir as t,init_paths as n}from"./filesystem/paths.mjs";import{STATUS_FILE_NAME as r,init_statusWriter as i}from"./runs/background/statusWriter.mjs";import{init_statusReader as a,parseAsyncRunStatus as o}from"./statusReader.mjs";import*as s from"node:fs";import*as c from"node:path";function l(e,t){let n=e.list().map(e=>e.runId).filter(e=>e===t||e.startsWith(t)),r=n.find(e=>e===t);if(r)return r;if(n.length===1)return n[0];throw n.length===0?Error(`No current-session run found for '${t}'.`):Error(`Multiple current-session runs match '${t}'. Use a longer id.`)}function u(e){return c.join(t(),e,r)}function d(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function f(e){return d(e)===`ENOENT`}function p(e){return typeof e==`string`&&y.has(e)?e:void 0}var m,h,g,_,v,y,b,x=e((()=>{a(),i(),n(),m=new Set([`complete`,`completed`,`failed`,`paused`,`stopped`]),h=1e4,g=250,_=`async-job-tracker`,v=`__unscoped__`,y=new Set([`starting`,`working`,`tool`,`waiting_for_reply`,`needs_attention`,`finalizing`,`active_long_running`]),b=class{scheduler;constructor(e){this.scheduler=e}pollIntervalMs=g;retentionMs=h;now(){return Date.now()}readFile(e){try{return s.readFileSync(e,`utf-8`)}catch(e){if(f(e))return;throw e}}async readFileAsync(e){try{return await s.promises.readFile(e,`utf-8`)}catch(e){if(f(e))return;throw e}}sessions=new Map;listeners=new Map;unregisterPoll;session(e){let t=this.sessions.get(e);return t||(t={jobs:new Map,terminalAt:new Map,handedOff:new Set},this.sessions.set(e,t)),t}invalidate(e){for(let t of this.listeners.get(e)??[])t()}forSession(e){if(!e.trim())throw Error(`Pi session identity is required to track subagent runs.`);return{track:t=>this.trackInSession(e,t),untrack:t=>this.untrackInSession(e,t),list:()=>this.listInSession(e),get:t=>this.getInSession(e,t),reset:()=>this.resetSession(e)}}parseStatus(e,t){if(t===void 0)return;let n=o(t,u(e));if(n.kind!==`ok`)return;let r=n.status;return{agent:r.agent,status:r.state,startedAt:r.startedAt,updatedAt:r.lastUpdate,error:r.error,activityState:p(r.activityState),attentionReason:r.attentionReason,runtime:r.runtime,tokens:r.tokens,cost:r.cost,currentTool:r.currentTool,toolCount:r.toolCount}}readStatus(e){return this.parseStatus(e,this.readFile(u(e)))}async readStatusAsync(e){return this.parseStatus(e,await this.readFileAsync(u(e)))}applyStatus(e,t,n){let r=this.sessions.get(e);if(!r)return!1;let i=r.jobs.get(t);if(n===void 0)return!i&&(r.jobs.set(t,{runId:t,status:void 0}),!0);let a={runId:t,...n};r.jobs.set(t,a);let o=i?.status!==void 0&&m.has(i.status),s=a.status!==void 0&&m.has(a.status);return s&&!o&&r.terminalAt.set(t,this.now()),s||(r.terminalAt.delete(t),r.handedOff.delete(t)),!i||i.status!==a.status||i.updatedAt!==a.updatedAt||i.tokens!==a.tokens||i.cost!==a.cost||i.currentTool!==a.currentTool||i.toolCount!==a.toolCount}refresh(e,t){let n=this.applyStatus(e,t,this.readStatus(t));return n&&this.invalidate(e),n}async refreshAsync(e,t){let n=this.applyStatus(e,t,await this.readStatusAsync(t));return n&&this.invalidate(e),n}async run(){let e=!1,t=this.now();for(let[n,r]of this.sessions){for(let t of r.jobs.keys())await this.refreshAsync(n,t)&&(e=!0);for(let[i,a]of r.terminalAt)!r.handedOff.has(i)||t-a<=this.retentionMs||(r.jobs.delete(i),r.terminalAt.delete(i),r.handedOff.delete(i),this.invalidate(n),e=!0);r.jobs.size===0&&this.sessions.get(n)===r&&this.sessions.delete(n)}return e}track(e){this.trackInSession(v,e)}untrack(e){this.untrackInSession(v,e)}acknowledgeHandoff(e,t){this.acknowledgeHandoffInSession(e,t)}listBackgroundWork(e){let t=this.sessions.get(e);return t?[...t.jobs.values()].filter(e=>e.status===void 0||!m.has(e.status)||!t.handedOff.has(e.runId)):[]}subscribe(e,t){if(!e.trim())throw Error(`Pi session identity is required to subscribe to subagent runs.`);return this.subscribeInSession(e,t)}list(){return this.listInSession(v)}get(e){return this.getInSession(v,e)}reset(){this.resetSession(v)}trackInSession(e,t){this.session(e),this.refresh(e,t)}untrackInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.delete(t)??!1;n?.terminalAt.delete(t),n?.handedOff.delete(t),n?.jobs.size===0&&this.sessions.delete(e),r&&this.invalidate(e)}acknowledgeHandoffInSession(e,t){let n=this.sessions.get(e),r=n?.jobs.get(t)?.status;n&&r&&m.has(r)&&!n.handedOff.has(t)&&(n.handedOff.add(t),this.invalidate(e))}subscribeInSession(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>{n.delete(t),n.size===0&&this.listeners.delete(e)}}listInSession(e){return[...this.sessions.get(e)?.jobs.values()??[]]}getInSession(e,t){return this.sessions.get(e)?.jobs.get(t)}resetSession(e){let t=(this.sessions.get(e)?.jobs.size??0)>0;this.sessions.delete(e),t&&this.invalidate(e)}start(){this.stop(),this.unregisterPoll=this.scheduler.register({id:_,intervalMs:this.pollIntervalMs,run:()=>this.run()})}stop(){this.unregisterPoll?.(),this.unregisterPoll=void 0,this.sessions.clear(),this.listeners.clear()}}}));x();export{b as AsyncJobTracker,m as TERMINAL_ASYNC_JOB_STATES,x as init_asyncJobTracker,l as resolveTrackedRunId};
2
2
  //# sourceMappingURL=asyncJobTracker.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"asyncJobTracker.mjs","names":["path","fs"],"sources":["../../../src/adapters/asyncJobTracker.ts"],"sourcesContent":["/**\n * Live, in-memory registry of the async runs this session currently cares\n * about, refreshed from `status.json` on a `PollScheduler` tick.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/asyncJobTracker.ts` is 590 lines\n * doing four different jobs: this registry, TUI widget rendering\n * (`renderWidget`/`widgetRenderKey`/`ctx.ui.requestRender`), forwarding\n * control events and steering notices by byte-range-tailing each run's\n * `events.jsonl`, and projecting nested/parallel-group run trees. Only the\n * registry is ported. The other three have no consumer in this package yet:\n * TUI rendering is `G8` (slash commands + TUI), and nested/chain/parallel run\n * modes are `G7`+ (this package is currently single-run async-only - see\n * `spawnHandshake.ts`'s header). Porting rendering or event-tailing logic\n * with nothing to render to or forward to would be untested, unreachable\n * code; it is deferred to whichever later port actually needs it, not\n * dropped silently - this note is where to look for it.\n *\n * DESIGN PATTERNS:\n * - The in-memory map is a pure CACHE, never authoritative on its own:\n * `status.json` (owned by `CoalescedStatusWriter` elsewhere) is the only\n * source of truth. `track()` reads it synchronously immediately, so a\n * caller that already knows a run's id sees *something* right away rather\n * than waiting out the first poll\n * - Restart survivability follows from the point above rather than a second\n * persistence layer: this package has not built session-scoped run\n * ownership yet (which ids \"belong\" to a restarted session is not\n * something this module can determine on its own - see the AVOID entry),\n * so recovery is the caller re-`track()`-ing whatever ids it still cares\n * about from ITS OWN durable record. The very next poll tick repopulates\n * that job's status straight from disk. There is no separate store to keep\n * in sync, and therefore no window where memory could diverge from disk\n * that a restart would not immediately close\n * - Registered with `PollScheduler`, not a private timer: this polls\n * potentially many jobs' status files on an unbounded, periodic cadence\n * with no low-latency requirement - the exact shape `PollScheduler` exists\n * for. Contrast `completionBatcher.ts`/`spawnHandshake.ts`, which own\n * their own timers because they are short-lived, single-shot, and need\n * sub-200ms responsiveness `PollScheduler`'s floor cannot give them; this\n * registry needs neither\n * - Retention-then-eviction happens on the SAME poll tick as the status\n * refresh, rather than the predecessor's per-job `setTimeout`\n * (`scheduleCleanup`): a terminal job is dropped only after its result was\n * safely handed off and `retentionMs` elapsed. Failed delivery therefore\n * keeps the tracker entry available for `ResultWatcher`'s later retry\n *\n * AVOID:\n * - Scanning `currentRunsDir()` to guess which run ids \"belong\" to this session on\n * startup. This package has no session-scoped run ownership concept yet;\n * inventing one here to answer \"what should I restore\" would be a guess,\n * not a fact this module has standing to assert. Restoration is the\n * caller's job (see the design note above)\n * - Growing `terminalStates` independently of `staleRunReconciler.ts`'s own\n * (unexported) copy; both must treat the same state strings as terminal,\n * or a job could be evicted here while the reconciler still thinks it is\n * in flight, or vice versa\n *\n * `activityState`/`attentionReason` (added for `subagentWait.ts`):\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and a `reason` (e.g. `'missing-deliverable'`), but returns it rather than\n * writing it anywhere - it has no standing to touch `status.json` itself (see\n * that module's header). Something upstream of this tracker - the run's own\n * runner main loop, not yet built - is responsible for folding that result\n * into `status.json` through `CoalescedStatusWriter`. This tracker's job is\n * only to read whatever ends up there, the same as every other field: it does\n * not compute attention, detect it, or decide when a run needs it.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { parseAsyncRunStatus } from './statusReader';\nimport { STATUS_FILE_NAME } from './runs/background/statusWriter';\nimport { currentRunsDir } from './filesystem/paths';\nimport type { PollSchedulerContract } from './pollScheduler';\nimport type { ActivityState } from '../types';\n\n/**\n * States that mean \"already finished\"; matches `staleRunReconciler.ts`'s\n * own (unexported) set. Exported so a caller outside this module (the slash\n * layer's poll-until-terminal helper) can recognize the same terminal set\n * `TrackedAsyncJob.status` uses, rather than declaring a third copy of this\n * literal.\n */\nexport const TERMINAL_ASYNC_JOB_STATES = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\n/** Minimum time a handed-off terminal job stays queryable before eviction. */\nconst DEFAULT_RETENTION_MS = 10_000;\n/** How often tracked jobs' status is refreshed from disk. */\nconst DEFAULT_POLL_INTERVAL_MS = 250;\n\nconst POLL_SUBSCRIBER_ID = 'async-job-tracker';\n\n/**\n * The minimum shape this module reads. A run's actual `status.json` carries\n * far more (this package has no shared status type yet - see\n * `staleRunReconciler.ts`'s `ReconcilableStatus` for the same caveat);\n * everything else is simply not read here.\n */\nexport interface TrackedAsyncJob {\n runId: string;\n /** Named agent executing this run. Absent until the first readable status. */\n agent?: string;\n /** `status.json`'s `state` field. `undefined` until the first successful read. */\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n /** `status.json`'s `activityState` field, e.g. `'needs_attention'`. See the module doc. */\n activityState?: ActivityState;\n /** `status.json`'s `reason` field, present alongside `activityState` (e.g. `'missing-deliverable'`). */\n attentionReason?: string;\n /** Which runtime is executing this run. Absent means the default `pi` path. */\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nexport type TrackedAsyncJobsContract = {\n /**\n * Start tracking `runId`: reads its current status synchronously (so a\n * caller sees something immediately) and adds it to the poll rotation.\n * Calling this again for an id already tracked simply re-reads it now,\n * the same idempotent-refresh idiom as `CoalescedStatusWriter.open()`.\n */\n track(runId: string): void;\n /** Stop tracking `runId` immediately, without waiting out its retention window. */\n untrack(runId: string): void;\n /** Every tracked job, including terminal jobs still awaiting a safe handoff. */\n list(): TrackedAsyncJob[];\n get(runId: string): TrackedAsyncJob | undefined;\n /** Drop every tracked job. */\n reset(): void;\n};\n\nexport type AsyncJobTrackerContract = TrackedAsyncJobsContract & {\n /** Return the isolated run collection owned by one Pi session context. */\n forSession(sessionId: string): TrackedAsyncJobsContract;\n /** Register with `PollScheduler`. Call once; a second call is a reset, same as `ResultWatcher.start()`. */\n start(): void;\n /** Unregister from `PollScheduler` and drop every tracked job. */\n stop(): void;\n};\n\n/** Resolve an exact id or unique prefix only inside one Pi session's run collection. */\nexport function resolveTrackedRunId(jobs: TrackedAsyncJobsContract, id: string): string {\n const matches = jobs\n .list()\n .map((job) => job.runId)\n .filter((runId) => runId === id || runId.startsWith(id));\n const exact = matches.find((runId) => runId === id);\n if (exact) return exact;\n if (matches.length === 1) return matches[0]!;\n if (matches.length === 0) throw new Error(`No current-session run found for '${id}'.`);\n throw new Error(`Multiple current-session runs match '${id}'. Use a longer id.`);\n}\n\ninterface SessionJobs {\n jobs: Map<string, TrackedAsyncJob>;\n terminalAt: Map<string, number>;\n handedOff: Set<string>;\n}\n\nconst UNSCOPED_SESSION = '__unscoped__';\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\ninterface ReadStatusResult {\n agent: string;\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n activityState?: ActivityState;\n attentionReason?: string;\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nconst VALID_ACTIVITY_STATES: ReadonlySet<string> = new Set<ActivityState>([\n 'starting',\n 'working',\n 'tool',\n 'waiting_for_reply',\n 'needs_attention',\n 'finalizing',\n 'active_long_running',\n]);\n\nfunction asActivityState(value: unknown): ActivityState | undefined {\n return typeof value === 'string' && VALID_ACTIVITY_STATES.has(value) ? (value as ActivityState) : undefined;\n}\n\nexport class AsyncJobTracker implements AsyncJobTrackerContract {\n constructor(private readonly scheduler: PollSchedulerContract) {}\n\n /**\n * Runtime tuning seams for tests, kept out of the dependency constructor.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly retentionMs: number = DEFAULT_RETENTION_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected async readFileAsync(filePath: string): Promise<string | undefined> {\n try {\n return await fs.promises.readFile(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n private readonly sessions = new Map<string, SessionJobs>();\n private readonly listeners = new Map<string, Set<() => void>>();\n private unregisterPoll: (() => void) | undefined;\n\n private session(sessionId: string): SessionJobs {\n let session = this.sessions.get(sessionId);\n if (!session) {\n session = { jobs: new Map(), terminalAt: new Map(), handedOff: new Set() };\n this.sessions.set(sessionId, session);\n }\n return session;\n }\n\n private invalidate(sessionId: string): void {\n for (const listener of this.listeners.get(sessionId) ?? []) listener();\n }\n\n forSession(sessionId: string): TrackedAsyncJobsContract {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to track subagent runs.');\n return {\n track: (runId) => this.trackInSession(sessionId, runId),\n untrack: (runId) => this.untrackInSession(sessionId, runId),\n list: () => this.listInSession(sessionId),\n get: (runId) => this.getInSession(sessionId, runId),\n reset: () => this.resetSession(sessionId),\n };\n }\n\n private parseStatus(runId: string, raw: string | undefined): ReadStatusResult | undefined {\n if (raw === undefined) return undefined;\n const result = parseAsyncRunStatus(raw, statusPathFor(runId));\n if (result.kind !== 'ok') return undefined;\n const status = result.status;\n return {\n agent: status.agent,\n status: status.state,\n startedAt: status.startedAt,\n updatedAt: status.lastUpdate,\n error: status.error,\n activityState: asActivityState(status.activityState),\n attentionReason: status.attentionReason,\n runtime: status.runtime,\n tokens: status.tokens,\n currentTool: status.currentTool,\n toolCount: status.toolCount,\n };\n }\n\n private readStatus(runId: string): ReadStatusResult | undefined {\n return this.parseStatus(runId, this.readFile(statusPathFor(runId)));\n }\n\n private async readStatusAsync(runId: string): Promise<ReadStatusResult | undefined> {\n return this.parseStatus(runId, await this.readFileAsync(statusPathFor(runId)));\n }\n\n private applyStatus(sessionId: string, runId: string, read: ReadStatusResult | undefined): boolean {\n // Teardown (`stop()`, `reset()`) can land between two of the tick's awaits.\n // A late refresh must observe that and drop its result, never recreate the\n // session it belonged to: resurrecting it here is what made `stop()` not stop.\n const session = this.sessions.get(sessionId);\n if (!session) return false;\n const existing = session.jobs.get(runId);\n if (read === undefined) {\n // A run may not have created its file yet, and a torn read must not blank\n // a previously known status for one polling pass.\n if (existing) return false;\n session.jobs.set(runId, { runId, status: undefined });\n return true;\n }\n\n const job: TrackedAsyncJob = { runId, ...read };\n session.jobs.set(runId, job);\n const wasTerminal = existing?.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(existing.status);\n const isTerminal = job.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(job.status);\n if (isTerminal && !wasTerminal) session.terminalAt.set(runId, this.now());\n if (!isTerminal) {\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n }\n return (\n !existing ||\n existing.status !== job.status ||\n existing.updatedAt !== job.updatedAt ||\n existing.tokens !== job.tokens ||\n existing.currentTool !== job.currentTool ||\n existing.toolCount !== job.toolCount\n );\n }\n\n private refresh(sessionId: string, runId: string): boolean {\n const changed = this.applyStatus(sessionId, runId, this.readStatus(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n private async refreshAsync(sessionId: string, runId: string): Promise<boolean> {\n const changed = this.applyStatus(sessionId, runId, await this.readStatusAsync(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n /** One scheduler tick: asynchronously refresh every tracked status, then evict expired terminal jobs. */\n protected async run(): Promise<boolean> {\n let changed = false;\n const now = this.now();\n for (const [sessionId, session] of this.sessions) {\n for (const runId of session.jobs.keys()) {\n if (await this.refreshAsync(sessionId, runId)) changed = true;\n }\n for (const [runId, terminatedAt] of session.terminalAt) {\n if (!session.handedOff.has(runId) || now - terminatedAt <= this.retentionMs) continue;\n session.jobs.delete(runId);\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n this.invalidate(sessionId);\n changed = true;\n }\n // Only drop the entry this pass actually walked: a `track()` that landed\n // during an await above may have installed a fresh one under the same id.\n if (session.jobs.size === 0 && this.sessions.get(sessionId) === session) this.sessions.delete(sessionId);\n }\n\n return changed;\n }\n\n track(runId: string): void {\n this.trackInSession(UNSCOPED_SESSION, runId);\n }\n\n untrack(runId: string): void {\n this.untrackInSession(UNSCOPED_SESSION, runId);\n }\n\n /** Mark one session's terminal result as safely handed off. */\n acknowledgeHandoff(sessionId: string, runId: string): void {\n this.acknowledgeHandoffInSession(sessionId, runId);\n }\n\n /** Work that still suppresses same-session idle continuation. */\n listBackgroundWork(sessionId: string): TrackedAsyncJob[] {\n const session = this.sessions.get(sessionId);\n if (!session) return [];\n return [...session.jobs.values()].filter(\n (job) =>\n job.status === undefined || !TERMINAL_ASYNC_JOB_STATES.has(job.status) || !session.handedOff.has(job.runId),\n );\n }\n\n /** Subscribe to invalidations for one exact Pi session. */\n subscribe(sessionId: string, listener: () => void): () => void {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to subscribe to subagent runs.');\n return this.subscribeInSession(sessionId, listener);\n }\n\n list(): TrackedAsyncJob[] {\n return this.listInSession(UNSCOPED_SESSION);\n }\n\n get(runId: string): TrackedAsyncJob | undefined {\n return this.getInSession(UNSCOPED_SESSION, runId);\n }\n\n reset(): void {\n this.resetSession(UNSCOPED_SESSION);\n }\n\n private trackInSession(sessionId: string, runId: string): void {\n // `applyStatus` no longer creates sessions (see its comment), so tracking is\n // the one path that does.\n this.session(sessionId);\n this.refresh(sessionId, runId);\n }\n\n private untrackInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const deleted = session?.jobs.delete(runId) ?? false;\n session?.terminalAt.delete(runId);\n session?.handedOff.delete(runId);\n if (session?.jobs.size === 0) this.sessions.delete(sessionId);\n if (deleted) this.invalidate(sessionId);\n }\n\n private acknowledgeHandoffInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const status = session?.jobs.get(runId)?.status;\n if (!session || !status || !TERMINAL_ASYNC_JOB_STATES.has(status) || session.handedOff.has(runId)) return;\n session.handedOff.add(runId);\n this.invalidate(sessionId);\n }\n\n private subscribeInSession(sessionId: string, listener: () => void): () => void {\n const listeners = this.listeners.get(sessionId) ?? new Set<() => void>();\n listeners.add(listener);\n this.listeners.set(sessionId, listeners);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) this.listeners.delete(sessionId);\n };\n }\n\n private listInSession(sessionId: string): TrackedAsyncJob[] {\n return [...(this.sessions.get(sessionId)?.jobs.values() ?? [])];\n }\n\n private getInSession(sessionId: string, runId: string): TrackedAsyncJob | undefined {\n return this.sessions.get(sessionId)?.jobs.get(runId);\n }\n\n private resetSession(sessionId: string): void {\n const hadJobs = (this.sessions.get(sessionId)?.jobs.size ?? 0) > 0;\n this.sessions.delete(sessionId);\n if (hadJobs) this.invalidate(sessionId);\n }\n\n start(): void {\n this.stop();\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n }\n\n stop(): void {\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.sessions.clear();\n this.listeners.clear();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"asyncJobTracker.mjs","names":["path","fs"],"sources":["../../../src/adapters/asyncJobTracker.ts"],"sourcesContent":["/**\n * Live, in-memory registry of the async runs this session currently cares\n * about, refreshed from `status.json` on a `PollScheduler` tick.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/asyncJobTracker.ts` is 590 lines\n * doing four different jobs: this registry, TUI widget rendering\n * (`renderWidget`/`widgetRenderKey`/`ctx.ui.requestRender`), forwarding\n * control events and steering notices by byte-range-tailing each run's\n * `events.jsonl`, and projecting nested/parallel-group run trees. Only the\n * registry is ported. The other three have no consumer in this package yet:\n * TUI rendering is `G8` (slash commands + TUI), and nested/chain/parallel run\n * modes are `G7`+ (this package is currently single-run async-only - see\n * `spawnHandshake.ts`'s header). Porting rendering or event-tailing logic\n * with nothing to render to or forward to would be untested, unreachable\n * code; it is deferred to whichever later port actually needs it, not\n * dropped silently - this note is where to look for it.\n *\n * DESIGN PATTERNS:\n * - The in-memory map is a pure CACHE, never authoritative on its own:\n * `status.json` (owned by `CoalescedStatusWriter` elsewhere) is the only\n * source of truth. `track()` reads it synchronously immediately, so a\n * caller that already knows a run's id sees *something* right away rather\n * than waiting out the first poll\n * - Restart survivability follows from the point above rather than a second\n * persistence layer: this package has not built session-scoped run\n * ownership yet (which ids \"belong\" to a restarted session is not\n * something this module can determine on its own - see the AVOID entry),\n * so recovery is the caller re-`track()`-ing whatever ids it still cares\n * about from ITS OWN durable record. The very next poll tick repopulates\n * that job's status straight from disk. There is no separate store to keep\n * in sync, and therefore no window where memory could diverge from disk\n * that a restart would not immediately close\n * - Registered with `PollScheduler`, not a private timer: this polls\n * potentially many jobs' status files on an unbounded, periodic cadence\n * with no low-latency requirement - the exact shape `PollScheduler` exists\n * for. Contrast `completionBatcher.ts`/`spawnHandshake.ts`, which own\n * their own timers because they are short-lived, single-shot, and need\n * sub-200ms responsiveness `PollScheduler`'s floor cannot give them; this\n * registry needs neither\n * - Retention-then-eviction happens on the SAME poll tick as the status\n * refresh, rather than the predecessor's per-job `setTimeout`\n * (`scheduleCleanup`): a terminal job is dropped only after its result was\n * safely handed off and `retentionMs` elapsed. Failed delivery therefore\n * keeps the tracker entry available for `ResultWatcher`'s later retry\n *\n * AVOID:\n * - Scanning `currentRunsDir()` to guess which run ids \"belong\" to this session on\n * startup. This package has no session-scoped run ownership concept yet;\n * inventing one here to answer \"what should I restore\" would be a guess,\n * not a fact this module has standing to assert. Restoration is the\n * caller's job (see the design note above)\n * - Growing `terminalStates` independently of `staleRunReconciler.ts`'s own\n * (unexported) copy; both must treat the same state strings as terminal,\n * or a job could be evicted here while the reconciler still thinks it is\n * in flight, or vice versa\n *\n * `activityState`/`attentionReason` (added for `subagentWait.ts`):\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and a `reason` (e.g. `'missing-deliverable'`), but returns it rather than\n * writing it anywhere - it has no standing to touch `status.json` itself (see\n * that module's header). Something upstream of this tracker - the run's own\n * runner main loop, not yet built - is responsible for folding that result\n * into `status.json` through `CoalescedStatusWriter`. This tracker's job is\n * only to read whatever ends up there, the same as every other field: it does\n * not compute attention, detect it, or decide when a run needs it.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { parseAsyncRunStatus } from './statusReader';\nimport { STATUS_FILE_NAME } from './runs/background/statusWriter';\nimport { currentRunsDir } from './filesystem/paths';\nimport type { PollSchedulerContract } from './pollScheduler';\nimport type { ActivityState } from '../types';\n\n/**\n * States that mean \"already finished\"; matches `staleRunReconciler.ts`'s\n * own (unexported) set. Exported so a caller outside this module (the slash\n * layer's poll-until-terminal helper) can recognize the same terminal set\n * `TrackedAsyncJob.status` uses, rather than declaring a third copy of this\n * literal.\n */\nexport const TERMINAL_ASYNC_JOB_STATES = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\n/** Minimum time a handed-off terminal job stays queryable before eviction. */\nconst DEFAULT_RETENTION_MS = 10_000;\n/** How often tracked jobs' status is refreshed from disk. */\nconst DEFAULT_POLL_INTERVAL_MS = 250;\n\nconst POLL_SUBSCRIBER_ID = 'async-job-tracker';\n\n/**\n * The minimum shape this module reads. A run's actual `status.json` carries\n * far more (this package has no shared status type yet - see\n * `staleRunReconciler.ts`'s `ReconcilableStatus` for the same caveat);\n * everything else is simply not read here.\n */\nexport interface TrackedAsyncJob {\n runId: string;\n /** Named agent executing this run. Absent until the first readable status. */\n agent?: string;\n /** `status.json`'s `state` field. `undefined` until the first successful read. */\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n /** `status.json`'s `activityState` field, e.g. `'needs_attention'`. See the module doc. */\n activityState?: ActivityState;\n /** `status.json`'s `reason` field, present alongside `activityState` (e.g. `'missing-deliverable'`). */\n attentionReason?: string;\n /** Which runtime is executing this run. Absent means the default `pi` path. */\n runtime?: string;\n tokens?: number;\n cost?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nexport type TrackedAsyncJobsContract = {\n /**\n * Start tracking `runId`: reads its current status synchronously (so a\n * caller sees something immediately) and adds it to the poll rotation.\n * Calling this again for an id already tracked simply re-reads it now,\n * the same idempotent-refresh idiom as `CoalescedStatusWriter.open()`.\n */\n track(runId: string): void;\n /** Stop tracking `runId` immediately, without waiting out its retention window. */\n untrack(runId: string): void;\n /** Every tracked job, including terminal jobs still awaiting a safe handoff. */\n list(): TrackedAsyncJob[];\n get(runId: string): TrackedAsyncJob | undefined;\n /** Drop every tracked job. */\n reset(): void;\n};\n\nexport type AsyncJobTrackerContract = TrackedAsyncJobsContract & {\n /** Return the isolated run collection owned by one Pi session context. */\n forSession(sessionId: string): TrackedAsyncJobsContract;\n /** Register with `PollScheduler`. Call once; a second call is a reset, same as `ResultWatcher.start()`. */\n start(): void;\n /** Unregister from `PollScheduler` and drop every tracked job. */\n stop(): void;\n};\n\n/** Resolve an exact id or unique prefix only inside one Pi session's run collection. */\nexport function resolveTrackedRunId(jobs: TrackedAsyncJobsContract, id: string): string {\n const matches = jobs\n .list()\n .map((job) => job.runId)\n .filter((runId) => runId === id || runId.startsWith(id));\n const exact = matches.find((runId) => runId === id);\n if (exact) return exact;\n if (matches.length === 1) return matches[0]!;\n if (matches.length === 0) throw new Error(`No current-session run found for '${id}'.`);\n throw new Error(`Multiple current-session runs match '${id}'. Use a longer id.`);\n}\n\ninterface SessionJobs {\n jobs: Map<string, TrackedAsyncJob>;\n terminalAt: Map<string, number>;\n handedOff: Set<string>;\n}\n\nconst UNSCOPED_SESSION = '__unscoped__';\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\ninterface ReadStatusResult {\n agent: string;\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n activityState?: ActivityState;\n attentionReason?: string;\n runtime?: string;\n tokens?: number;\n cost?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nconst VALID_ACTIVITY_STATES: ReadonlySet<string> = new Set<ActivityState>([\n 'starting',\n 'working',\n 'tool',\n 'waiting_for_reply',\n 'needs_attention',\n 'finalizing',\n 'active_long_running',\n]);\n\nfunction asActivityState(value: unknown): ActivityState | undefined {\n return typeof value === 'string' && VALID_ACTIVITY_STATES.has(value) ? (value as ActivityState) : undefined;\n}\n\nexport class AsyncJobTracker implements AsyncJobTrackerContract {\n constructor(private readonly scheduler: PollSchedulerContract) {}\n\n /**\n * Runtime tuning seams for tests, kept out of the dependency constructor.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly retentionMs: number = DEFAULT_RETENTION_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected async readFileAsync(filePath: string): Promise<string | undefined> {\n try {\n return await fs.promises.readFile(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n private readonly sessions = new Map<string, SessionJobs>();\n private readonly listeners = new Map<string, Set<() => void>>();\n private unregisterPoll: (() => void) | undefined;\n\n private session(sessionId: string): SessionJobs {\n let session = this.sessions.get(sessionId);\n if (!session) {\n session = { jobs: new Map(), terminalAt: new Map(), handedOff: new Set() };\n this.sessions.set(sessionId, session);\n }\n return session;\n }\n\n private invalidate(sessionId: string): void {\n for (const listener of this.listeners.get(sessionId) ?? []) listener();\n }\n\n forSession(sessionId: string): TrackedAsyncJobsContract {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to track subagent runs.');\n return {\n track: (runId) => this.trackInSession(sessionId, runId),\n untrack: (runId) => this.untrackInSession(sessionId, runId),\n list: () => this.listInSession(sessionId),\n get: (runId) => this.getInSession(sessionId, runId),\n reset: () => this.resetSession(sessionId),\n };\n }\n\n private parseStatus(runId: string, raw: string | undefined): ReadStatusResult | undefined {\n if (raw === undefined) return undefined;\n const result = parseAsyncRunStatus(raw, statusPathFor(runId));\n if (result.kind !== 'ok') return undefined;\n const status = result.status;\n return {\n agent: status.agent,\n status: status.state,\n startedAt: status.startedAt,\n updatedAt: status.lastUpdate,\n error: status.error,\n activityState: asActivityState(status.activityState),\n attentionReason: status.attentionReason,\n runtime: status.runtime,\n tokens: status.tokens,\n cost: status.cost,\n currentTool: status.currentTool,\n toolCount: status.toolCount,\n };\n }\n\n private readStatus(runId: string): ReadStatusResult | undefined {\n return this.parseStatus(runId, this.readFile(statusPathFor(runId)));\n }\n\n private async readStatusAsync(runId: string): Promise<ReadStatusResult | undefined> {\n return this.parseStatus(runId, await this.readFileAsync(statusPathFor(runId)));\n }\n\n private applyStatus(sessionId: string, runId: string, read: ReadStatusResult | undefined): boolean {\n // Teardown (`stop()`, `reset()`) can land between two of the tick's awaits.\n // A late refresh must observe that and drop its result, never recreate the\n // session it belonged to: resurrecting it here is what made `stop()` not stop.\n const session = this.sessions.get(sessionId);\n if (!session) return false;\n const existing = session.jobs.get(runId);\n if (read === undefined) {\n // A run may not have created its file yet, and a torn read must not blank\n // a previously known status for one polling pass.\n if (existing) return false;\n session.jobs.set(runId, { runId, status: undefined });\n return true;\n }\n\n const job: TrackedAsyncJob = { runId, ...read };\n session.jobs.set(runId, job);\n const wasTerminal = existing?.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(existing.status);\n const isTerminal = job.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(job.status);\n if (isTerminal && !wasTerminal) session.terminalAt.set(runId, this.now());\n if (!isTerminal) {\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n }\n return (\n !existing ||\n existing.status !== job.status ||\n existing.updatedAt !== job.updatedAt ||\n existing.tokens !== job.tokens ||\n existing.cost !== job.cost ||\n existing.currentTool !== job.currentTool ||\n existing.toolCount !== job.toolCount\n );\n }\n\n private refresh(sessionId: string, runId: string): boolean {\n const changed = this.applyStatus(sessionId, runId, this.readStatus(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n private async refreshAsync(sessionId: string, runId: string): Promise<boolean> {\n const changed = this.applyStatus(sessionId, runId, await this.readStatusAsync(runId));\n if (changed) this.invalidate(sessionId);\n return changed;\n }\n\n /** One scheduler tick: asynchronously refresh every tracked status, then evict expired terminal jobs. */\n protected async run(): Promise<boolean> {\n let changed = false;\n const now = this.now();\n for (const [sessionId, session] of this.sessions) {\n for (const runId of session.jobs.keys()) {\n if (await this.refreshAsync(sessionId, runId)) changed = true;\n }\n for (const [runId, terminatedAt] of session.terminalAt) {\n if (!session.handedOff.has(runId) || now - terminatedAt <= this.retentionMs) continue;\n session.jobs.delete(runId);\n session.terminalAt.delete(runId);\n session.handedOff.delete(runId);\n this.invalidate(sessionId);\n changed = true;\n }\n // Only drop the entry this pass actually walked: a `track()` that landed\n // during an await above may have installed a fresh one under the same id.\n if (session.jobs.size === 0 && this.sessions.get(sessionId) === session) this.sessions.delete(sessionId);\n }\n\n return changed;\n }\n\n track(runId: string): void {\n this.trackInSession(UNSCOPED_SESSION, runId);\n }\n\n untrack(runId: string): void {\n this.untrackInSession(UNSCOPED_SESSION, runId);\n }\n\n /** Mark one session's terminal result as safely handed off. */\n acknowledgeHandoff(sessionId: string, runId: string): void {\n this.acknowledgeHandoffInSession(sessionId, runId);\n }\n\n /** Work that still suppresses same-session idle continuation. */\n listBackgroundWork(sessionId: string): TrackedAsyncJob[] {\n const session = this.sessions.get(sessionId);\n if (!session) return [];\n return [...session.jobs.values()].filter(\n (job) =>\n job.status === undefined || !TERMINAL_ASYNC_JOB_STATES.has(job.status) || !session.handedOff.has(job.runId),\n );\n }\n\n /** Subscribe to invalidations for one exact Pi session. */\n subscribe(sessionId: string, listener: () => void): () => void {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to subscribe to subagent runs.');\n return this.subscribeInSession(sessionId, listener);\n }\n\n list(): TrackedAsyncJob[] {\n return this.listInSession(UNSCOPED_SESSION);\n }\n\n get(runId: string): TrackedAsyncJob | undefined {\n return this.getInSession(UNSCOPED_SESSION, runId);\n }\n\n reset(): void {\n this.resetSession(UNSCOPED_SESSION);\n }\n\n private trackInSession(sessionId: string, runId: string): void {\n // `applyStatus` no longer creates sessions (see its comment), so tracking is\n // the one path that does.\n this.session(sessionId);\n this.refresh(sessionId, runId);\n }\n\n private untrackInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const deleted = session?.jobs.delete(runId) ?? false;\n session?.terminalAt.delete(runId);\n session?.handedOff.delete(runId);\n if (session?.jobs.size === 0) this.sessions.delete(sessionId);\n if (deleted) this.invalidate(sessionId);\n }\n\n private acknowledgeHandoffInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n const status = session?.jobs.get(runId)?.status;\n if (!session || !status || !TERMINAL_ASYNC_JOB_STATES.has(status) || session.handedOff.has(runId)) return;\n session.handedOff.add(runId);\n this.invalidate(sessionId);\n }\n\n private subscribeInSession(sessionId: string, listener: () => void): () => void {\n const listeners = this.listeners.get(sessionId) ?? new Set<() => void>();\n listeners.add(listener);\n this.listeners.set(sessionId, listeners);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) this.listeners.delete(sessionId);\n };\n }\n\n private listInSession(sessionId: string): TrackedAsyncJob[] {\n return [...(this.sessions.get(sessionId)?.jobs.values() ?? [])];\n }\n\n private getInSession(sessionId: string, runId: string): TrackedAsyncJob | undefined {\n return this.sessions.get(sessionId)?.jobs.get(runId);\n }\n\n private resetSession(sessionId: string): void {\n const hadJobs = (this.sessions.get(sessionId)?.jobs.size ?? 0) > 0;\n this.sessions.delete(sessionId);\n if (hadJobs) this.invalidate(sessionId);\n }\n\n start(): void {\n this.stop();\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n }\n\n stop(): void {\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.sessions.clear();\n this.listeners.clear();\n }\n}\n"],"mappings":"2WAkJA,SAAgB,EAAoB,EAAgC,EAAoB,CACtF,IAAM,EAAU,EACb,KAAK,CAAC,CACN,IAAK,GAAQ,EAAI,KAAK,CAAC,CACvB,OAAQ,GAAU,IAAU,GAAM,EAAM,WAAW,CAAE,CAAC,EACnD,EAAQ,EAAQ,KAAM,GAAU,IAAU,CAAE,EAClD,GAAI,EAAO,OAAO,EAClB,GAAI,EAAQ,SAAW,EAAG,OAAO,EAAQ,GAEzC,MADI,EAAQ,SAAW,EAAa,MAAM,qCAAqC,EAAG,GAAG,EAC3E,MAAM,wCAAwC,EAAG,oBAAoB,CACjF,CAUA,SAAS,EAAc,EAAuB,CAC5C,OAAOA,EAAK,KAAK,EAAe,EAAG,EAAO,CAAgB,CAC5D,CAEA,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,EACN,CAEA,SAAS,EAAW,EAAyB,CAC3C,OAAO,EAAU,CAAK,IAAM,QAC9B,CA2BA,SAAS,EAAgB,EAA2C,CAClE,OAAO,OAAO,GAAU,UAAY,EAAsB,IAAI,CAAK,EAAK,EAA0B,IAAA,EACpG,6BAzIoD,EAAA,EACa,EAAA,EACd,EAAA,EAWtC,EAA4B,IAAI,IAAI,CAAC,WAAY,YAAa,SAAU,SAAU,SAAS,CAAC,EAGnG,EAAuB,IAEvB,EAA2B,IAE3B,EAAqB,oBA0ErB,EAAmB,eA+BnB,EAA6C,IAAI,IAAmB,CACxE,WACA,UACA,OACA,oBACA,kBACA,aACA,qBACF,CAAC,EAMY,EAAb,KAAgE,CACjC,UAA7B,YAAY,EAAmD,CAAlC,KAAA,UAAA,CAAmC,CAKhE,eAA4C,EAC5C,YAAyC,EAEzC,KAAwB,CACtB,OAAO,KAAK,IAAI,CAClB,CAEA,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAOC,EAAG,aAAa,EAAU,OAAO,CAC1C,OAAS,EAAO,CACd,GAAI,EAAW,CAAK,EAAG,OACvB,MAAM,CACR,CACF,CAEA,MAAgB,cAAc,EAA+C,CAC3E,GAAI,CACF,OAAO,MAAMA,EAAG,SAAS,SAAS,EAAU,OAAO,CACrD,OAAS,EAAO,CACd,GAAI,EAAW,CAAK,EAAG,OACvB,MAAM,CACR,CACF,CAEA,SAA4B,IAAI,IAChC,UAA6B,IAAI,IACjC,eAEA,QAAgB,EAAgC,CAC9C,IAAI,EAAU,KAAK,SAAS,IAAI,CAAS,EAKzC,OAJK,IACH,EAAU,CAAE,KAAM,IAAI,IAAO,WAAY,IAAI,IAAO,UAAW,IAAI,GAAM,EACzE,KAAK,SAAS,IAAI,EAAW,CAAO,GAE/B,CACT,CAEA,WAAmB,EAAyB,CAC1C,IAAK,IAAM,KAAY,KAAK,UAAU,IAAI,CAAS,GAAK,CAAC,EAAG,EAAS,CACvE,CAEA,WAAW,EAA6C,CACtD,GAAI,CAAC,EAAU,KAAK,EAAG,MAAU,MAAM,yDAAyD,EAChG,MAAO,CACL,MAAQ,GAAU,KAAK,eAAe,EAAW,CAAK,EACtD,QAAU,GAAU,KAAK,iBAAiB,EAAW,CAAK,EAC1D,SAAY,KAAK,cAAc,CAAS,EACxC,IAAM,GAAU,KAAK,aAAa,EAAW,CAAK,EAClD,UAAa,KAAK,aAAa,CAAS,CAC1C,CACF,CAEA,YAAoB,EAAe,EAAuD,CACxF,GAAI,IAAQ,IAAA,GAAW,OACvB,IAAM,EAAS,EAAoB,EAAK,EAAc,CAAK,CAAC,EAC5D,GAAI,EAAO,OAAS,KAAM,OAC1B,IAAM,EAAS,EAAO,OACtB,MAAO,CACL,MAAO,EAAO,MACd,OAAQ,EAAO,MACf,UAAW,EAAO,UAClB,UAAW,EAAO,WAClB,MAAO,EAAO,MACd,cAAe,EAAgB,EAAO,aAAa,EACnD,gBAAiB,EAAO,gBACxB,QAAS,EAAO,QAChB,OAAQ,EAAO,OACf,KAAM,EAAO,KACb,YAAa,EAAO,YACpB,UAAW,EAAO,SACpB,CACF,CAEA,WAAmB,EAA6C,CAC9D,OAAO,KAAK,YAAY,EAAO,KAAK,SAAS,EAAc,CAAK,CAAC,CAAC,CACpE,CAEA,MAAc,gBAAgB,EAAsD,CAClF,OAAO,KAAK,YAAY,EAAO,MAAM,KAAK,cAAc,EAAc,CAAK,CAAC,CAAC,CAC/E,CAEA,YAAoB,EAAmB,EAAe,EAA6C,CAIjG,IAAM,EAAU,KAAK,SAAS,IAAI,CAAS,EAC3C,GAAI,CAAC,EAAS,MAAO,GACrB,IAAM,EAAW,EAAQ,KAAK,IAAI,CAAK,EACvC,GAAI,IAAS,IAAA,GAKX,MAFA,CAAI,IACJ,EAAQ,KAAK,IAAI,EAAO,CAAE,QAAO,OAAQ,IAAA,EAAU,CAAC,EAC7C,IAGT,IAAM,EAAuB,CAAE,QAAO,GAAG,CAAK,EAC9C,EAAQ,KAAK,IAAI,EAAO,CAAG,EAC3B,IAAM,EAAc,GAAU,SAAW,IAAA,IAAa,EAA0B,IAAI,EAAS,MAAM,EAC7F,EAAa,EAAI,SAAW,IAAA,IAAa,EAA0B,IAAI,EAAI,MAAM,EAMvF,OALI,GAAc,CAAC,GAAa,EAAQ,WAAW,IAAI,EAAO,KAAK,IAAI,CAAC,EACnE,IACH,EAAQ,WAAW,OAAO,CAAK,EAC/B,EAAQ,UAAU,OAAO,CAAK,GAG9B,CAAC,GACD,EAAS,SAAW,EAAI,QACxB,EAAS,YAAc,EAAI,WAC3B,EAAS,SAAW,EAAI,QACxB,EAAS,OAAS,EAAI,MACtB,EAAS,cAAgB,EAAI,aAC7B,EAAS,YAAc,EAAI,SAE/B,CAEA,QAAgB,EAAmB,EAAwB,CACzD,IAAM,EAAU,KAAK,YAAY,EAAW,EAAO,KAAK,WAAW,CAAK,CAAC,EAEzE,OADI,GAAS,KAAK,WAAW,CAAS,EAC/B,CACT,CAEA,MAAc,aAAa,EAAmB,EAAiC,CAC7E,IAAM,EAAU,KAAK,YAAY,EAAW,EAAO,MAAM,KAAK,gBAAgB,CAAK,CAAC,EAEpF,OADI,GAAS,KAAK,WAAW,CAAS,EAC/B,CACT,CAGA,MAAgB,KAAwB,CACtC,IAAI,EAAU,GACR,EAAM,KAAK,IAAI,EACrB,IAAK,GAAM,CAAC,EAAW,KAAY,KAAK,SAAU,CAChD,IAAK,IAAM,KAAS,EAAQ,KAAK,KAAK,EAChC,MAAM,KAAK,aAAa,EAAW,CAAK,IAAG,EAAU,IAE3D,IAAK,GAAM,CAAC,EAAO,KAAiB,EAAQ,WACtC,CAAC,EAAQ,UAAU,IAAI,CAAK,GAAK,EAAM,GAAgB,KAAK,cAChE,EAAQ,KAAK,OAAO,CAAK,EACzB,EAAQ,WAAW,OAAO,CAAK,EAC/B,EAAQ,UAAU,OAAO,CAAK,EAC9B,KAAK,WAAW,CAAS,EACzB,EAAU,IAIR,EAAQ,KAAK,OAAS,GAAK,KAAK,SAAS,IAAI,CAAS,IAAM,GAAS,KAAK,SAAS,OAAO,CAAS,CACzG,CAEA,OAAO,CACT,CAEA,MAAM,EAAqB,CACzB,KAAK,eAAe,EAAkB,CAAK,CAC7C,CAEA,QAAQ,EAAqB,CAC3B,KAAK,iBAAiB,EAAkB,CAAK,CAC/C,CAGA,mBAAmB,EAAmB,EAAqB,CACzD,KAAK,4BAA4B,EAAW,CAAK,CACnD,CAGA,mBAAmB,EAAsC,CACvD,IAAM,EAAU,KAAK,SAAS,IAAI,CAAS,EAE3C,OADK,EACE,CAAC,GAAG,EAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAC/B,GACC,EAAI,SAAW,IAAA,IAAa,CAAC,EAA0B,IAAI,EAAI,MAAM,GAAK,CAAC,EAAQ,UAAU,IAAI,EAAI,KAAK,CAC9G,EAJqB,CAAC,CAKxB,CAGA,UAAU,EAAmB,EAAkC,CAC7D,GAAI,CAAC,EAAU,KAAK,EAAG,MAAU,MAAM,gEAAgE,EACvG,OAAO,KAAK,mBAAmB,EAAW,CAAQ,CACpD,CAEA,MAA0B,CACxB,OAAO,KAAK,cAAc,CAAgB,CAC5C,CAEA,IAAI,EAA4C,CAC9C,OAAO,KAAK,aAAa,EAAkB,CAAK,CAClD,CAEA,OAAc,CACZ,KAAK,aAAa,CAAgB,CACpC,CAEA,eAAuB,EAAmB,EAAqB,CAG7D,KAAK,QAAQ,CAAS,EACtB,KAAK,QAAQ,EAAW,CAAK,CAC/B,CAEA,iBAAyB,EAAmB,EAAqB,CAC/D,IAAM,EAAU,KAAK,SAAS,IAAI,CAAS,EACrC,EAAU,GAAS,KAAK,OAAO,CAAK,GAAK,GAC/C,GAAS,WAAW,OAAO,CAAK,EAChC,GAAS,UAAU,OAAO,CAAK,EAC3B,GAAS,KAAK,OAAS,GAAG,KAAK,SAAS,OAAO,CAAS,EACxD,GAAS,KAAK,WAAW,CAAS,CACxC,CAEA,4BAAoC,EAAmB,EAAqB,CAC1E,IAAM,EAAU,KAAK,SAAS,IAAI,CAAS,EACrC,EAAS,GAAS,KAAK,IAAI,CAAK,CAAC,EAAE,OACpC,GAAY,GAAW,EAA0B,IAAI,CAAM,GAAK,GAAQ,UAAU,IAAI,CAAK,IAChG,EAAQ,UAAU,IAAI,CAAK,EAC3B,KAAK,WAAW,CAAS,EAC3B,CAEA,mBAA2B,EAAmB,EAAkC,CAC9E,IAAM,EAAY,KAAK,UAAU,IAAI,CAAS,GAAK,IAAI,IAGvD,OAFA,EAAU,IAAI,CAAQ,EACtB,KAAK,UAAU,IAAI,EAAW,CAAS,MAC1B,CACX,EAAU,OAAO,CAAQ,EACrB,EAAU,OAAS,GAAG,KAAK,UAAU,OAAO,CAAS,CAC3D,CACF,CAEA,cAAsB,EAAsC,CAC1D,MAAO,CAAC,GAAI,KAAK,SAAS,IAAI,CAAS,CAAC,EAAE,KAAK,OAAO,GAAK,CAAC,CAAE,CAChE,CAEA,aAAqB,EAAmB,EAA4C,CAClF,OAAO,KAAK,SAAS,IAAI,CAAS,CAAC,EAAE,KAAK,IAAI,CAAK,CACrD,CAEA,aAAqB,EAAyB,CAC5C,IAAM,GAAW,KAAK,SAAS,IAAI,CAAS,CAAC,EAAE,KAAK,MAAQ,GAAK,EACjE,KAAK,SAAS,OAAO,CAAS,EAC1B,GAAS,KAAK,WAAW,CAAS,CACxC,CAEA,OAAc,CACZ,KAAK,KAAK,EACV,KAAK,eAAiB,KAAK,UAAU,SAAS,CAC5C,GAAI,EACJ,WAAY,KAAK,eACjB,QAAW,KAAK,IAAI,CACtB,CAAC,CACH,CAEA,MAAa,CACX,KAAK,iBAAiB,EACtB,KAAK,eAAiB,IAAA,GACtB,KAAK,SAAS,MAAM,EACpB,KAAK,UAAU,MAAM,CACvB,CACF"}
@@ -3,7 +3,7 @@ const e=require("../../../../_virtual/_rolldown/runtime.cjs"),t=require("../../.
3
3
  `),T=[`You are a child subagent, not the parent orchestrator.`,`The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.`,`Ignore prior parent-only orchestration instructions in inherited conversation history.`,`Do not propose or run subagents. Complete only your assigned role-specific task with the tools available to you.`,`If you need to edit files, use the available editing tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.`,w].join(`
4
4
  `),E=[`You are a child subagent with explicit fanout responsibility for this assigned task.`,`The parent session owns final orchestration, acceptance, and follow-up implementation launches.`,"You may use the `subagent` tool only for the fanout work explicitly requested in this task.",`Do not broaden yourself into general parent orchestration. Do not launch follow-up workers unless the task explicitly asks for that.`,`The maxSubagentDepth cap still applies and may block further fanout.`,`If you need to edit files, use the available editing tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.`,w].join(`
5
5
  `);function D(e,t,n){e.on.call(e,t,n)}function ee(e,t){return RegExp(`(?:^|\\D)-?${t}(?!\\d)`).test(e)}function O(e,t){if(h.test(e)||g.test(e))return b;if(m.test(e))return _.test(e)||v.test(e)||y.test(e)||/\bxargs\s+(?:[^;&|\n]+\s+)?kill(?:pg)?\b/i.test(e)||t.some(t=>ee(e,t))?b:void 0}function k(e,n={}){let r=[...new Set(n.protectedProcessIds??[...t.decodeProtectedParentProcessIds(process.env.PI_SUBAGENT_PROTECTED_PARENT_PIDS),process.pid,process.ppid])].filter(e=>Number.isSafeInteger(e)&&e>1);D(e,`tool_call`,e=>{if(e.toolName!==`bash`||!e.input||typeof e.input!=`object`)return;let t=e.input.command;if(typeof t!=`string`)return;let n=O(t,r);return n?{block:!0,reason:n}:void 0})}function te(e){return typeof e==`object`&&!!e&&e.type===`text`&&typeof e.text==`string`}function A(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(n?.role!==`assistant`||!Array.isArray(n.content))continue;let r=n.content.filter(te).map(e=>e.text).join(`
6
- `).trim();if(r)return r}return``}function j(e,n=s.createTeamPromptRuntime()){let r=process.env[t.SUBAGENT_RUN_ID_ENV]?.trim();if(!r)return;let{pollScheduler:a,bootstrap:o,execution:c,reporting:l}=n,d,f,p=0,m=new WeakSet;D(e,`session_start`,(t,n)=>{if(d)return;a.start();let s=n.sessionManager.getSessionFile(),u=s?l.recordSessionFile(s):void 0,f=o.bootstrap(r,u);f.teamContext&&Object.assign(process.env,i.nativeTeamMemberEnvironment(f.teamContext)),c.start(r,{onSteer:t=>{let n=e.sendUserMessage;if(!n){Z(t,`failed`,`Child Pi session does not support sendUserMessage steering.`);return}try{n(X(t),{deliverAs:`steer`}),Z(t,`delivered`,`Pi accepted the correlated steering input.`)}catch(e){throw Z(t,`failed`,e instanceof Error?e.message:String(e)),e}}}),d={teamContext:f.teamContext},c.setActivity(`working`)}),D(e,`agent_start`,()=>{d&&c.setActivity(`working`)}),D(e,`message_update`,e=>{if(!d||!e.message)return;let t=u.getMessageActivity(e.message);t&&c.setProgress({currentTool:t})}),D(e,`message_end`,e=>{if(!d||!e.message||typeof e.message!=`object`||m.has(e.message))return;m.add(e.message);let t=u.getMessageUsageTokens(e.message);t!==void 0&&(f=(f??0)+t,c.setProgress({tokens:f}))}),D(e,`tool_execution_start`,e=>{if(!d)return;let t=e.toolName??e.tool?.name;p+=1,c.setProgress({toolCount:p,...typeof t==`string`&&t.length>0?{currentTool:u.formatToolActivity(t,e.args)}:{}}),c.setActivity(t===`intercom`&&e.args?.action===`ask`?`waiting_for_reply`:`tool`)}),D(e,`tool_execution_end`,()=>{d&&(c.setProgress({currentTool:`working`}),c.setActivity(`working`))}),D(e,`agent_end`,e=>{if(!d)return;c.setActivity(`finalizing`);let t=A(e.messages);c.complete(!0,t)})}const ne=new Set([`subagent-orchestration-instructions`,`subagent-slash-result`,`subagent-slash-text-result`,`subagent-notify`,`subagent_control_notice`,`subagent-control`,`subagent-control-notice`]),M=/<name>\s*pi-subagents\s*<\/name>/,N=`
6
+ `).trim();if(r)return r}return``}function j(e,n=s.createTeamPromptRuntime()){let r=process.env[t.SUBAGENT_RUN_ID_ENV]?.trim();if(!r)return;let{pollScheduler:a,bootstrap:o,execution:c,reporting:l}=n,d,f,p=0,m=0,h=new WeakSet;D(e,`session_start`,(t,n)=>{if(d)return;a.start();let s=n.sessionManager.getSessionFile(),u=s?l.recordSessionFile(s):void 0,f=o.bootstrap(r,u);f.teamContext&&Object.assign(process.env,i.nativeTeamMemberEnvironment(f.teamContext)),c.start(r,{onSteer:t=>{let n=e.sendUserMessage;if(!n){Z(t,`failed`,`Child Pi session does not support sendUserMessage steering.`);return}try{n(X(t),{deliverAs:`steer`}),Z(t,`delivered`,`Pi accepted the correlated steering input.`)}catch(e){throw Z(t,`failed`,e instanceof Error?e.message:String(e)),e}}}),d={teamContext:f.teamContext},c.setActivity(`working`)}),D(e,`agent_start`,()=>{d&&c.setActivity(`working`)}),D(e,`message_update`,e=>{if(!d||!e.message)return;let t=u.getMessageActivity(e.message);t&&c.setProgress({currentTool:t})}),D(e,`message_end`,e=>{if(!d||!e.message||typeof e.message!=`object`||h.has(e.message))return;h.add(e.message);let t=u.getMessageUsageTokens(e.message);t!==void 0&&(f=(f??0)+t,p+=u.getMessageUsageCost(e.message)??0,c.setProgress({tokens:f,cost:p}))}),D(e,`tool_execution_start`,e=>{if(!d)return;let t=e.toolName??e.tool?.name;m+=1,c.setProgress({toolCount:m,...typeof t==`string`&&t.length>0?{currentTool:u.formatToolActivity(t,e.args)}:{}}),c.setActivity(t===`intercom`&&e.args?.action===`ask`?`waiting_for_reply`:`tool`)}),D(e,`tool_execution_end`,()=>{d&&(c.setProgress({currentTool:`working`}),c.setActivity(`working`))}),D(e,`agent_end`,e=>{if(!d)return;c.setActivity(`finalizing`);let t=A(e.messages);c.complete(!0,t)})}const ne=new Set([`subagent-orchestration-instructions`,`subagent-slash-result`,`subagent-slash-text-result`,`subagent-notify`,`subagent_control_notice`,`subagent-control`,`subagent-control-notice`]),M=/<name>\s*pi-subagents\s*<\/name>/,N=`
7
7
 
8
8
  The following skills provide specialized instructions for specific tasks.`,P=`
9
9
  Current date:`;function F(e){let t=process.env[e];if(t!==void 0)return t!==`0`}function I(){let e=process.env[t.REQUIRED_CHILD_TOOLS_ENV]?.trim();if(!e)return;let n=JSON.parse(e);if(!Array.isArray(n)||n.some(e=>typeof e!=`string`||!e))throw Error(`Invalid ${t.REQUIRED_CHILD_TOOLS_ENV} payload.`);return n}function L(){let e=process.env[t.MCP_DIRECT_CHILD_TOOLS_ENV]?.trim();if(e)try{let t=JSON.parse(e);return!Array.isArray(t)||t.some(e=>typeof e!=`string`||!e)?void 0:t}catch{return}}function R(e){let n=process.env[t.CHILD_TOOL_DIAGNOSTIC_PATH_ENV]?.trim(),r=I();if(!n||!r)return;let i=e.getAllTools().map(e=>e.name);return l.writeChildToolDiagnostic(n,r,i,process.env[t.SUBAGENT_CHILD_AGENT_ENV]?.trim(),L())}function z(e,t,n){let r=e.length;for(let i of n){let n=e.indexOf(i,t);n!==-1&&n<r&&(r=n)}return r}function B(e){let t=e.indexOf(`