@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5

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 (347) hide show
  1. package/CHANGELOG.md +1654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1533 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +199 -0
  43. package/dist/history/ingest.js +468 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +466 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/bridge.js +750 -0
  53. package/dist/pi-intercom/broker/broker.js +426 -0
  54. package/dist/pi-intercom/broker/client.js +492 -0
  55. package/dist/pi-intercom/broker/framing.js +65 -0
  56. package/dist/pi-intercom/broker/paths.js +42 -0
  57. package/dist/pi-intercom/broker/pid.js +66 -0
  58. package/dist/pi-intercom/broker/spawn.js +221 -0
  59. package/dist/pi-intercom/config.js +102 -0
  60. package/dist/pi-intercom/index.js +2942 -0
  61. package/dist/pi-intercom/reply-tracker.js +182 -0
  62. package/dist/pi-intercom/session-targets.js +128 -0
  63. package/dist/pi-intercom/subagent-live-events.js +132 -0
  64. package/dist/pi-intercom/topics.js +157 -0
  65. package/dist/pi-intercom/types.js +163 -0
  66. package/dist/pi-intercom/ui/compose.js +269 -0
  67. package/dist/pi-intercom/ui/inline-message.js +80 -0
  68. package/dist/pi-intercom/ui/session-list.js +124 -0
  69. package/dist/runs/background/async-control.js +95 -0
  70. package/dist/runs/background/async-execution.js +676 -0
  71. package/dist/runs/background/async-job-tracker.js +383 -0
  72. package/dist/runs/background/async-result-file.js +58 -0
  73. package/dist/runs/background/async-resume.js +305 -0
  74. package/dist/runs/background/async-status.js +442 -0
  75. package/dist/runs/background/completion-dedupe.js +69 -0
  76. package/dist/runs/background/completion-delivery.js +245 -0
  77. package/dist/runs/background/notify.js +71 -0
  78. package/dist/runs/background/parallel-groups.js +55 -0
  79. package/dist/runs/background/result-watcher.js +426 -0
  80. package/dist/runs/background/run-id-resolver.js +64 -0
  81. package/dist/runs/background/run-status.js +382 -0
  82. package/dist/runs/background/stale-run-reconciler.js +335 -0
  83. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  84. package/dist/runs/background/subagent-runner.js +1872 -0
  85. package/dist/runs/background/top-level-async.js +5 -0
  86. package/dist/runs/foreground/chain-clarify.js +1152 -0
  87. package/dist/runs/foreground/clarify-invocation.js +72 -0
  88. package/dist/runs/foreground/execution-input.js +423 -0
  89. package/dist/runs/foreground/foreground-control.js +817 -0
  90. package/dist/runs/foreground/question-control.js +88 -0
  91. package/dist/runs/foreground/run-async-path.js +205 -0
  92. package/dist/runs/foreground/subagent-executor.js +442 -0
  93. package/dist/runs/foreground/subagent-params.js +165 -0
  94. package/dist/runs/foreground/wait-run.js +141 -0
  95. package/dist/runs/shared/acceptance-contract.js +253 -0
  96. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  97. package/dist/runs/shared/acceptance-finalization.js +280 -0
  98. package/dist/runs/shared/acceptance-reports.js +147 -0
  99. package/dist/runs/shared/acceptance.js +4 -0
  100. package/dist/runs/shared/chain-outputs.js +110 -0
  101. package/dist/runs/shared/child-attempt.js +693 -0
  102. package/dist/runs/shared/child-execution-cwd.js +96 -0
  103. package/dist/runs/shared/child-observations.js +114 -0
  104. package/dist/runs/shared/claude-code.js +199 -0
  105. package/dist/runs/shared/completion-guard.js +31 -0
  106. package/dist/runs/shared/dynamic-fanout.js +274 -0
  107. package/dist/runs/shared/history-index.js +72 -0
  108. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  109. package/dist/runs/shared/model-fallback.js +185 -0
  110. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  111. package/dist/runs/shared/native-finalization.js +101 -0
  112. package/dist/runs/shared/native-usage.js +153 -0
  113. package/dist/runs/shared/nested-events.js +700 -0
  114. package/dist/runs/shared/nested-path.js +48 -0
  115. package/dist/runs/shared/nested-render.js +108 -0
  116. package/dist/runs/shared/parallel-utils.js +61 -0
  117. package/dist/runs/shared/parent-receipts.js +68 -0
  118. package/dist/runs/shared/parent-usage.js +121 -0
  119. package/dist/runs/shared/pi-args.js +310 -0
  120. package/dist/runs/shared/pi-spawn.js +52 -0
  121. package/dist/runs/shared/run-history-queries.js +45 -0
  122. package/dist/runs/shared/run-history.js +110 -0
  123. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  124. package/dist/runs/shared/run-records.js +679 -0
  125. package/dist/runs/shared/session-cwd-preload.js +33 -0
  126. package/dist/runs/shared/single-output.js +222 -0
  127. package/dist/runs/shared/streaming-text.js +15 -0
  128. package/dist/runs/shared/structured-output.js +55 -0
  129. package/dist/runs/shared/subagent-control.js +179 -0
  130. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  131. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  132. package/dist/runs/shared/supervisor-questions.js +350 -0
  133. package/dist/runs/shared/workflow-graph.js +214 -0
  134. package/dist/runs/shared/workflow-policy.js +84 -0
  135. package/dist/runs/shared/worktree.js +525 -0
  136. package/dist/shared/agent-context-policy.js +126 -0
  137. package/dist/shared/artifacts.js +96 -0
  138. package/dist/shared/atomic-json.js +13 -0
  139. package/dist/shared/cooperative.js +20 -0
  140. package/dist/shared/execution-cwd.js +14 -0
  141. package/dist/shared/file-coalescer.js +21 -0
  142. package/dist/shared/fork-context.js +49 -0
  143. package/dist/shared/formatters.js +85 -0
  144. package/dist/shared/journal-reader.js +407 -0
  145. package/dist/shared/lazy-tools.js +32 -0
  146. package/dist/shared/model-info.js +68 -0
  147. package/dist/shared/native-session.js +15 -0
  148. package/dist/shared/native-tui.js +18 -0
  149. package/dist/shared/native-typebox.js +20 -0
  150. package/dist/shared/post-exit-stdio-guard.js +197 -0
  151. package/dist/shared/prompt-sections.js +8 -0
  152. package/dist/shared/session-entries.js +52 -0
  153. package/dist/shared/session-identity.js +14 -0
  154. package/dist/shared/settings.js +254 -0
  155. package/dist/shared/status-format.js +111 -0
  156. package/dist/shared/temp-root.js +114 -0
  157. package/dist/shared/types.js +207 -0
  158. package/dist/shared/ui-mode.js +3 -0
  159. package/dist/shared/utils.js +334 -0
  160. package/dist/slash/prompt-template-bridge.js +298 -0
  161. package/dist/slash/slash-bridge.js +116 -0
  162. package/dist/slash/slash-commands.js +607 -0
  163. package/dist/slash/slash-live-state.js +246 -0
  164. package/dist/tui/action-hints.js +75 -0
  165. package/dist/tui/agent-history.js +197 -0
  166. package/dist/tui/agent-view.js +1656 -0
  167. package/dist/tui/render.js +1515 -0
  168. package/docs/intercom-bridge.md +212 -0
  169. package/docs/intercom.md +581 -0
  170. package/docs/performance.md +121 -0
  171. package/package.json +102 -4
  172. package/prompts/gather-context-and-clarify.md +14 -0
  173. package/prompts/parallel-cleanup.md +62 -0
  174. package/prompts/parallel-context-build.md +56 -0
  175. package/prompts/parallel-handoff-plan.md +62 -0
  176. package/prompts/parallel-research.md +51 -0
  177. package/prompts/parallel-review.md +59 -0
  178. package/prompts/review-loop.md +42 -0
  179. package/scripts/build.mjs +134 -0
  180. package/scripts/check-compat.mjs +48 -0
  181. package/scripts/compat-host.mjs +19 -0
  182. package/scripts/intercom-bridge-credentials.mjs +143 -0
  183. package/scripts/intercom-remote.mjs +103 -0
  184. package/scripts/linux-smoke.sh +51 -0
  185. package/scripts/local-install-smoke.mjs +66 -0
  186. package/scripts/native-package-smoke.mjs +82 -0
  187. package/scripts/package-smoke.mjs +209 -0
  188. package/scripts/prepare.mjs +67 -0
  189. package/scripts/real-pi-smoke.mjs +415 -0
  190. package/scripts/run-tests.mjs +149 -0
  191. package/skills/pi-intercom/SKILL.md +113 -0
  192. package/skills/pi-intercom/evals/evals.json +29 -0
  193. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  194. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  195. package/skills/pi-subagents/SKILL.md +76 -0
  196. package/skills/pi-subagents/evals/evals.json +29 -0
  197. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  198. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  199. package/src/agents/agent-management.ts +813 -0
  200. package/src/agents/agent-scope.ts +7 -0
  201. package/src/agents/agent-selection.ts +23 -0
  202. package/src/agents/agent-serializer.ts +99 -0
  203. package/src/agents/agents.ts +916 -0
  204. package/src/agents/chain-serializer.ts +329 -0
  205. package/src/agents/frontmatter.ts +21 -0
  206. package/src/agents/identity.ts +30 -0
  207. package/src/agents/skills.ts +637 -0
  208. package/src/extension/build-info.ts +2 -0
  209. package/src/extension/compact-tools.ts +114 -0
  210. package/src/extension/config.ts +23 -0
  211. package/src/extension/control-notices.ts +47 -0
  212. package/src/extension/doctor.ts +171 -0
  213. package/src/extension/fanout-child.ts +312 -0
  214. package/src/extension/index.ts +649 -0
  215. package/src/extension/schemas.ts +444 -0
  216. package/src/extension/tool-input.ts +19 -0
  217. package/src/extension/tool-result.ts +16 -0
  218. package/src/history/canonical-result.ts +62 -0
  219. package/src/history/database.ts +59 -0
  220. package/src/history/index.ts +156 -0
  221. package/src/history/ingest.ts +349 -0
  222. package/src/history/queries.ts +241 -0
  223. package/src/history/store.ts +203 -0
  224. package/src/history/types.ts +166 -0
  225. package/src/history/worker.ts +321 -0
  226. package/src/intercom/intercom-bridge.ts +123 -0
  227. package/src/intercom/live-intercom.ts +104 -0
  228. package/src/intercom/result-intercom.ts +429 -0
  229. package/src/pi-intercom/agent-dir.ts +9 -0
  230. package/src/pi-intercom/bridge.ts +668 -0
  231. package/src/pi-intercom/broker/broker.ts +463 -0
  232. package/src/pi-intercom/broker/client.ts +558 -0
  233. package/src/pi-intercom/broker/framing.ts +77 -0
  234. package/src/pi-intercom/broker/paths.ts +47 -0
  235. package/src/pi-intercom/broker/pid.ts +59 -0
  236. package/src/pi-intercom/broker/spawn.ts +248 -0
  237. package/src/pi-intercom/config.ts +142 -0
  238. package/src/pi-intercom/index.ts +2929 -0
  239. package/src/pi-intercom/reply-tracker.ts +211 -0
  240. package/src/pi-intercom/session-targets.ts +172 -0
  241. package/src/pi-intercom/subagent-live-events.ts +133 -0
  242. package/src/pi-intercom/topics.ts +125 -0
  243. package/src/pi-intercom/types.ts +271 -0
  244. package/src/pi-intercom/ui/compose.ts +289 -0
  245. package/src/pi-intercom/ui/inline-message.ts +92 -0
  246. package/src/pi-intercom/ui/session-list.ts +139 -0
  247. package/src/runs/background/async-control.ts +89 -0
  248. package/src/runs/background/async-execution.ts +850 -0
  249. package/src/runs/background/async-job-tracker.ts +385 -0
  250. package/src/runs/background/async-result-file.ts +62 -0
  251. package/src/runs/background/async-resume.ts +333 -0
  252. package/src/runs/background/async-status.ts +481 -0
  253. package/src/runs/background/completion-dedupe.ts +79 -0
  254. package/src/runs/background/completion-delivery.ts +195 -0
  255. package/src/runs/background/notify.ts +123 -0
  256. package/src/runs/background/parallel-groups.ts +53 -0
  257. package/src/runs/background/result-watcher.ts +423 -0
  258. package/src/runs/background/run-id-resolver.ts +69 -0
  259. package/src/runs/background/run-status.ts +384 -0
  260. package/src/runs/background/stale-run-reconciler.ts +369 -0
  261. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  262. package/src/runs/background/subagent-runner.ts +2053 -0
  263. package/src/runs/background/top-level-async.ts +13 -0
  264. package/src/runs/foreground/chain-clarify.ts +1340 -0
  265. package/src/runs/foreground/clarify-invocation.ts +77 -0
  266. package/src/runs/foreground/execution-input.ts +467 -0
  267. package/src/runs/foreground/foreground-control.ts +821 -0
  268. package/src/runs/foreground/question-control.ts +82 -0
  269. package/src/runs/foreground/run-async-path.ts +243 -0
  270. package/src/runs/foreground/subagent-executor.ts +501 -0
  271. package/src/runs/foreground/subagent-params.ts +320 -0
  272. package/src/runs/foreground/wait-run.ts +124 -0
  273. package/src/runs/shared/acceptance-contract.ts +257 -0
  274. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  275. package/src/runs/shared/acceptance-finalization.ts +353 -0
  276. package/src/runs/shared/acceptance-reports.ts +152 -0
  277. package/src/runs/shared/acceptance.ts +33 -0
  278. package/src/runs/shared/chain-outputs.ts +114 -0
  279. package/src/runs/shared/child-attempt.ts +623 -0
  280. package/src/runs/shared/child-execution-cwd.ts +91 -0
  281. package/src/runs/shared/child-observations.ts +99 -0
  282. package/src/runs/shared/claude-code.ts +243 -0
  283. package/src/runs/shared/completion-guard.ts +35 -0
  284. package/src/runs/shared/dynamic-fanout.ts +295 -0
  285. package/src/runs/shared/history-index.ts +69 -0
  286. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  287. package/src/runs/shared/model-fallback.ts +219 -0
  288. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  289. package/src/runs/shared/native-finalization.ts +128 -0
  290. package/src/runs/shared/native-usage.ts +113 -0
  291. package/src/runs/shared/nested-events.ts +752 -0
  292. package/src/runs/shared/nested-path.ts +52 -0
  293. package/src/runs/shared/nested-render.ts +119 -0
  294. package/src/runs/shared/parallel-utils.ts +153 -0
  295. package/src/runs/shared/parent-receipts.ts +59 -0
  296. package/src/runs/shared/parent-usage.ts +121 -0
  297. package/src/runs/shared/pi-args.ts +350 -0
  298. package/src/runs/shared/pi-spawn.ts +52 -0
  299. package/src/runs/shared/run-history-queries.ts +44 -0
  300. package/src/runs/shared/run-history.ts +100 -0
  301. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  302. package/src/runs/shared/run-records.ts +607 -0
  303. package/src/runs/shared/session-cwd-preload.ts +27 -0
  304. package/src/runs/shared/single-output.ts +267 -0
  305. package/src/runs/shared/streaming-text.ts +18 -0
  306. package/src/runs/shared/structured-output.ts +68 -0
  307. package/src/runs/shared/subagent-control.ts +219 -0
  308. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  309. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  310. package/src/runs/shared/supervisor-questions.ts +415 -0
  311. package/src/runs/shared/workflow-graph.ts +220 -0
  312. package/src/runs/shared/workflow-policy.ts +125 -0
  313. package/src/runs/shared/worktree.ts +635 -0
  314. package/src/shared/agent-context-policy.ts +184 -0
  315. package/src/shared/artifacts.ts +95 -0
  316. package/src/shared/atomic-json.ts +16 -0
  317. package/src/shared/cooperative.ts +21 -0
  318. package/src/shared/execution-cwd.ts +14 -0
  319. package/src/shared/file-coalescer.ts +29 -0
  320. package/src/shared/fork-context.ts +68 -0
  321. package/src/shared/formatters.ts +84 -0
  322. package/src/shared/journal-reader.ts +333 -0
  323. package/src/shared/lazy-tools.ts +36 -0
  324. package/src/shared/model-info.ts +81 -0
  325. package/src/shared/native-session.ts +8 -0
  326. package/src/shared/native-tui.ts +10 -0
  327. package/src/shared/native-typebox.ts +14 -0
  328. package/src/shared/post-exit-stdio-guard.ts +204 -0
  329. package/src/shared/prompt-sections.ts +10 -0
  330. package/src/shared/session-entries.ts +49 -0
  331. package/src/shared/session-identity.ts +18 -0
  332. package/src/shared/settings.ts +384 -0
  333. package/src/shared/status-format.ts +123 -0
  334. package/src/shared/temp-root.ts +107 -0
  335. package/src/shared/types.ts +1260 -0
  336. package/src/shared/ui-mode.ts +5 -0
  337. package/src/shared/utils.ts +342 -0
  338. package/src/slash/prompt-template-bridge.ts +408 -0
  339. package/src/slash/slash-bridge.ts +178 -0
  340. package/src/slash/slash-commands.ts +617 -0
  341. package/src/slash/slash-live-state.ts +292 -0
  342. package/src/tui/action-hints.ts +69 -0
  343. package/src/tui/agent-history.ts +210 -0
  344. package/src/tui/agent-view.ts +1323 -0
  345. package/src/tui/render.ts +1550 -0
  346. package/tsconfig.build.json +12 -0
  347. package/tsconfig.json +19 -0
