@bastani/atomic 0.9.12 → 0.9.13-alpha.1

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 (264) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/README.md +5 -12
  4. package/dist/builtin/intercom/broker/client.ts +11 -2
  5. package/dist/builtin/intercom/contact-supervisor-tool.ts +13 -3
  6. package/dist/builtin/intercom/index-heavy.ts +13 -3
  7. package/dist/builtin/intercom/index.ts +43 -19
  8. package/dist/builtin/intercom/intercom-utils.ts +15 -29
  9. package/dist/builtin/intercom/package.json +1 -1
  10. package/dist/builtin/intercom/source-ownership.ts +9 -12
  11. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  12. package/dist/builtin/mcp/README.md +3 -1
  13. package/dist/builtin/mcp/index.ts +10 -16
  14. package/dist/builtin/mcp/package.json +1 -1
  15. package/dist/builtin/mcp/startup-warmup.ts +20 -8
  16. package/dist/builtin/subagents/CHANGELOG.md +30 -0
  17. package/dist/builtin/subagents/README.md +22 -30
  18. package/dist/builtin/subagents/package.json +1 -1
  19. package/dist/builtin/subagents/src/extension/doctor.ts +1 -1
  20. package/dist/builtin/subagents/src/extension/fanout-child.ts +76 -56
  21. package/dist/builtin/subagents/src/extension/index.ts +23 -32
  22. package/dist/builtin/subagents/src/extension/startup-maintenance.ts +25 -44
  23. package/dist/builtin/subagents/src/intercom/result-intercom.ts +7 -7
  24. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +166 -434
  25. package/dist/builtin/subagents/src/runs/background/notify.ts +29 -8
  26. package/dist/builtin/subagents/src/runs/foreground/chain-execution-dynamic-step.ts +2 -2
  27. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +4 -4
  28. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-step.ts +2 -2
  29. package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +2 -2
  30. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +1 -281
  31. package/dist/builtin/subagents/src/runs/foreground/execution-structured-retries.ts +11 -75
  32. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +457 -0
  33. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +119 -246
  34. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-chain.ts +1 -1
  35. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +18 -9
  36. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +1 -1
  37. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +4 -4
  38. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-resume.ts +1 -646
  39. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +2 -7
  40. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +5 -5
  41. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +7 -8
  42. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +14 -5
  43. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +158 -93
  44. package/dist/builtin/subagents/src/runs/inprocess/background-single.ts +179 -0
  45. package/dist/builtin/subagents/src/runs/inprocess/background.ts +89 -0
  46. package/dist/builtin/subagents/src/runs/inprocess/child-policy.ts +27 -0
  47. package/dist/builtin/subagents/src/runs/inprocess/control-registry.ts +55 -0
  48. package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +106 -0
  49. package/dist/builtin/subagents/src/runs/inprocess/index.ts +50 -0
  50. package/dist/builtin/subagents/src/runs/inprocess/nested-routing.ts +218 -0
  51. package/dist/builtin/subagents/src/runs/{shared/subagent-prompt-runtime.ts → inprocess/prompt-behavior.ts} +37 -75
  52. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +1138 -0
  53. package/dist/builtin/subagents/src/runs/{shared/nested-events.ts → inprocess/runtime-support/nested-api.ts} +13 -7
  54. package/dist/builtin/subagents/src/runs/{shared/nested-events-control.ts → inprocess/runtime-support/nested-control.ts} +18 -17
  55. package/dist/builtin/subagents/src/runs/{shared/nested-events-core.ts → inprocess/runtime-support/nested-core.ts} +14 -61
  56. package/dist/builtin/subagents/src/runs/{shared/nested-events-projection.ts → inprocess/runtime-support/nested-projection.ts} +4 -10
  57. package/dist/builtin/subagents/src/runs/{shared/nested-events-registry.ts → inprocess/runtime-support/nested-registry.ts} +19 -4
  58. package/dist/builtin/subagents/src/runs/{shared/nested-render.ts → inprocess/runtime-support/nested-rendering.ts} +3 -3
  59. package/dist/builtin/subagents/src/runs/{shared/nested-events-sanitize.ts → inprocess/runtime-support/nested-sanitize.ts} +3 -3
  60. package/dist/builtin/subagents/src/runs/inprocess.ts +1 -0
  61. package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +10 -4
  62. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +0 -1
  63. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +15 -492
  64. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +13 -11
  65. package/dist/builtin/subagents/src/runs/shared/run-history.ts +3 -5
  66. package/dist/builtin/subagents/src/runs/shared/single-output.ts +4 -4
  67. package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +5 -5
  68. package/dist/builtin/subagents/src/shared/types-async.ts +0 -1
  69. package/dist/builtin/subagents/src/shared/types-config.ts +18 -0
  70. package/dist/builtin/subagents/src/shared/types-results.ts +9 -2
  71. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +2 -3
  72. package/dist/builtin/subagents/src/slash/slash-live-state.ts +2 -2
  73. package/dist/builtin/subagents/src/tui/render-chain-graph.ts +10 -4
  74. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +12 -11
  75. package/dist/builtin/subagents/src/tui/render-result-compact.ts +49 -12
  76. package/dist/builtin/subagents/src/tui/render-result.ts +46 -17
  77. package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -1
  78. package/dist/builtin/subagents/src/tui/render-status-progress.ts +8 -5
  79. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +0 -8
  80. package/dist/builtin/subagents/src/tui/render-widget.ts +0 -24
  81. package/dist/builtin/web-access/package.json +1 -1
  82. package/dist/builtin/workflows/CHANGELOG.md +25 -0
  83. package/dist/builtin/workflows/README.md +13 -3
  84. package/dist/builtin/workflows/package.json +1 -1
  85. package/dist/builtin/workflows/src/durable/backend.ts +7 -0
  86. package/dist/builtin/workflows/src/durable/dbos-metadata.ts +11 -1
  87. package/dist/builtin/workflows/src/durable/resume-runtime.ts +1 -0
  88. package/dist/builtin/workflows/src/durable/tool-primitive.ts +4 -28
  89. package/dist/builtin/workflows/src/durable/types.ts +5 -0
  90. package/dist/builtin/workflows/src/engine/run.ts +17 -1
  91. package/dist/builtin/workflows/src/extension/atomic-stage-session.ts +12 -39
  92. package/dist/builtin/workflows/src/extension/config-file-loader.ts +10 -1
  93. package/dist/builtin/workflows/src/extension/config-loader.ts +1 -1
  94. package/dist/builtin/workflows/src/extension/dispatcher.ts +4 -0
  95. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1151 -737
  96. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +303 -38
  97. package/dist/builtin/workflows/src/extension/render-result.ts +4 -0
  98. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +6 -3
  99. package/dist/builtin/workflows/src/extension/runtime.ts +7 -1
  100. package/dist/builtin/workflows/src/extension/wiring.ts +53 -11
  101. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +12 -3
  102. package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +5 -2
  103. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +9 -7
  104. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +11 -0
  105. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +18 -1
  106. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +10 -8
  107. package/dist/builtin/workflows/src/extension/workflow-tool.ts +9 -4
  108. package/dist/builtin/workflows/src/runs/background/quit.ts +28 -4
  109. package/dist/builtin/workflows/src/runs/background/resume-acknowledgements.ts +2 -1
  110. package/dist/builtin/workflows/src/runs/background/status.ts +55 -8
  111. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +4 -0
  112. package/dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts +3 -1
  113. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +1 -1
  114. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +1 -1
  115. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +688 -38
  116. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -1
  117. package/dist/builtin/workflows/src/runs/shared/model-fallback-failures.ts +19 -514
  118. package/dist/builtin/workflows/src/runs/shared/retry.ts +44 -0
  119. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +5 -0
  120. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +6 -0
  121. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +5 -1
  122. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +5 -0
  123. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +5 -0
  124. package/dist/builtin/workflows/src/shared/persistence-restore.ts +3 -0
  125. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  126. package/dist/builtin/workflows/src/shared/run-indicator-status.ts +75 -0
  127. package/dist/builtin/workflows/src/shared/store-public-types.ts +43 -7
  128. package/dist/builtin/workflows/src/shared/store-run-methods.ts +42 -8
  129. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +31 -8
  130. package/dist/builtin/workflows/src/shared/store-types.ts +34 -0
  131. package/dist/builtin/workflows/src/shared/timing.ts +10 -0
  132. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -2
  133. package/dist/builtin/workflows/src/tui/session-list.ts +1 -1
  134. package/dist/builtin/workflows/src/tui/session-overlays.ts +14 -10
  135. package/dist/builtin/workflows/src/tui/session-picker.ts +20 -4
  136. package/dist/builtin/workflows/src/tui/status-list.ts +31 -47
  137. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +9 -1
  138. package/dist/builtin/workflows/src/tui/widget.ts +16 -46
  139. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  140. package/dist/core/agent-session-auto-compaction.js +54 -11
  141. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  142. package/dist/core/agent-session-events.d.ts.map +1 -1
  143. package/dist/core/agent-session-events.js +49 -14
  144. package/dist/core/agent-session-events.js.map +1 -1
  145. package/dist/core/agent-session-methods.d.ts +13 -0
  146. package/dist/core/agent-session-methods.d.ts.map +1 -1
  147. package/dist/core/agent-session-methods.js.map +1 -1
  148. package/dist/core/agent-session-models.d.ts.map +1 -1
  149. package/dist/core/agent-session-models.js +12 -1
  150. package/dist/core/agent-session-models.js.map +1 -1
  151. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  152. package/dist/core/agent-session-prompt.js +4 -0
  153. package/dist/core/agent-session-prompt.js.map +1 -1
  154. package/dist/core/agent-session-retry.d.ts +21 -4
  155. package/dist/core/agent-session-retry.d.ts.map +1 -1
  156. package/dist/core/agent-session-retry.js +232 -106
  157. package/dist/core/agent-session-retry.js.map +1 -1
  158. package/dist/core/agent-session-state.d.ts.map +1 -1
  159. package/dist/core/agent-session-state.js +2 -1
  160. package/dist/core/agent-session-state.js.map +1 -1
  161. package/dist/core/agent-session-tool-hooks.d.ts.map +1 -1
  162. package/dist/core/agent-session-tool-hooks.js +12 -0
  163. package/dist/core/agent-session-tool-hooks.js.map +1 -1
  164. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  165. package/dist/core/agent-session-tool-registry.js +1 -1
  166. package/dist/core/agent-session-tool-registry.js.map +1 -1
  167. package/dist/core/agent-session-types.d.ts +3 -1
  168. package/dist/core/agent-session-types.d.ts.map +1 -1
  169. package/dist/core/agent-session-types.js.map +1 -1
  170. package/dist/core/agent-session.d.ts +12 -1
  171. package/dist/core/agent-session.d.ts.map +1 -1
  172. package/dist/core/agent-session.js +7 -0
  173. package/dist/core/agent-session.js.map +1 -1
  174. package/dist/core/extensions/context-types.d.ts +25 -0
  175. package/dist/core/extensions/context-types.d.ts.map +1 -1
  176. package/dist/core/extensions/context-types.js.map +1 -1
  177. package/dist/core/extensions/index.d.ts +1 -1
  178. package/dist/core/extensions/index.d.ts.map +1 -1
  179. package/dist/core/extensions/index.js.map +1 -1
  180. package/dist/core/extensions/runner-context.d.ts +2 -1
  181. package/dist/core/extensions/runner-context.d.ts.map +1 -1
  182. package/dist/core/extensions/runner-context.js +4 -0
  183. package/dist/core/extensions/runner-context.js.map +1 -1
  184. package/dist/core/extensions/runner.d.ts +3 -2
  185. package/dist/core/extensions/runner.d.ts.map +1 -1
  186. package/dist/core/extensions/runner.js +3 -1
  187. package/dist/core/extensions/runner.js.map +1 -1
  188. package/dist/core/model-fallback-failures.d.ts +30 -0
  189. package/dist/core/model-fallback-failures.d.ts.map +1 -0
  190. package/dist/core/model-fallback-failures.js +610 -0
  191. package/dist/core/model-fallback-failures.js.map +1 -0
  192. package/dist/core/retry-policy.d.ts +30 -0
  193. package/dist/core/retry-policy.d.ts.map +1 -0
  194. package/dist/core/retry-policy.js +25 -0
  195. package/dist/core/retry-policy.js.map +1 -0
  196. package/dist/core/sdk-types.d.ts +8 -2
  197. package/dist/core/sdk-types.d.ts.map +1 -1
  198. package/dist/core/sdk-types.js.map +1 -1
  199. package/dist/core/sdk.d.ts.map +1 -1
  200. package/dist/core/sdk.js +6 -1
  201. package/dist/core/sdk.js.map +1 -1
  202. package/dist/core/session-manager-core.d.ts +2 -0
  203. package/dist/core/session-manager-core.d.ts.map +1 -1
  204. package/dist/core/session-manager-core.js +7 -0
  205. package/dist/core/session-manager-core.js.map +1 -1
  206. package/dist/index-extensions.d.ts +1 -1
  207. package/dist/index-extensions.d.ts.map +1 -1
  208. package/dist/index-extensions.js.map +1 -1
  209. package/dist/index.d.ts +3 -0
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +2 -0
  212. package/dist/index.js.map +1 -1
  213. package/docs/intercom.md +5 -10
  214. package/docs/settings.md +7 -1
  215. package/docs/subagents.md +19 -19
  216. package/docs/workflows.md +21 -5
  217. package/examples/extensions/gondolin/package-lock.json +3 -3
  218. package/npm-shrinkwrap.json +29 -29
  219. package/package.json +2 -2
  220. package/dist/builtin/subagents/src/runs/background/async-event-journal.ts +0 -112
  221. package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +0 -543
  222. package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +0 -140
  223. package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +0 -279
  224. package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +0 -99
  225. package/dist/builtin/subagents/src/runs/background/async-execution.ts +0 -17
  226. package/dist/builtin/subagents/src/runs/background/async-resume.ts +0 -402
  227. package/dist/builtin/subagents/src/runs/background/async-status.ts +0 -411
  228. package/dist/builtin/subagents/src/runs/background/completion-claims.ts +0 -196
  229. package/dist/builtin/subagents/src/runs/background/completion-dedupe.ts +0 -102
  230. package/dist/builtin/subagents/src/runs/background/parallel-groups.ts +0 -59
  231. package/dist/builtin/subagents/src/runs/background/result-delivery-processor.ts +0 -300
  232. package/dist/builtin/subagents/src/runs/background/result-file-claims.ts +0 -166
  233. package/dist/builtin/subagents/src/runs/background/result-quarantine.ts +0 -77
  234. package/dist/builtin/subagents/src/runs/background/result-retry-scheduler.ts +0 -48
  235. package/dist/builtin/subagents/src/runs/background/result-status.ts +0 -87
  236. package/dist/builtin/subagents/src/runs/background/result-watcher-data.ts +0 -72
  237. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +0 -320
  238. package/dist/builtin/subagents/src/runs/background/run-id-resolver.ts +0 -105
  239. package/dist/builtin/subagents/src/runs/background/run-status.ts +0 -358
  240. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +0 -457
  241. package/dist/builtin/subagents/src/runs/background/subagent-runner-dynamic.ts +0 -376
  242. package/dist/builtin/subagents/src/runs/background/subagent-runner-finalize.ts +0 -150
  243. package/dist/builtin/subagents/src/runs/background/subagent-runner-output.ts +0 -109
  244. package/dist/builtin/subagents/src/runs/background/subagent-runner-parallel-helpers.ts +0 -128
  245. package/dist/builtin/subagents/src/runs/background/subagent-runner-parallel.ts +0 -309
  246. package/dist/builtin/subagents/src/runs/background/subagent-runner-sequential.ts +0 -173
  247. package/dist/builtin/subagents/src/runs/background/subagent-runner-state.ts +0 -578
  248. package/dist/builtin/subagents/src/runs/background/subagent-runner-step.ts +0 -330
  249. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +0 -330
  250. package/dist/builtin/subagents/src/runs/background/subagent-runner-types.ts +0 -210
  251. package/dist/builtin/subagents/src/runs/background/subagent-runner-utils.ts +0 -69
  252. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +0 -90
  253. package/dist/builtin/subagents/src/runs/background/top-level-async.ts +0 -12
  254. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-control.ts +0 -140
  255. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-finalize.ts +0 -108
  256. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-types.ts +0 -17
  257. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +0 -558
  258. package/dist/builtin/subagents/src/runs/shared/attempt-watchdog.ts +0 -126
  259. package/dist/builtin/subagents/src/runs/shared/final-drain.ts +0 -39
  260. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +0 -339
  261. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +0 -165
  262. package/dist/builtin/subagents/src/runs/shared/spawn-env.ts +0 -21
  263. package/dist/builtin/subagents/src/shared/post-exit-stdio-guard.ts +0 -86
  264. /package/dist/builtin/subagents/src/runs/{shared/nested-path.ts → inprocess/runtime-support/nested-paths.ts} +0 -0
@@ -1,57 +1,18 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@bastani/atomic";
4
- import {
5
- type AsyncJobState,
6
- type AsyncStartedEvent,
7
- type ControlEvent,
8
- POLL_INTERVAL_MS,
9
- RESULTS_DIR,
10
- SUBAGENT_CONTROL_EVENT,
11
- SUBAGENT_CONTROL_INTERCOM_EVENT,
12
- type SubagentState,
13
- } from "../../shared/types.ts";
14
- import { readStatus } from "../../shared/utils.ts";
15
- import { renderWidget, widgetRenderKey } from "../../tui/render.ts";
16
- import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
17
- import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
18
- import { type AsyncRunSummary, listAsyncRuns } from "./async-status.ts";
19
- import { normalizeParallelGroups } from "./parallel-groups.ts";
20
- import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
4
+ import type { AsyncJobState, AsyncStartedEvent, AsyncStatus, SubagentState } from "../../shared/types.ts";
5
+ import { renderWidget } from "../../tui/render.ts";
6
+ import { listSubagentControls } from "../inprocess/control-registry.ts";
21
7
 
22
8
  interface AsyncJobTrackerOptions {
23
9
  completionRetentionMs?: number;
24
- pollIntervalMs?: number;
25
10
  resultsDir?: string;
11
+ pollIntervalMs?: number;
26
12
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
27
13
  now?: () => number;
28
14
  }