@@ -0,0 +1,481 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
+ import { formatRunAction, formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
6
+ import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
7
+ import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
8
+ import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
9
+ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
10
+ import { asyncRunRoots, readAsyncRunRecord, type AsyncRunRecord } from "./async-resume.ts";
11
+ import { RESULTS_DIR } from "../../shared/types.ts";
12
+ import { getRunMetadataDir, readRunJson } from "../shared/supervisor-questions.ts";
13
+ import { runSynchronously } from "../../shared/cooperative.ts";
14
+
15
+ interface AsyncRunStepSummary {
16
+ index: number;
17
+ sessionFile?: string;
18
+ agent: string;
19
+ label?: string;
20
+ phase?: string;
21
+ outputName?: string;
22
+ structured?: boolean;
23
+ status: AsyncJobStep["status"];
24
+ activityState?: ActivityState;
25
+ lastActivityAt?: number;
26
+ currentTool?: string;
27
+ currentToolArgs?: string;
28
+ currentToolStartedAt?: number;
29
+ currentPath?: string;
30
+ recentTools?: Array<{ tool: string; args: string; endMs: number }>;
31
+ recentOutput?: string[];
32
+ turnCount?: number;
33
+ toolCount?: number;
34
+ durationMs?: number;
35
+ tokens?: TokenUsage;
36
+ skills?: string[];
37
+ model?: string;
38
+ thinking?: string;
39
+ attemptedModels?: string[];
40
+ error?: string;
41
+ children?: NestedRunSummary[];
42
+ }
43
+
44
+ export interface AsyncRunSummary {
45
+ id: string;
46
+ asyncDir: string;
47
+ pid?: number;
48
+ sessionId?: string;
49
+ state: "queued" | "running" | "complete" | "failed" | "blocked" | "paused";
50
+ activityState?: ActivityState;
51
+ lastActivityAt?: number;
52
+ currentTool?: string;
53
+ currentToolStartedAt?: number;
54
+ currentPath?: string;
55
+ turnCount?: number;
56
+ toolCount?: number;
57
+ mode: SubagentRunMode;
58
+ cwd?: string;
59
+ startedAt: number;
60
+ lastUpdate?: number;
61
+ endedAt?: number;
62
+ currentStep?: number;
63
+ chainStepCount?: number;
64
+ parallelGroups?: AsyncParallelGroupStatus[];
65
+ steps: AsyncRunStepSummary[];
66
+ sessionDir?: string;
67
+ outputFile?: string;
68
+ totalTokens?: TokenUsage;
69
+ sessionFile?: string;
70
+ nestedChildren?: NestedRunSummary[];
71
+ nestedWarnings?: string[];
72
+ }
73
+
74
+ interface AsyncRunListOptions {
75
+ states?: Array<AsyncRunSummary["state"]>;
76
+ sessionId?: string;
77
+ limit?: number;
78
+ resultsDir?: string;
79
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
80
+ now?: () => number;
81
+ reconcile?: boolean;
82
+ skipInvalid?: boolean;
83
+ records?: AsyncRunRecord[];
84
+ }
85
+
86
+ interface AsyncRunDiscoveryOptions extends Omit<AsyncRunListOptions, "states" | "limit" | "records"> {
87
+ ownerSessionId?: string;
88
+ receiptRunIds?: () => Iterable<string>;
89
+ }
90
+
91
+ function getErrorMessage(error: unknown): string {
92
+ return error instanceof Error ? error.message : String(error);
93
+ }
94
+
95
+ function isNotFoundError(error: unknown): boolean {
96
+ return typeof error === "object"
97
+ && error !== null
98
+ && "code" in error
99
+ && (error as NodeJS.ErrnoException).code === "ENOENT";
100
+ }
101
+
102
+ function isAsyncRunDir(root: string, entry: string): boolean {
103
+ const entryPath = path.join(root, entry);
104
+ try {
105
+ return fs.statSync(entryPath).isDirectory();
106
+ } catch (error) {
107
+ if (isNotFoundError(error)) return false;
108
+ throw new Error(`Failed to inspect async run path '${entryPath}': ${getErrorMessage(error)}`, {
109
+ cause: error instanceof Error ? error : undefined,
110
+ });
111
+ }
112
+ }
113
+
114
+ function outputFileMtime(outputFile: string | undefined): number | undefined {
115
+ if (!outputFile) return undefined;
116
+ try {
117
+ return fs.statSync(outputFile).mtimeMs;
118
+ } catch (error) {
119
+ if (isNotFoundError(error)) return undefined;
120
+ throw new Error(`Failed to inspect async output file '${outputFile}': ${getErrorMessage(error)}`, {
121
+ cause: error instanceof Error ? error : undefined,
122
+ });
123
+ }
124
+ }
125
+
126
+ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { activityState?: ActivityState; lastActivityAt?: number } {
127
+ if (status.state !== "running") return { activityState: status.activityState, lastActivityAt: status.lastActivityAt };
128
+ const outputPath = status.outputFile ? (path.isAbsolute(status.outputFile) ? status.outputFile : path.join(asyncDir, status.outputFile)) : undefined;
129
+ const currentStep = typeof status.currentStep === "number" ? status.steps?.[status.currentStep] : undefined;
130
+ return {
131
+ activityState: status.activityState,
132
+ lastActivityAt: status.lastActivityAt ?? outputFileMtime(outputPath) ?? currentStep?.lastActivityAt ?? currentStep?.startedAt ?? status.startedAt,
133
+ };
134
+ }
135
+
136
+ function assertOptionalFields(record: Record<string, unknown>, fields: string[], valid: (value: unknown) => boolean, expected: string, source: string): void {
137
+ for (const field of fields) {
138
+ if (record[field] !== undefined && !valid(record[field])) {
139
+ throw new Error(`Invalid async status '${source}': ${field} must be ${expected}.`);
140
+ }
141
+ }
142
+ }
143
+
144
+ function validateTokenUsage(value: unknown, field: string, source: string): void {
145
+ if (value === undefined) return;
146
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
147
+ throw new Error(`Invalid async status '${source}': ${field} must be an object.`);
148
+ }
149
+ const tokens = value as Record<string, unknown>;
150
+ if (![tokens.input, tokens.output, tokens.total].every((part) => typeof part === "number" && Number.isFinite(part))) {
151
+ throw new Error(`Invalid async status '${source}': ${field} must contain finite input, output, and total numbers.`);
152
+ }
153
+ }
154
+
155
+ function validateStatusForSummary(status: AsyncStatus, source: string): void {
156
+ if (!status || typeof status !== "object" || Array.isArray(status)) {
157
+ throw new Error(`Invalid async status '${source}': status must be an object.`);
158
+ }
159
+ const record = status as unknown as Record<string, unknown>;
160
+ assertOptionalFields(record, ["runId", "sessionId", "currentTool", "currentPath", "cwd", "sessionDir", "outputFile", "sessionFile"], (value) => typeof value === "string", "a string", source);
161
+ assertOptionalFields(record, ["lastActivityAt", "currentToolStartedAt", "turnCount", "toolCount", "startedAt", "endedAt", "lastUpdate", "pid", "currentStep", "chainStepCount"], (value) => typeof value === "number" && Number.isFinite(value), "a finite number", source);
162
+ if (typeof record.runId !== "string") throw new Error(`Invalid async status '${source}': runId must be a string.`);
163
+ if (typeof record.startedAt !== "number") throw new Error(`Invalid async status '${source}': startedAt must be a number.`);
164
+ if (!(["single", "parallel", "chain"] as unknown[]).includes(record.mode)) throw new Error(`Invalid async status '${source}': mode is invalid.`);
165
+ if (!(["queued", "running", "complete", "failed", "blocked", "paused"] as unknown[]).includes(record.state)) throw new Error(`Invalid async status '${source}': state is invalid.`);
166
+ if (record.activityState !== undefined && record.activityState !== "needs_attention") throw new Error(`Invalid async status '${source}': activityState is invalid.`);
167
+ validateTokenUsage(record.totalTokens, "totalTokens", source);
168
+ if (record.steps !== undefined && !Array.isArray(record.steps)) throw new Error(`Invalid async status '${source}': steps must be an array.`);
169
+ const steps = Array.isArray(record.steps) ? record.steps : [];
170
+ if (record.currentStep !== undefined && (typeof record.currentStep !== "number" || !Number.isSafeInteger(record.currentStep) || record.currentStep < 0 || record.currentStep >= steps.length)) {
171
+ throw new Error(`Invalid async status '${source}': currentStep must index a persisted step.`);
172
+ }
173
+ if (record.chainStepCount !== undefined && (typeof record.chainStepCount !== "number" || !Number.isSafeInteger(record.chainStepCount) || record.chainStepCount < 1)) {
174
+ throw new Error(`Invalid async status '${source}': chainStepCount must be a positive safe integer.`);
175
+ }
176
+ for (const [index, value] of steps.entries()) {
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async status '${source}': steps[${index}] must be an object.`);
178
+ const step = value as Record<string, unknown>;
179
+ const stepSource = `${source} (steps[${index}])`;
180
+ assertOptionalFields(step, ["agent", "phase", "label", "outputName", "sessionFile", "currentTool", "currentToolArgs", "currentPath", "model", "thinking", "error"], (field) => typeof field === "string", "a string", stepSource);
181
+ assertOptionalFields(step, ["lastActivityAt", "currentToolStartedAt", "turnCount", "toolCount", "startedAt", "endedAt", "durationMs"], (field) => typeof field === "number" && Number.isFinite(field), "a finite number", stepSource);
182
+ if (typeof step.agent !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].agent must be a string.`);
183
+ if (!(["pending", "running", "complete", "completed", "failed", "blocked", "paused", "timed-out"] as unknown[]).includes(step.status)) throw new Error(`Invalid async status '${source}': steps[${index}].status is invalid.`);
184
+ if (step.activityState !== undefined && step.activityState !== "needs_attention") throw new Error(`Invalid async status '${source}': steps[${index}].activityState is invalid.`);
185
+ if (step.structured !== undefined && typeof step.structured !== "boolean") throw new Error(`Invalid async status '${source}': steps[${index}].structured must be a boolean.`);
186
+ for (const field of ["recentOutput", "skills", "attemptedModels"]) {
187
+ if (step[field] !== undefined && (!Array.isArray(step[field]) || !(step[field] as unknown[]).every((item) => typeof item === "string"))) {
188
+ throw new Error(`Invalid async status '${source}': steps[${index}].${field} must be an array of strings.`);
189
+ }
190
+ }
191
+ if (step.recentTools !== undefined) {
192
+ const validRecentTools = Array.isArray(step.recentTools) && step.recentTools.every((tool) => tool
193
+ && typeof tool === "object"
194
+ && !Array.isArray(tool)
195
+ && typeof (tool as Record<string, unknown>).tool === "string"
196
+ && typeof (tool as Record<string, unknown>).args === "string"
197
+ && typeof (tool as Record<string, unknown>).endMs === "number"
198
+ && Number.isFinite((tool as Record<string, unknown>).endMs));
199
+ if (!validRecentTools) throw new Error(`Invalid async status '${source}': steps[${index}].recentTools is invalid.`);
200
+ }
201
+ if (step.children !== undefined && !Array.isArray(step.children)) throw new Error(`Invalid async status '${source}': steps[${index}].children must be an array.`);
202
+ validateTokenUsage(step.tokens, `steps[${index}].tokens`, source);
203
+ }
204
+ }
205
+
206
+ export function asyncStatusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = [], projectedChildren?: NestedRunSummary[]): AsyncRunSummary {
207
+ validateStatusForSummary(status, path.join(asyncDir, "status.json"));
208
+ const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
209
+ const steps = status.steps ?? [];
210
+ const chainStepCount = status.chainStepCount ?? steps.length;
211
+ const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
212
+ let nestedChildren: NestedRunSummary[] = projectedChildren ?? [];
213
+ if (projectedChildren === undefined && nestedWarnings.length === 0) {
214
+ try {
215
+ nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir))?.children ?? [];
216
+ } catch (error) {
217
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
218
+ }
219
+ }
220
+ const summarizedSteps = steps.map((step, index) => {
221
+ const stepActivityState = step.activityState;
222
+ const stepLastActivityAt = step.lastActivityAt;
223
+ const stepChildren = (step.children ?? []).map((child) => sanitizeSummary(child)).filter((child): child is NestedRunSummary => Boolean(child));
224
+ return {
225
+ index,
226
+ agent: step.agent,
227
+ ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
228
+ ...(step.label ? { label: step.label } : {}),
229
+ ...(step.phase ? { phase: step.phase } : {}),
230
+ ...(step.outputName ? { outputName: step.outputName } : {}),
231
+ ...(step.structured ? { structured: step.structured } : {}),
232
+ status: step.status,
233
+ ...(stepActivityState ? { activityState: stepActivityState } : {}),
234
+ ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
235
+ ...(step.currentTool ? { currentTool: step.currentTool } : {}),
236
+ ...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
237
+ ...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
238
+ ...(step.currentPath ? { currentPath: step.currentPath } : {}),
239
+ ...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
240
+ ...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
241
+ ...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
242
+ ...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
243
+ ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
244
+ ...(step.tokens ? { tokens: step.tokens } : {}),
245
+ ...(step.skills ? { skills: step.skills } : {}),
246
+ ...(step.model ? { model: step.model } : {}),
247
+ ...(step.thinking ? { thinking: step.thinking } : {}),
248
+ ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
249
+ ...(step.error ? { error: step.error } : {}),
250
+ ...(stepChildren.length ? { children: stepChildren } : {}),
251
+ };
252
+ });
253
+ attachRootChildrenToSteps(status.runId || path.basename(asyncDir), summarizedSteps, nestedChildren);
254
+ return {
255
+ id: status.runId || path.basename(asyncDir),
256
+ asyncDir,
257
+ ...(typeof status.pid === "number" ? { pid: status.pid } : {}),
258
+ ...(status.sessionId ? { sessionId: status.sessionId } : {}),
259
+ state: status.state,
260
+ activityState,
261
+ lastActivityAt,
262
+ currentTool: status.currentTool,
263
+ currentToolStartedAt: status.currentToolStartedAt,
264
+ currentPath: status.currentPath,
265
+ turnCount: status.turnCount,
266
+ toolCount: status.toolCount,
267
+ mode: status.mode,
268
+ cwd: status.cwd,
269
+ startedAt: status.startedAt,
270
+ lastUpdate: status.lastUpdate,
271
+ endedAt: status.endedAt,
272
+ currentStep: status.currentStep,
273
+ ...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
274
+ ...(parallelGroups.length ? { parallelGroups } : {}),
275
+ steps: summarizedSteps,
276
+ ...(nestedChildren.length ? { nestedChildren } : {}),
277
+ ...(nestedWarnings.length ? { nestedWarnings } : {}),
278
+ ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
279
+ ...(status.outputFile ? { outputFile: status.outputFile } : {}),
280
+ ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
281
+ ...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
282
+ };
283
+ }
284
+
285
+ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
286
+ const rank = (state: AsyncRunSummary["state"]): number => {
287
+ switch (state) {
288
+ case "running": return 0;
289
+ case "queued": return 1;
290
+ case "failed": return 2;
291
+ case "blocked": return 2;
292
+ case "paused": return 2;
293
+ case "complete": return 3;
294
+ }
295
+ };
296
+ return [...runs].sort((a, b) => {
297
+ const byState = rank(a.state) - rank(b.state);
298
+ if (byState !== 0) return byState;
299
+ const aTime = a.lastUpdate ?? a.endedAt ?? a.startedAt;
300
+ const bTime = b.lastUpdate ?? b.endedAt ?? b.startedAt;
301
+ return bTime - aTime;
302
+ });
303
+ }
304
+
305
+ /** Retry new/unpublished runs without retaining payloads or negative ownership facts. */
306
+ export function createAsyncRunDiscovery(asyncDirRoot: string, options: AsyncRunDiscoveryOptions = {}): (() => AsyncRunRecord[]) & { steps: () => Generator<void, AsyncRunRecord[]> } {
307
+ const owners = new Map<string, string>();
308
+ const discovered = new Set<string>();
309
+ const sessionIds = new Set([options.sessionId, options.ownerSessionId].filter((id): id is string => Boolean(id)));
310
+ // A legacy file-path-only query cannot classify native UUID owners as foreign.
311
+ const ownerSessionId = options.ownerSessionId ?? (options.sessionId && !path.isAbsolute(options.sessionId) ? options.sessionId : undefined);
312
+ function* steps(): Generator<void, AsyncRunRecord[]> {
313
+ const receipts = new Set(options.receiptRunIds?.());
314
+ const entries = new Set(receipts);
315
+ for (const root of asyncRunRoots(asyncDirRoot)) try {
316
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
317
+ yield;
318
+ try {
319
+ if (entry.isDirectory() || (entry.isSymbolicLink() && isAsyncRunDir(root, entry.name))) entries.add(entry.name);
320
+ } catch (error) {
321
+ if (!options.skipInvalid) throw error;
322
+ console.error(`Skipping invalid async run '${path.join(root, entry.name)}':`, error);
323
+ }
324
+ }
325
+ } catch (error) {
326
+ if (isNotFoundError(error)) continue;
327
+ throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
328
+ cause: error instanceof Error ? error : undefined,
329
+ });
330
+ }
331
+
332
+ const records: AsyncRunRecord[] = [];
333
+ for (const entry of entries) {
334
+ yield;
335
+ try {
336
+ if (discovered.has(entry)) continue;
337
+ let owner = owners.get(entry);
338
+ if (!owner && ownerSessionId) {
339
+ try {
340
+ const saved = readRunJson<{ sessionId?: unknown }>(path.join(getRunMetadataDir(entry), "question-owner.json"));
341
+ if (typeof saved?.sessionId === "string" && saved.sessionId.trim()) {
342
+ owner = saved.sessionId;
343
+ owners.set(entry, owner);
344
+ }
345
+ } catch {
346
+ // Missing/unusable identity metadata retains the legacy status fallback.
347
+ }
348
+ }
349
+ const receipt = receipts.has(entry);
350
+ const matchingOwner = owner !== undefined && owner === ownerSessionId;
351
+ if (owner && !matchingOwner && !receipt) continue;
352
+ const record = readAsyncRunRecord(entry, asyncDirRoot, options.resultsDir ?? RESULTS_DIR);
353
+ const { asyncDir } = record.location;
354
+ if (!asyncDir) continue;
355
+ if (sessionIds.size && !receipt && !matchingOwner && !sessionIds.has(record.status?.sessionId ?? "")) continue;
356
+ if (options.reconcile !== false) {
357
+ record.status = reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, record).status;
358
+ }
359
+ if (record.status) {
360
+ validateStatusForSummary(record.status, path.join(asyncDir, "status.json"));
361
+ discovered.add(entry);
362
+ }
363
+ records.push(record);
364
+ } catch (error) {
365
+ if (!options.skipInvalid) throw error;
366
+ console.error(`Skipping invalid async run '${entry}':`, error);
367
+ }
368
+ }
369
+ return records;
370
+ }
371
+ return Object.assign(() => runSynchronously(steps()), { steps });
372
+ }
373
+
374
+ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
375
+ const allowedStates = options.states ? new Set(options.states) : undefined;
376
+ const runs: AsyncRunSummary[] = [];
377
+ for (const { location, status } of options.records ?? createAsyncRunDiscovery(asyncDirRoot, options)()) {
378
+ const asyncDir = location.asyncDir;
379
+ if (!asyncDir) continue;
380
+ try {
381
+ if (!status) continue;
382
+ const nestedWarnings: string[] = [];
383
+ let nestedChildren: NestedRunSummary[] | undefined;
384
+ try {
385
+ const nestedRoute = findNestedRouteForRootId(status.runId || path.basename(asyncDir));
386
+ if (nestedRoute) nestedChildren = reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
387
+ } catch (error) {
388
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
389
+ }
390
+ const summary = asyncStatusToSummary(asyncDir, status, nestedWarnings, nestedChildren);
391
+ if (allowedStates && !allowedStates.has(summary.state)) continue;
392
+ runs.push(summary);
393
+ } catch (error) {
394
+ if (!options.skipInvalid) throw error;
395
+ console.error(`Skipping invalid async run '${location.resolvedId}':`, error);
396
+ }
397
+ }
398
+
399
+ const sorted = sortRuns(runs);
400
+ return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
401
+ }
402
+
403
+ export function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number }): string | undefined {
404
+ const facts: string[] = [];
405
+ if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
406
+ else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
407
+ if (input.currentPath) facts.push(shortenPath(input.currentPath));
408
+ if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
409
+ if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
410
+ const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
411
+ return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
412
+ }
413
+
414
+ function formatStepLine(step: AsyncRunStepSummary): string {
415
+ const display = step.label ? `${step.label} (${step.agent})` : step.agent;
416
+ const phase = step.phase ? `[${step.phase}] ` : "";
417
+ const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
418
+ const activity = formatActivityFacts(step);
419
+ if (activity) parts.push(activity);
420
+ const modelThinking = formatModelThinking(step.model, step.thinking);
421
+ if (modelThinking) parts.push(modelThinking);
422
+ if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
423
+ if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
424
+ return parts.join(" | ");
425
+ }
426
+
427
+ export function formatAsyncRunOutputPath(run: Pick<AsyncRunSummary, "asyncDir" | "outputFile">): string | undefined {
428
+ if (!run.outputFile) return undefined;
429
+ return path.isAbsolute(run.outputFile) ? run.outputFile : path.join(run.asyncDir, run.outputFile);
430
+ }
431
+
432
+ export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
433
+ const stepCount = run.steps.length || 1;
434
+ const chainStepCount = run.chainStepCount ?? stepCount;
435
+ const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, chainStepCount);
436
+ const activeGroup = run.currentStep !== undefined
437
+ ? groups.find((group) => run.currentStep! >= group.start && run.currentStep! < group.start + group.count)
438
+ : undefined;
439
+ if (activeGroup) {
440
+ const groupSteps = run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count);
441
+ const groupLabel = formatParallelOutcome(groupSteps, activeGroup.count, { showRunning: run.state === "running" });
442
+ if (run.mode === "parallel") return groupLabel;
443
+ return `step ${activeGroup.stepIndex + 1}/${chainStepCount} · parallel group: ${groupLabel}`;
444
+ }
445
+ if (run.mode === "parallel") return formatParallelOutcome(run.steps, stepCount, { showRunning: run.state === "running" });
446
+ if (run.mode === "chain" && run.currentStep !== undefined && groups.length > 0) {
447
+ const logicalStep = flatToLogicalStepIndex(run.currentStep, chainStepCount, groups);
448
+ return `step ${logicalStep + 1}/${chainStepCount}`;
449
+ }
450
+ return run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
451
+ }
452
+
453
+ function formatRunHeader(run: AsyncRunSummary): string {
454
+ const stepLabel = formatAsyncRunProgressLabel(run);
455
+ const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
456
+ const activity = formatActivityFacts(run);
457
+ return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel} | ${cwd}`;
458
+ }
459
+
460
+ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
461
+ if (runs.length === 0) return `No ${heading.toLowerCase()}.`;
462
+
463
+ const lines = [`${heading}: ${runs.length}`, ""];
464
+ for (const run of runs) {
465
+ lines.push(`- ${formatRunHeader(run)}`);
466
+ lines.push(` Status: ${formatRunAction("status", run.id)}`);
467
+ for (const step of run.steps) {
468
+ lines.push(` ${formatStepLine(step)}`);
469
+ lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
470
+ }
471
+ const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
472
+ const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
473
+ lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
474
+ for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
475
+ const outputPath = formatAsyncRunOutputPath(run);
476
+ if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
477
+ if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
478
+ lines.push("");
479
+ }
480
+ return lines.join("\n").trimEnd();
481
+ }
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ interface CompletionDataLike {
4
+ completionId?: unknown;
5
+ id?: unknown;
6
+ agent?: unknown;
7
+ timestamp?: unknown;
8
+ sessionId?: unknown;
9
+ taskIndex?: unknown;
10
+ totalTasks?: unknown;
11
+ success?: unknown;
12
+ }
13
+
14
+ function asNonEmptyString(value: unknown): string | undefined {
15
+ if (typeof value !== "string") return undefined;
16
+ const trimmed = value.trim();
17
+ return trimmed.length > 0 ? trimmed : undefined;
18
+ }
19
+
20
+ function asFiniteNumber(value: unknown): number | undefined {
21
+ if (typeof value !== "number") return undefined;
22
+ return Number.isFinite(value) ? value : undefined;
23
+ }
24
+
25
+ function stableStringify(value: unknown): string {
26
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
27
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
28
+ const record = value as Record<string, unknown>;
29
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
30
+ }
31
+
32
+ function payloadDigest(value: unknown): string {
33
+ return createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, 16);
34
+ }
35
+
36
+ export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
37
+ const completionId = asNonEmptyString(data.completionId);
38
+ if (completionId) return `completion:${completionId}`;
39
+ const id = asNonEmptyString(data.id);
40
+ if (id) return `id:${id}:${payloadDigest(data)}`;
41
+ const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
42
+ const agent = asNonEmptyString(data.agent) ?? "unknown";
43
+ const timestamp = asFiniteNumber(data.timestamp);
44
+ const taskIndex = asFiniteNumber(data.taskIndex);
45
+ const totalTasks = asFiniteNumber(data.totalTasks);
46
+ const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
47
+ return [
48
+ "meta",
49
+ sessionId,
50
+ agent,
51
+ timestamp !== undefined ? String(timestamp) : "no-ts",
52
+ taskIndex !== undefined ? String(taskIndex) : "-",
53
+ totalTasks !== undefined ? String(totalTasks) : "-",
54
+ success,
55
+ fallback,
56
+ ].join(":");
57
+ }
58
+
59
+ function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void {
60
+ for (const [key, ts] of seen.entries()) {
61
+ if (now - ts > ttlMs) seen.delete(key);
62
+ }
63
+ }
64
+
65
+ export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean {
66
+ pruneSeenMap(seen, now, ttlMs);
67
+ if (seen.has(key)) return true;
68
+ seen.set(key, now);
69
+ return false;
70
+ }
71
+
72
+ export function getGlobalSeenMap(storeKey: string): Map<string, number> {
73
+ const globalStore = globalThis as Record<string, unknown>;
74
+ const existing = globalStore[storeKey];
75
+ if (existing instanceof Map) return existing as Map<string, number>;
76
+ const map = new Map<string, number>();
77
+ globalStore[storeKey] = map;
78
+ return map;
79
+ }