29
15
 
30
- const ACTIVE_HYDRATION_STATES: Array<AsyncRunSummary["state"]> = ["queued", "running"];
31
-
32
- /** Only eagerly hydrate runs touched within the last 7 days; older runs stay on-demand. */
33
- const ACTIVE_HYDRATION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
34
-
35
- function cwdMatches(summaryCwd: string | undefined, currentCwd: string | undefined): boolean {
36
- return Boolean(summaryCwd && currentCwd && path.resolve(summaryCwd) === path.resolve(currentCwd));
37
- }
38
-
39
- function shouldHydrateRunForCurrentUi(
40
- summary: AsyncRunSummary,
41
- currentSessionId: string | null,
42
- currentCwd: string | undefined,
43
- ): boolean {
44
- if (summary.sessionId && currentSessionId) return summary.sessionId === currentSessionId;
45
- return cwdMatches(summary.cwd, currentCwd);
46
- }
47
-
48
- /**
49
- * Extension contexts captured for timer-driven work (deferred hydration and
50
- * state.lastUiContext) go stale after ctx.newSession(), ctx.fork(),
51
- * ctx.switchSession(), or ctx.reload(); every accessor on a stale context
52
- * throws. Probe defensively so an unref'd timer can never crash the host
53
- * process with an unhandled stale-context error.
54
- */
55
16
  function ctxHasUI(ctx: ExtensionContext | null | undefined): boolean {
56
17
  if (!ctx) return false;
57
18
  try {
@@ -61,70 +22,131 @@ function ctxHasUI(ctx: ExtensionContext | null | undefined): boolean {
61
22
  }
62
23
  }
63
24
 
64
- function ctxCwd(ctx: ExtensionContext | null | undefined): string | undefined {
25
+ function liveUiContext(state: SubagentState): ExtensionContext | undefined {
26
+ const ctx = state.lastUiContext;
65
27
  if (!ctx) return undefined;
66
28
  try {
67
- return ctx.cwd;
29
+ void ctx.hasUI;
30
+ return ctx;
68
31
  } catch {
32
+ state.lastUiContext = null;
69
33
  return undefined;
70
34
  }
71
35
  }
72
36
 
73
- function asyncRunSummaryToJobState(summary: AsyncRunSummary, existing?: AsyncJobState): AsyncJobState {
74
- const chainStepCount = summary.chainStepCount ?? summary.steps.length;
75
- const groups = normalizeParallelGroups(summary.parallelGroups, summary.steps.length, chainStepCount);
76
- const activeGroup =
77
- summary.currentStep !== undefined
78
- ? groups.find(
79
- (group) => summary.currentStep! >= group.start && summary.currentStep! < group.start + group.count,
80
- )
81
- : undefined;
82
- const steps = activeGroup
83
- ? summary.steps
84
- .slice(activeGroup.start, activeGroup.start + activeGroup.count)
85
- .map((step, index) => ({ ...step, index: activeGroup.start + index }))
86
- : summary.steps.map((step, index) => ({ ...step, index: step.index ?? index }));
87
- const agents = steps.map((step) => step.agent);
88
- const stepsTotal =
89
- steps.length > 0 ? steps.length : (existing?.stepsTotal ?? (agents.length > 0 ? agents.length : undefined));
90
- const completedSteps =
91
- summary.state === "complete"
92
- ? steps.length
93
- : steps.filter((step) => step.status === "complete" || step.status === "completed").length;
37
+ function stateFromStatus(status: AsyncStatus, asyncDir: string): AsyncJobState {
38
+ const steps = (status.steps ?? []).map((step, index) => ({ ...step, index }));
39
+ const completedSteps = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
94
40
  return {
95
- ...existing,
96
- asyncId: summary.id,
97
- asyncDir: summary.asyncDir,
98
- status: summary.state,
99
- sessionId: summary.sessionId ?? existing?.sessionId,
100
- activityState: summary.activityState,
101
- lastActivityAt: summary.lastActivityAt,
102
- currentTool: summary.currentTool,
103
- currentToolStartedAt: summary.currentToolStartedAt,
104
- currentPath: summary.currentPath,
105
- turnCount: summary.turnCount,
106
- toolCount: summary.toolCount,
107
- mode: summary.mode,
108
- agents: agents.length > 0 ? agents : existing?.agents,
109
- currentStep: summary.currentStep,
110
- chainStepCount: summary.chainStepCount ?? existing?.chainStepCount,
111
- parallelGroups: groups,
112
- steps: steps.length > 0 ? steps : existing?.steps,
113
- stepsTotal,
41
+ asyncId: status.runId,
42
+ asyncDir,
43
+ status: status.state,
44
+ ...(status.sessionId ? { sessionId: status.sessionId } : {}),
45
+ ...(status.activityState ? { activityState: status.activityState } : {}),
46
+ ...(status.lastActivityAt !== undefined ? { lastActivityAt: status.lastActivityAt } : {}),
47
+ ...(status.currentTool ? { currentTool: status.currentTool } : {}),
48
+ ...(status.currentToolStartedAt !== undefined ? { currentToolStartedAt: status.currentToolStartedAt } : {}),
49
+ ...(status.currentPath ? { currentPath: status.currentPath } : {}),
50
+ ...(status.turnCount !== undefined ? { turnCount: status.turnCount } : {}),
51
+ ...(status.toolCount !== undefined ? { toolCount: status.toolCount } : {}),
52
+ mode: status.mode,
53
+ agents: steps.map((step) => step.agent),
54
+ currentStep: status.currentStep,
55
+ chainStepCount: status.chainStepCount,
56
+ parallelGroups: status.parallelGroups,
57
+ steps,
58
+ stepsTotal: steps.length,
114
59
  runningSteps: steps.filter((step) => step.status === "running").length,
115
- completedSteps,
116
- hasParallelGroups: groups.length > 0,
117
- activeParallelGroup: Boolean(activeGroup),
118
- startedAt: summary.startedAt,
119
- updatedAt: summary.lastUpdate ?? summary.startedAt,
120
- sessionDir: summary.sessionDir ?? existing?.sessionDir,
121
- outputFile: summary.outputFile ?? existing?.outputFile,
122
- totalTokens: summary.totalTokens,
123
- sessionFile: summary.sessionFile ?? existing?.sessionFile,
124
- nestedChildren: summary.nestedChildren,
60
+ completedSteps: status.state === "complete" ? steps.length : completedSteps,
61
+ hasParallelGroups: (status.parallelGroups?.length ?? 0) > 0,
62
+ activeParallelGroup: (status.parallelGroups?.length ?? 0) > 0 && status.state === "running",
63
+ startedAt: status.startedAt,
64
+ updatedAt: status.lastUpdate ?? status.startedAt,
65
+ ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
66
+ ...(status.outputFile ? { outputFile: status.outputFile } : {}),
67
+ ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
68
+ ...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
125
69
  };
126
70
  }
127
71
 
72
+ function readActiveFileJobs(
73
+ asyncDirRoot: string,
74
+ currentSessionId: string | null,
75
+ currentCwd: string,
76
+ ): Map<string, AsyncJobState> {
77
+ const jobs = new Map<string, AsyncJobState>();
78
+ let entries: string[];
79
+ try {
80
+ entries = fs.readdirSync(asyncDirRoot);
81
+ } catch {
82
+ return jobs;
83
+ }
84
+ for (const entry of entries) {
85
+ const asyncDir = path.join(asyncDirRoot, entry);
86
+ let status: AsyncStatus;
87
+ try {
88
+ if (!fs.statSync(asyncDir).isDirectory()) continue;
89
+ status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8")) as AsyncStatus;
90
+ } catch {
91
+ continue;
92
+ }
93
+ if (status.state !== "queued" && status.state !== "running") continue;
94
+ if (status.sessionId && currentSessionId && status.sessionId !== currentSessionId) continue;
95
+ if (!status.sessionId && status.cwd && path.resolve(status.cwd) !== path.resolve(currentCwd)) continue;
96
+ jobs.set(status.runId || entry, stateFromStatus({ ...status, runId: status.runId || entry }, asyncDir));
97
+ }
98
+ return jobs;
99
+ }
100
+
101
+ function hydrateRegistryJobs(_state: SubagentState, currentSessionId: string | null): Map<string, AsyncJobState> {
102
+ const jobs = new Map<string, AsyncJobState>();
103
+ for (const control of listSubagentControls()) {
104
+ const children = control.listChildren();
105
+ if (children.length === 0) continue;
106
+ const steps = children.map((child, index) => ({
107
+ index,
108
+ agent: child.taskName,
109
+ status:
110
+ child.status === "ok"
111
+ ? ("complete" as const)
112
+ : child.status === "error"
113
+ ? ("failed" as const)
114
+ : child.status === "interrupted"
115
+ ? ("paused" as const)
116
+ : child.status === "running" || child.status === "continued"
117
+ ? ("running" as const)
118
+ : ("pending" as const),
119
+ }));
120
+ const status: AsyncJobState["status"] = steps.some(
121
+ (step) => step.status === "running" || step.status === "pending",
122
+ )
123
+ ? "running"
124
+ : steps.some((step) => step.status === "failed")
125
+ ? "failed"
126
+ : steps.some((step) => step.status === "paused")
127
+ ? "paused"
128
+ : "complete";
129
+ if (status !== "running") continue;
130
+ jobs.set(control.parent.path, {
131
+ asyncId: control.parent.path,
132
+ asyncDir: control.parent.path,
133
+ status,
134
+ sessionId: currentSessionId ?? undefined,
135
+ mode: steps.length > 1 ? "parallel" : "single",
136
+ agents: steps.map((step) => step.agent),
137
+ steps,
138
+ stepsTotal: steps.length,
139
+ runningSteps: steps.filter((step) => step.status === "running").length,
140
+ completedSteps: steps.filter((step) => step.status === "complete").length,
141
+ hasParallelGroups: steps.length > 1,
142
+ activeParallelGroup: steps.some((step) => step.status === "running"),
143
+ startedAt: Date.now(),
144
+ updatedAt: Date.now(),
145
+ });
146
+ }
147
+ return jobs;
148
+ }
149
+
128
150
  export function createAsyncJobTracker(
129
151
  pi: Pick<ExtensionAPI, "events">,
130
152
  state: SubagentState,
@@ -138,360 +160,74 @@ export function createAsyncJobTracker(
138
160
  hydrateActiveJobs: (ctx?: ExtensionContext) => void;
139
161
  hydrateActiveJobsDeferred: (ctx?: ExtensionContext) => void;
140
162
  } {
141
- const completionRetentionMs = options.completionRetentionMs ?? 10000;
142
- const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
143
- const resultsDir = options.resultsDir ?? RESULTS_DIR;
144
- const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
145
- renderWidget(ctx, jobs, pi);
146
- };
147
- /** Return the retained UI context when it is still usable; drop it once stale. */
148
- const liveUiContext = (): ExtensionContext | undefined => {
149
- const ctx = state.lastUiContext;
150
- if (!ctx) return undefined;
151
- try {
152
- void ctx.hasUI;
153
- return ctx;
154
- } catch {
155
- state.lastUiContext = null;
156
- return undefined;
157
- }
158
- };
159
- const cancelCleanup = (asyncId: string) => {
160
- const existingTimer = state.cleanupTimers.get(asyncId);
161
- if (!existingTimer) return;
162
- clearTimeout(existingTimer);
163
- state.cleanupTimers.delete(asyncId);
163
+ const completionRetentionMs = options.completionRetentionMs ?? 10_000;
164
+ const rerender = () => {
165
+ const ctx = liveUiContext(state);
166
+ if (ctxHasUI(ctx)) renderWidget(ctx!, [...state.asyncJobs.values()], pi);
164
167
  };
165
- const scheduleCleanup = (asyncId: string) => {
166
- cancelCleanup(asyncId);
168
+ const scheduleCleanup = (id: string) => {
169
+ const previous = state.cleanupTimers.get(id);
170
+ if (previous) clearTimeout(previous);
167
171
  const timer = setTimeout(() => {
168
- state.cleanupTimers.delete(asyncId);
169
- state.asyncJobs.delete(asyncId);
170
- const uiCtx = liveUiContext();
171
- if (uiCtx) {
172
- rerenderWidget(uiCtx);
173
- }
172
+ state.cleanupTimers.delete(id);
173
+ state.asyncJobs.delete(id);
174
+ rerender();
174
175
  }, completionRetentionMs);
175
- state.cleanupTimers.set(asyncId, timer);
176
- };
177
- const emitNewControlEvents = (job: AsyncJobState) => {
178
- const eventsPath = path.join(job.asyncDir, "events.jsonl");
179
- let fd: number;
180
- try {
181
- fd = fs.openSync(eventsPath, "r");
182
- } catch (error) {
183
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
184
- console.error(`Failed to open async control events for '${job.asyncDir}':`, error);
185
- return;
186
- }
187
- try {
188
- const stat = fs.fstatSync(fd);
189
- const cursor = stat.size < (job.controlEventCursor ?? 0) ? 0 : (job.controlEventCursor ?? 0);
190
- if (stat.size <= cursor) return;
191
- const buffer = Buffer.alloc(stat.size - cursor);
192
- fs.readSync(fd, buffer, 0, buffer.length, cursor);
193
- const lastNewline = buffer.lastIndexOf(0x0a);
194
- if (lastNewline === -1) return;
195
- job.controlEventCursor = cursor + lastNewline + 1;
196
- for (const line of buffer.subarray(0, lastNewline).toString("utf-8").split("\n")) {
197
- if (!line.trim()) continue;
198
- let parsed: unknown;
199
- try {
200
- parsed = JSON.parse(line);
201
- } catch (error) {
202
- console.error(`Ignoring malformed async control event in '${eventsPath}':`, error);
203
- continue;
204
- }
205
- if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control")
206
- continue;
207
- const record = parsed as {
208
- event?: ControlEvent;
209
- channels?: string[];
210
- childIntercomTarget?: string;
211
- noticeText?: string;
212
- intercom?: { to?: string; message?: string };
213
- };
214
- if (!record.event || !Array.isArray(record.channels)) continue;
215
- const payload = {
216
- event: record.event,
217
- source: "async" as const,
218
- asyncDir: job.asyncDir,
219
- childIntercomTarget: record.childIntercomTarget,
220
- noticeText: record.noticeText ?? formatControlNoticeMessage(record.event, record.childIntercomTarget),
221
- };
222
- if (record.channels.includes("event")) {
223
- pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
224
- }
225
- if (
226
- record.event.type !== "active_long_running" &&
227
- record.channels.includes("intercom") &&
228
- record.intercom?.to &&
229
- record.intercom.message
230
- ) {
231
- pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
232
- ...payload,
233
- to: record.intercom.to,
234
- message: record.intercom.message,
235
- });
236
- }
237
- }
238
- } catch (error) {
239
- console.error(`Failed to read async control events for '${job.asyncDir}':`, error);
240
- } finally {
241
- fs.closeSync(fd);
242
- }
176
+ timer.unref?.();
177
+ state.cleanupTimers.set(id, timer);
243
178
  };
244
-
245
179
  const ensurePoller = () => {
246
- if (state.poller) return;
247
- state.poller = setInterval(() => {
248
- if (state.asyncJobs.size === 0) {
249
- const idleUiCtx = liveUiContext();
250
- if (ctxHasUI(idleUiCtx)) rerenderWidget(idleUiCtx!, []);
251
- if (state.poller) {
252
- clearInterval(state.poller);
253
- state.poller = null;
254
- }
255
- return;
256
- }
257
-
258
- let widgetChanged = false;
259
- for (const job of state.asyncJobs.values()) {
260
- const widgetStateBefore = widgetRenderKey(job);
261
- let nestedRefreshFailed = false;
262
- const refreshNestedProjection = () => {
263
- try {
264
- updateAsyncJobNestedProjection(job);
265
- } catch (error) {
266
- nestedRefreshFailed = true;
267
- console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
268
- }
269
- };
270
- const reconcileNestedDescendants = () => {
271
- try {
272
- if (job.nestedRoute)
273
- reconcileNestedAsyncDescendants(job.nestedRoute, {
274
- resultsDir,
275
- kill: options.kill,
276
- now: options.now,
277
- });
278
- } catch (error) {
279
- nestedRefreshFailed = true;
280
- console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
281
- }
282
- refreshNestedProjection();
283
- };
284
- try {
285
- emitNewControlEvents(job);
286
- reconcileNestedDescendants();
287
- const reconciliation = reconcileAsyncRun(job.asyncDir, {
288
- resultsDir,
289
- kill: options.kill,
290
- now: options.now,
291
- startedRun: {
292
- runId: job.asyncId,
293
- pid: job.pid,
294
- sessionId: job.sessionId,
295
- mode: job.mode,
296
- agents: job.agents,
297
- chainStepCount: job.chainStepCount,
298
- parallelGroups: job.parallelGroups,
299
- startedAt: job.startedAt,
300
- sessionFile: job.sessionFile,
301
- },
302
- });
303
- const status = reconciliation.status ?? readStatus(job.asyncDir);
304
- if (status) {
305
- const previousStatus = job.status;
306
- job.status = status.state;
307
- if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused")
308
- cancelCleanup(job.asyncId);
309
- job.sessionId = status.sessionId ?? job.sessionId;
310
- job.activityState = status.activityState;
311
- job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
312
- job.currentTool = status.currentTool;
313
- job.currentToolStartedAt = status.currentToolStartedAt;
314
- job.currentPath = status.currentPath;
315
- job.turnCount = status.turnCount ?? job.turnCount;
316
- job.toolCount = status.toolCount ?? job.toolCount;
317
- job.mode = status.mode;
318
- job.currentStep = status.currentStep ?? job.currentStep;
319
- job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
320
- job.startedAt = status.startedAt ?? job.startedAt;
321
- if (status.lastUpdate !== undefined) job.updatedAt = status.lastUpdate;
322
- if (status.steps?.length) {
323
- const groups = normalizeParallelGroups(
324
- status.parallelGroups,
325
- status.steps.length,
326
- status.chainStepCount ?? status.steps.length,
327
- );
328
- job.parallelGroups = groups.length ? groups : job.parallelGroups;
329
- job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
330
- const activeGroup =
331
- status.currentStep !== undefined
332
- ? groups.find(
333
- (group) =>
334
- status.currentStep! >= group.start &&
335
- status.currentStep! < group.start + group.count,
336
- )
337
- : undefined;
338
- const visibleSteps = activeGroup
339
- ? status.steps
340
- .slice(activeGroup.start, activeGroup.start + activeGroup.count)
341
- .map((step, index) => ({ ...step, index: activeGroup.start + index }))
342
- : status.steps.map((step, index) => ({ ...step, index }));
343
- job.activeParallelGroup = Boolean(activeGroup);
344
- job.agents = visibleSteps.map((step) => step.agent);
345
- job.steps = visibleSteps;
346
- refreshNestedProjection();
347
- job.stepsTotal = visibleSteps.length;
348
- job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
349
- job.completedSteps = visibleSteps.filter(
350
- (step) => step.status === "complete" || step.status === "completed",
351
- ).length;
352
- if (status.state === "complete") job.completedSteps = visibleSteps.length;
353
- }
354
- job.sessionDir = status.sessionDir ?? job.sessionDir;
355
- job.outputFile = status.outputFile ?? job.outputFile;
356
- job.totalTokens = status.totalTokens ?? job.totalTokens;
357
- job.sessionFile = status.sessionFile ?? job.sessionFile;
358
- if (
359
- (job.status === "complete" || job.status === "failed" || job.status === "paused") &&
360
- !nestedRefreshFailed &&
361
- !hasLiveNestedDescendants(job.nestedChildren) &&
362
- (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))
363
- ) {
364
- scheduleCleanup(job.asyncId);
365
- }
366
- if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
367
- continue;
368
- }
369
- if (job.status === "queued") {
370
- job.status = "running";
371
- job.updatedAt = Date.now();
372
- }
373
- } catch (error) {
374
- if (job.status !== "failed") {
375
- console.error(`Failed to read async status for '${job.asyncDir}':`, error);
376
- job.status = "failed";
377
- job.updatedAt = Date.now();
378
- }
379
- if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
380
- scheduleCleanup(job.asyncId);
381
- }
382
- }
383
- if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
384
- }
385
-
386
- const pollUiCtx = liveUiContext();
387
- if (widgetChanged && ctxHasUI(pollUiCtx)) rerenderWidget(pollUiCtx!);
388
- }, pollIntervalMs);
389
- state.poller.unref?.();
180
+ hydrateActiveJobs();
390
181
  };
391
-
392
- const hydrateActiveJobs = (ctx?: ExtensionContext) => {
393
- if (ctxHasUI(ctx)) state.lastUiContext = ctx!;
394
- const retainedCtx = liveUiContext();
395
- const renderCtx = ctxHasUI(retainedCtx) ? retainedCtx : undefined;
396
- const currentCwd = (ctxCwd(ctx) ?? ctxCwd(retainedCtx) ?? state.baseCwd) || undefined;
397
- let summaries: AsyncRunSummary[];
182
+ const safeCtxCwd = (ctx?: ExtensionContext): string | undefined => {
183
+ if (!ctx) return undefined;
398
184
  try {
399
- summaries = listAsyncRuns(asyncDirRoot, {
400
- states: ACTIVE_HYDRATION_STATES,
401
- resultsDir,
402
- kill: options.kill,
403
- now: options.now,
404
- maxAgeMs: ACTIVE_HYDRATION_MAX_AGE_MS,
405
- });
406
- } catch (error) {
407
- console.error(`Failed to hydrate active async jobs from '${asyncDirRoot}':`, error);
408
- if (renderCtx) rerenderWidget(renderCtx);
409
- return;
410
- }
411
-
412
- for (const summary of summaries) {
413
- if (!shouldHydrateRunForCurrentUi(summary, state.currentSessionId, currentCwd)) continue;
414
- cancelCleanup(summary.id);
415
- state.asyncJobs.set(summary.id, asyncRunSummaryToJobState(summary, state.asyncJobs.get(summary.id)));
185
+ return ctx.cwd;
186
+ } catch {
187
+ // The extension ctx goes stale after session replacement or reload; a
188
+ // deferred hydration must degrade to the base cwd, never crash the host.
189
+ return undefined;
416
190
  }
417
-
418
- if (state.asyncJobs.size > 0) ensurePoller();
419
- if (renderCtx) rerenderWidget(renderCtx);
420
191
  };
421
-
192
+ const hydrateActiveJobs = (ctx?: ExtensionContext, cwdOverride?: string) => {
193
+ if (ctxHasUI(ctx)) state.lastUiContext = ctx!;
194
+ const currentCwd = cwdOverride ?? safeCtxCwd(ctx) ?? state.baseCwd;
195
+ const fileJobs = readActiveFileJobs(asyncDirRoot, state.currentSessionId, currentCwd);
196
+ const registryJobs = hydrateRegistryJobs(state, state.currentSessionId);
197
+ const next = new Map([...fileJobs, ...registryJobs]);
198
+ for (const id of state.asyncJobs.keys()) if (!next.has(id)) state.asyncJobs.delete(id);
199
+ for (const [id, job] of next) state.asyncJobs.set(id, job);
200
+ rerender();
201
+ };
422
202
  const handleStarted = (data: unknown) => {
423
203
  const info = data as AsyncStartedEvent;
424
204
  if (!info.id) return;
425
205
  const now = Date.now();
426
- const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
427
- let rawAgents: string[] | undefined;
428
- if (info.agents?.length) {
429
- rawAgents = info.agents;
430
- } else if (info.chain && info.chain.length > 0) {
431
- rawAgents = info.chain;
432
- } else if (info.agent) {
433
- rawAgents = [info.agent];
434
- }
435
- const validParallelGroups = normalizeParallelGroups(
436
- info.parallelGroups,
437
- Number.MAX_SAFE_INTEGER,
438
- info.chainStepCount ?? Number.MAX_SAFE_INTEGER,
439
- );
440
- const firstGroup = validParallelGroups.find((group) => group.start === 0);
441
- const firstGroupCount = firstGroup?.count;
442
- const agents = firstGroupCount && firstGroupCount > 0 ? rawAgents?.slice(0, firstGroupCount) : rawAgents;
443
206
  state.asyncJobs.set(info.id, {
444
207
  asyncId: info.id,
445
- asyncDir,
446
- status: "queued",
447
- pid: typeof info.pid === "number" ? info.pid : undefined,
448
- ...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
208
+ asyncDir: info.asyncDir ?? path.join(asyncDirRoot, info.id),
209
+ status: "running",
210
+ sessionId: info.sessionId,
449
211
  mode: info.mode ?? (info.chain ? "chain" : "single"),
450
- agents,
451
- chainStepCount: info.chainStepCount,
452
- parallelGroups: validParallelGroups,
453
- nestedRoute: info.nestedRoute,
454
- stepsTotal: firstGroupCount ?? agents?.length,
455
- hasParallelGroups: validParallelGroups.length > 0,
456
- activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
212
+ agents: info.agents ?? info.chain ?? (info.agent ? [info.agent] : undefined),
457
213
  startedAt: now,
458
214
  updatedAt: now,
459
215
  });
460
- ensurePoller();
461
- const startedUiCtx = liveUiContext();
462
- if (startedUiCtx) {
463
- rerenderWidget(startedUiCtx);
464
- }
216
+ rerender();
465
217
  };
466
-
467
218
  const handleComplete = (data: unknown) => {
468
- const result = data as { id?: string; success?: boolean; asyncDir?: string };
469
- const asyncId = result.id;
470
- if (!asyncId) return;
471
- const job = state.asyncJobs.get(asyncId);
472
- let nestedRefreshFailed = false;
219
+ const result = data as { id?: string; success?: boolean; status?: string };
220
+ if (!result.id) return;
221
+ const job = state.asyncJobs.get(result.id);
473
222
  if (job) {
474
- job.status = result.success ? "complete" : "failed";
223
+ job.status = result.status === "interrupted" ? "paused" : result.success === false ? "failed" : "complete";
475
224
  job.updatedAt = Date.now();
476
- if (result.asyncDir) job.asyncDir = result.asyncDir;
477
- try {
478
- updateAsyncJobNestedProjection(job);
479
- } catch (error) {
480
- nestedRefreshFailed = true;
481
- console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
482
- }
225
+ scheduleCleanup(result.id);
483
226
  }
484
- const completeUiCtx = liveUiContext();
485
- if (completeUiCtx) {
486
- rerenderWidget(completeUiCtx);
487
- }
488
- if (!nestedRefreshFailed && !hasLiveNestedDescendants(job?.nestedChildren)) scheduleCleanup(asyncId);
227
+ rerender();
489
228
  };
490
-
491
229
  const resetJobs = (ctx?: ExtensionContext) => {
492
- for (const timer of state.cleanupTimers.values()) {
493
- clearTimeout(timer);
494
- }
230
+ for (const timer of state.cleanupTimers.values()) clearTimeout(timer);
495
231
  state.cleanupTimers.clear();
496
232
  state.asyncJobs.clear();
497
233
  state.foregroundControls?.clear();
@@ -499,20 +235,16 @@ export function createAsyncJobTracker(
499
235
  state.resultFileCoalescer.clear();
500
236
  if (ctxHasUI(ctx)) state.lastUiContext = ctx!;
501
237
  };
502
-
503
- // Hydration scans the async run root synchronously; deferring it off the
504
- // session_start hot path keeps startup latency independent of run history.
505
- let pendingDeferredHydration: ReturnType<typeof setTimeout> | null = null;
238
+ let pending: ReturnType<typeof setTimeout> | null = null;
506
239
  const hydrateActiveJobsDeferred = (ctx?: ExtensionContext) => {
507
240
  if (ctxHasUI(ctx)) state.lastUiContext = ctx!;
508
- if (pendingDeferredHydration !== null) clearTimeout(pendingDeferredHydration);
509
- const timer = setTimeout(() => {
510
- pendingDeferredHydration = null;
511
- hydrateActiveJobs(ctx);
241
+ const capturedCwd = safeCtxCwd(ctx);
242
+ if (pending) clearTimeout(pending);
243
+ pending = setTimeout(() => {
244
+ pending = null;
245
+ hydrateActiveJobs(undefined, capturedCwd);
512
246
  }, 0);
513
- timer.unref?.();
514
- pendingDeferredHydration = timer;
247
+ pending.unref?.();
515
248
  };
516
-
517
249
  return { ensurePoller, handleStarted, handleComplete, resetJobs, hydrateActiveJobs, hydrateActiveJobsDeferred };
518
250
  }