@selesai/code 0.3.0 → 0.3.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 (230) hide show
  1. package/dist/core/diagnostics.d.ts +1 -0
  2. package/dist/core/diagnostics.d.ts.map +1 -1
  3. package/dist/core/diagnostics.js.map +1 -1
  4. package/dist/core/package-manager.d.ts +10 -0
  5. package/dist/core/package-manager.d.ts.map +1 -1
  6. package/dist/core/package-manager.js +93 -8
  7. package/dist/core/package-manager.js.map +1 -1
  8. package/dist/core/package-manager.test.d.ts +2 -0
  9. package/dist/core/package-manager.test.d.ts.map +1 -0
  10. package/dist/core/package-manager.test.js +20 -0
  11. package/dist/core/package-manager.test.js.map +1 -0
  12. package/dist/core/resource-loader.d.ts +3 -0
  13. package/dist/core/resource-loader.d.ts.map +1 -1
  14. package/dist/core/resource-loader.js +10 -0
  15. package/dist/core/resource-loader.js.map +1 -1
  16. package/dist/core/settings-manager.d.ts +1 -0
  17. package/dist/core/settings-manager.d.ts.map +1 -1
  18. package/dist/core/settings-manager.js.map +1 -1
  19. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  20. package/dist/extensions/caveman/index.js +116 -0
  21. package/dist/extensions/caveman/package.json +8 -0
  22. package/dist/extensions/caveman/test/extension.test.js +185 -0
  23. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  24. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  25. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  26. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  27. package/dist/extensions/pi-intercom/config.ts +2 -2
  28. package/dist/extensions/pi-intercom/index.ts +2 -2
  29. package/dist/extensions/pi-intercom/package.json +2 -2
  30. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  31. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  32. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  33. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  34. package/dist/extensions/pi-powerline-footer/index.ts +9 -16
  35. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  36. package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
  37. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  38. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  39. package/dist/extensions/pi-subagents/README.md +224 -52
  40. package/dist/extensions/pi-subagents/install.mjs +93 -0
  41. package/dist/extensions/pi-subagents/package.json +4 -5
  42. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  43. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  44. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  45. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  46. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  47. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  48. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  49. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  50. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  51. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  52. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  53. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  54. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  55. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  56. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  57. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  58. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  59. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  61. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  62. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  63. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  64. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  66. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  67. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  68. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  69. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  70. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  71. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  72. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  73. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  74. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  75. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  76. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  77. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  78. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  79. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  80. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  81. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  87. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  88. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  89. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  90. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  92. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  93. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  94. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  95. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  96. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  97. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  98. package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
  99. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  100. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  101. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  102. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  103. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  104. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  105. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  106. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  107. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  108. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  109. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  110. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  111. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  112. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  113. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  114. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  115. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  116. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  117. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  118. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  119. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  120. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  121. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  122. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  123. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  124. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  125. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  126. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  127. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  128. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  129. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  130. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  131. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  132. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  133. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  134. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  135. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  136. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  137. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  138. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  139. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  140. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  141. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  142. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  143. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  144. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  145. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  146. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  147. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  148. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  149. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  150. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  152. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  154. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  155. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  156. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  157. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  158. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  159. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  160. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  161. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  162. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  163. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  164. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  165. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  166. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  167. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  168. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  169. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  170. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  171. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  172. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  173. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  174. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  175. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  176. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  177. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  178. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  179. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  180. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  181. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  182. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  183. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  184. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  185. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  186. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  187. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  188. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  189. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  190. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  191. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  192. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  193. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  194. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  195. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  196. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  197. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  198. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  199. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  200. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  201. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  202. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  203. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  204. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  205. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  206. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  207. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  208. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  209. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  210. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  211. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  212. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  213. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  215. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  216. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  217. package/dist/main.d.ts.map +1 -1
  218. package/dist/main.js +4 -0
  219. package/dist/main.js.map +1 -1
  220. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  221. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  222. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  223. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  224. package/dist/skills/caveman/SKILL.md +49 -0
  225. package/dist/skills/handoff-text/SKILL.md +13 -0
  226. package/docs/docs.json +4 -0
  227. package/docs/extensions.md +2 -0
  228. package/docs/settings.md +15 -0
  229. package/docs/shared-host-extensions.md +109 -0
  230. package/package.json +1 -1
@@ -17,6 +17,7 @@ import { readStatus } from "../../shared/utils.ts";
17
17
  import { normalizeParallelGroups } from "./parallel-groups.ts";
18
18
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
19
19
  import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
20
+ import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
20
21
 
21
22
  interface AsyncJobTrackerOptions {
22
23
  completionRetentionMs?: number;
@@ -35,6 +36,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
35
36
  handleStarted: (data: unknown) => void;
36
37
  handleComplete: (data: unknown) => void;
37
38
  resetJobs: (ctx?: ExtensionContext) => void;
39
+ restoreActiveJobs: (ctx?: ExtensionContext) => void;
38
40
  } {
39
41
  const completionRetentionMs = options.completionRetentionMs ?? 10000;
40
42
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
@@ -43,6 +45,61 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
43
45
  renderWidget(ctx, jobs);
44
46
  ctx.ui.requestRender?.();
45
47
  };
48
+ const restoredControlEventCursor = (asyncDir: string) => {
49
+ try {
50
+ return fs.statSync(path.join(asyncDir, "events.jsonl")).size;
51
+ } catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
53
+ throw error;
54
+ }
55
+ };
56
+ const summaryToJob = (run: AsyncRunSummary): AsyncJobState => {
57
+ const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, run.chainStepCount ?? run.steps.length);
58
+ const activeGroup = run.currentStep !== undefined
59
+ ? groups.find((group) => run.currentStep! >= group.start && run.currentStep! < group.start + group.count)
60
+ : undefined;
61
+ const visibleSteps = activeGroup
62
+ ? run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
63
+ : run.steps.map((step, index) => ({ ...step, index }));
64
+ return {
65
+ asyncId: run.id,
66
+ asyncDir: run.asyncDir,
67
+ status: run.state,
68
+ sessionId: run.sessionId,
69
+ activityState: run.activityState,
70
+ lastActivityAt: run.lastActivityAt,
71
+ currentTool: run.currentTool,
72
+ currentToolStartedAt: run.currentToolStartedAt,
73
+ currentPath: run.currentPath,
74
+ turnCount: run.turnCount,
75
+ toolCount: run.toolCount,
76
+ mode: run.mode,
77
+ agents: visibleSteps.map((step) => step.agent),
78
+ currentStep: run.currentStep,
79
+ chainStepCount: run.chainStepCount,
80
+ parallelGroups: groups,
81
+ steps: visibleSteps,
82
+ stepsTotal: visibleSteps.length,
83
+ runningSteps: visibleSteps.filter((step) => step.status === "running").length,
84
+ completedSteps: visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length,
85
+ hasParallelGroups: groups.length > 0,
86
+ activeParallelGroup: Boolean(activeGroup),
87
+ startedAt: run.startedAt,
88
+ updatedAt: run.lastUpdate ?? run.startedAt,
89
+ timeoutMs: run.timeoutMs,
90
+ deadlineAt: run.deadlineAt,
91
+ timedOut: run.timedOut,
92
+ turnBudget: run.turnBudget,
93
+ turnBudgetExceeded: run.turnBudgetExceeded,
94
+ wrapUpRequested: run.wrapUpRequested,
95
+ sessionDir: run.sessionDir,
96
+ outputFile: run.outputFile,
97
+ totalTokens: run.totalTokens,
98
+ sessionFile: run.sessionFile,
99
+ controlEventCursor: restoredControlEventCursor(run.asyncDir),
100
+ nestedChildren: run.nestedChildren,
101
+ };
102
+ };
46
103
  const cancelCleanup = (asyncId: string) => {
47
104
  const existingTimer = state.cleanupTimers.get(asyncId);
48
105
  if (!existingTimer) return;
@@ -248,6 +305,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
248
305
  job.sessionDir = status.sessionDir ?? job.sessionDir;
249
306
  job.outputFile = status.outputFile ?? job.outputFile;
250
307
  job.totalTokens = status.totalTokens ?? job.totalTokens;
308
+ job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
309
+ job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
310
+ job.timedOut = status.timedOut ?? job.timedOut;
311
+ job.turnBudget = status.turnBudget ?? job.turnBudget;
312
+ job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
313
+ job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
251
314
  job.sessionFile = status.sessionFile ?? job.sessionFile;
252
315
  if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
253
316
  scheduleCleanup(job.asyncId);
@@ -280,6 +343,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
280
343
  const handleStarted = (data: unknown) => {
281
344
  const info = data as AsyncStartedEvent;
282
345
  if (!info.id) return;
346
+ if (typeof state.currentSessionId === "string" && info.sessionId !== state.currentSessionId) return;
283
347
  const now = Date.now();
284
348
  const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
285
349
  const rawAgents = info.agents?.length ? info.agents : info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
@@ -305,6 +369,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
305
369
  activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
306
370
  startedAt: now,
307
371
  updatedAt: now,
372
+ timeoutMs: info.timeoutMs,
373
+ deadlineAt: info.deadlineAt,
374
+ turnBudget: info.turnBudget,
308
375
  controlEventCursor: 0,
309
376
  });
310
377
  ensurePoller();
@@ -314,7 +381,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
314
381
  };
315
382
 
316
383
  const handleComplete = (data: unknown) => {
317
- const result = data as { id?: string; success?: boolean; asyncDir?: string };
384
+ const result = data as { id?: string; success?: boolean; asyncDir?: string; sessionId?: string };
385
+ if (typeof state.currentSessionId === "string" && result.sessionId !== state.currentSessionId) return;
318
386
  const asyncId = result.id;
319
387
  if (!asyncId) return;
320
388
  const job = state.asyncJobs.get(asyncId);
@@ -351,5 +419,23 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
351
419
  }
352
420
  };
353
421
 
354
- return { ensurePoller, handleStarted, handleComplete, resetJobs };
422
+ const restoreActiveJobs = (ctx?: ExtensionContext) => {
423
+ if (ctx?.hasUI) state.lastUiContext = ctx;
424
+ if (!state.currentSessionId) return;
425
+ let runs: AsyncRunSummary[];
426
+ try {
427
+ runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: state.currentSessionId, resultsDir, kill: options.kill, now: options.now });
428
+ } catch (error) {
429
+ console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
430
+ return;
431
+ }
432
+ for (const run of runs) {
433
+ state.asyncJobs.set(run.id, summaryToJob(run));
434
+ }
435
+ if (runs.length === 0) return;
436
+ ensurePoller();
437
+ if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
438
+ };
439
+
440
+ return { ensurePoller, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
355
441
  }
@@ -2,9 +2,9 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { 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";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
6
6
  import { readStatus } from "../../shared/utils.ts";
7
- import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot } from "../shared/nested-events.ts";
7
+ import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
8
8
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
9
9
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
10
10
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
@@ -27,13 +27,20 @@ interface AsyncRunStepSummary {
27
27
  recentOutput?: string[];
28
28
  turnCount?: number;
29
29
  toolCount?: number;
30
+ steerCount?: number;
31
+ lastSteerAt?: number;
30
32
  durationMs?: number;
31
33
  tokens?: TokenUsage;
34
+ totalCost?: CostSummary;
32
35
  skills?: string[];
33
36
  model?: string;
34
37
  thinking?: string;
35
38
  attemptedModels?: string[];
36
39
  error?: string;
40
+ timedOut?: boolean;
41
+ turnBudget?: TurnBudgetState;
42
+ turnBudgetExceeded?: boolean;
43
+ wrapUpRequested?: boolean;
37
44
  children?: NestedRunSummary[];
38
45
  }
39
46
 
@@ -42,6 +49,7 @@ export interface AsyncRunSummary {
42
49
  asyncDir: string;
43
50
  sessionId?: string;
44
51
  state: "queued" | "running" | "complete" | "failed" | "paused";
52
+ error?: string;
45
53
  activityState?: ActivityState;
46
54
  lastActivityAt?: number;
47
55
  currentTool?: string;
@@ -49,11 +57,19 @@ export interface AsyncRunSummary {
49
57
  currentPath?: string;
50
58
  turnCount?: number;
51
59
  toolCount?: number;
60
+ steerCount?: number;
61
+ lastSteerAt?: number;
52
62
  mode: SubagentRunMode;
53
63
  cwd?: string;
54
64
  startedAt: number;
55
65
  lastUpdate?: number;
56
66
  endedAt?: number;
67
+ timeoutMs?: number;
68
+ deadlineAt?: number;
69
+ timedOut?: boolean;
70
+ turnBudget?: TurnBudgetState;
71
+ turnBudgetExceeded?: boolean;
72
+ wrapUpRequested?: boolean;
57
73
  currentStep?: number;
58
74
  chainStepCount?: number;
59
75
  pendingAppends?: number;
@@ -62,6 +78,7 @@ export interface AsyncRunSummary {
62
78
  sessionDir?: string;
63
79
  outputFile?: string;
64
80
  totalTokens?: TokenUsage;
81
+ totalCost?: CostSummary;
65
82
  sessionFile?: string;
66
83
  nestedChildren?: NestedRunSummary[];
67
84
  nestedWarnings?: string[];
@@ -122,7 +139,7 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
122
139
  };
123
140
  }
124
141
 
125
- function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = []): AsyncRunSummary {
142
+ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = [], nestedRoute?: NestedRoute): AsyncRunSummary {
126
143
  if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
127
144
  throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
128
145
  }
@@ -131,9 +148,11 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
131
148
  const chainStepCount = status.chainStepCount ?? steps.length;
132
149
  const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
133
150
  let nestedChildren: NestedRunSummary[] = [];
134
- if (nestedWarnings.length === 0) {
151
+ if (nestedWarnings.length === 0 && nestedRoute) {
135
152
  try {
136
- nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir))?.children ?? [];
153
+ // The route is resolved by the caller via buildNestedRouteIndex, so this
154
+ // avoids a fresh scan of the nested-events directory per run.
155
+ nestedChildren = projectNestedEvents(nestedRoute)?.children ?? [];
137
156
  } catch (error) {
138
157
  nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
139
158
  }
@@ -159,13 +178,20 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
159
178
  ...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
160
179
  ...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
161
180
  ...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
181
+ ...(step.steerCount !== undefined ? { steerCount: step.steerCount } : {}),
182
+ ...(step.lastSteerAt !== undefined ? { lastSteerAt: step.lastSteerAt } : {}),
162
183
  ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
163
184
  ...(step.tokens ? { tokens: step.tokens } : {}),
185
+ ...(step.totalCost ? { totalCost: step.totalCost } : {}),
164
186
  ...(step.skills ? { skills: step.skills } : {}),
165
187
  ...(step.model ? { model: step.model } : {}),
166
188
  ...(step.thinking ? { thinking: step.thinking } : {}),
167
189
  ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
168
190
  ...(step.error ? { error: step.error } : {}),
191
+ ...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
192
+ ...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
193
+ ...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
194
+ ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
169
195
  ...(step.children?.length ? { children: step.children } : {}),
170
196
  };
171
197
  });
@@ -175,6 +201,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
175
201
  asyncDir,
176
202
  ...(status.sessionId ? { sessionId: status.sessionId } : {}),
177
203
  state: status.state,
204
+ ...(status.error ? { error: status.error } : {}),
178
205
  activityState,
179
206
  lastActivityAt,
180
207
  currentTool: status.currentTool,
@@ -182,11 +209,19 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
182
209
  currentPath: status.currentPath,
183
210
  turnCount: status.turnCount,
184
211
  toolCount: status.toolCount,
212
+ steerCount: status.steerCount,
213
+ lastSteerAt: status.lastSteerAt,
185
214
  mode: status.mode,
186
215
  cwd: status.cwd,
187
216
  startedAt: status.startedAt,
188
217
  lastUpdate: status.lastUpdate,
189
218
  endedAt: status.endedAt,
219
+ ...(status.timeoutMs !== undefined ? { timeoutMs: status.timeoutMs } : {}),
220
+ ...(status.deadlineAt !== undefined ? { deadlineAt: status.deadlineAt } : {}),
221
+ ...(status.timedOut !== undefined ? { timedOut: status.timedOut } : {}),
222
+ ...(status.turnBudget ? { turnBudget: status.turnBudget } : {}),
223
+ ...(status.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: status.turnBudgetExceeded } : {}),
224
+ ...(status.wrapUpRequested !== undefined ? { wrapUpRequested: status.wrapUpRequested } : {}),
190
225
  currentStep: status.currentStep,
191
226
  ...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
192
227
  ...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
@@ -197,6 +232,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
197
232
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
198
233
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
199
234
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
235
+ ...(status.totalCost ? { totalCost: status.totalCost } : {}),
200
236
  ...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
201
237
  };
202
238
  }
@@ -233,6 +269,16 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
233
269
 
234
270
  const allowedStates = options.states ? new Set(options.states) : undefined;
235
271
  const runs: AsyncRunSummary[] = [];
272
+ // Route resolution for every run shares a single index built from the
273
+ // nested-events directory, so the per-run lookup is O(1) instead of scanning
274
+ // the directory once per run. The index is built lazily on first use, so
275
+ // load-time restoration (which only wants queued/running runs) skips it
276
+ // entirely when no active runs match.
277
+ let nestedRouteIndex: Map<string, NestedRoute> | undefined;
278
+ const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
279
+ if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
280
+ return nestedRouteIndex.get(rootRunId);
281
+ };
236
282
  for (const entry of entries) {
237
283
  const asyncDir = path.join(asyncDirRoot, entry);
238
284
  const reconciliation = options.reconcile === false
@@ -240,16 +286,21 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
240
286
  : reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
241
287
  const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
242
288
  if (!status) continue;
289
+ // Filter before the nested-route lookup: the lookup builds an index over
290
+ // the nested-events directory, so deferring it for filtered-out runs keeps
291
+ // restoration at load from scanning that directory when no active runs
292
+ // match.
293
+ if (allowedStates && !allowedStates.has(status.state)) continue;
294
+ if (options.sessionId && status.sessionId !== options.sessionId) continue;
243
295
  const nestedWarnings: string[] = [];
296
+ let nestedRoute: NestedRoute | undefined;
244
297
  try {
245
- const nestedRoute = findNestedRouteForRootId(status.runId || path.basename(asyncDir));
298
+ nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
246
299
  if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
247
300
  } catch (error) {
248
301
  nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
249
302
  }
250
- const summary = statusToSummary(asyncDir, status, nestedWarnings);
251
- if (allowedStates && !allowedStates.has(summary.state)) continue;
252
- if (options.sessionId && summary.sessionId !== options.sessionId) continue;
303
+ const summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
253
304
  runs.push(summary);
254
305
  }
255
306
 
@@ -257,13 +308,18 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
257
308
  return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
258
309
  }
259
310
 
260
- function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number }): string | undefined {
311
+ function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number; steerCount?: number; lastSteerAt?: number; turnBudget?: TurnBudgetState; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean }): string | undefined {
261
312
  const facts: string[] = [];
262
313
  if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
263
314
  else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
264
315
  if (input.currentPath) facts.push(shortenPath(input.currentPath));
265
316
  if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
317
+ if (input.turnBudgetExceeded && input.turnBudget) facts.push(`turn budget exceeded ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
318
+ else if (input.wrapUpRequested && input.turnBudget) facts.push(`wrap-up requested ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}`);
319
+ else if (input.turnBudget) facts.push(`turn budget ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
266
320
  if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
321
+ if (input.steerCount !== undefined) facts.push(`${input.steerCount} steers`);
322
+ if (typeof input.lastSteerAt === "number" && Number.isFinite(input.lastSteerAt)) facts.push(`last steer ${new Date(input.lastSteerAt).toISOString()}`);
267
323
  const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
268
324
  return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
269
325
  }
@@ -328,6 +384,7 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
328
384
  const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
329
385
  const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
330
386
  lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
387
+ if (run.error) lines.push(` Error: ${run.error}`);
331
388
  for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
332
389
  const outputPath = formatAsyncRunOutputPath(run);
333
390
  if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { AcceptanceLedger, AsyncStatus, ModelAttempt } from "../../shared/types.ts";
3
+ import type { AcceptanceLedger, AsyncStatus, CostSummary, ModelAttempt } from "../../shared/types.ts";
4
4
  import { readStatus } from "../../shared/utils.ts";
5
5
 
6
6
  export interface ImportedAsyncRoot {
@@ -21,26 +21,32 @@ export interface ImportedAsyncRootResult {
21
21
  model?: string;
22
22
  attemptedModels?: string[];
23
23
  modelAttempts?: ModelAttempt[];
24
+ totalCost?: CostSummary;
24
25
  structuredOutput?: unknown;
25
26
  structuredOutputPath?: string;
26
27
  structuredOutputSchemaPath?: string;
27
28
  acceptance?: AcceptanceLedger;
29
+ timedOut?: boolean;
28
30
  }
29
31
 
30
32
  interface AsyncResultFile {
31
33
  state?: string;
32
34
  success?: boolean;
33
35
  summary?: string;
36
+ error?: string;
37
+ timedOut?: boolean;
34
38
  results?: Array<{
35
39
  agent?: string;
36
40
  output?: string;
37
41
  error?: string;
38
42
  success?: boolean;
43
+ timedOut?: boolean;
39
44
  sessionFile?: string;
40
45
  intercomTarget?: string;
41
46
  model?: string;
42
47
  attemptedModels?: string[];
43
48
  modelAttempts?: ModelAttempt[];
49
+ totalCost?: CostSummary;
44
50
  structuredOutput?: unknown;
45
51
  structuredOutputPath?: string;
46
52
  structuredOutputSchemaPath?: string;
@@ -85,6 +91,7 @@ function resultState(result: AsyncResultFile | undefined, child: NonNullable<Asy
85
91
 
86
92
  function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus, step: NonNullable<AsyncStatus["steps"]>[number] | undefined): ImportedAsyncRootResult {
87
93
  const agent = step?.agent ?? status.steps?.[root.index]?.agent ?? "subagent";
94
+ const timedOut = step?.timedOut === true || status.timedOut === true;
88
95
  const message = step?.error ?? status.error ?? `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`;
89
96
  return {
90
97
  agent,
@@ -92,10 +99,12 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
92
99
  success: false,
93
100
  exitCode: 1,
94
101
  error: message,
102
+ ...(timedOut ? { timedOut: true } : {}),
95
103
  ...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
96
104
  ...(step?.model ? { model: step.model } : {}),
97
105
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
98
106
  ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
107
+ ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
99
108
  ...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
100
109
  ...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
101
110
  ...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
@@ -103,25 +112,45 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
103
112
  };
104
113
  }
105
114
 
115
+ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null, message: string): ImportedAsyncRootResult {
116
+ const step = selectedStatusStep(status, root.index);
117
+ return {
118
+ agent: step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent",
119
+ output: message,
120
+ success: false,
121
+ exitCode: 1,
122
+ error: message,
123
+ timedOut: true,
124
+ ...(step?.sessionFile ?? status?.sessionFile ? { sessionFile: step?.sessionFile ?? status?.sessionFile } : {}),
125
+ ...(step?.model ? { model: step.model } : {}),
126
+ ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
127
+ ...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
128
+ ...(step?.totalCost ? { totalCost: step.totalCost } : {}),
129
+ };
130
+ }
131
+
106
132
  function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null, result: AsyncResultFile): ImportedAsyncRootResult {
107
133
  const child = result.results?.[root.index];
108
134
  const step = selectedStatusStep(status, root.index);
109
135
  const state = resultState(result, child);
110
136
  const agent = child?.agent ?? step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent";
111
137
  const output = child?.output ?? result.summary ?? "";
112
- const success = state === "complete";
113
- const error = child?.error ?? (success ? undefined : result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
138
+ const timedOut = child?.timedOut === true || step?.timedOut === true || result.timedOut === true || status?.timedOut === true;
139
+ const success = state === "complete" && !timedOut;
140
+ const error = child?.error ?? (success ? undefined : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
114
141
  return {
115
142
  agent,
116
143
  output: success ? output : (output || error || ""),
117
144
  success,
118
145
  exitCode: success ? 0 : 1,
119
146
  ...(error ? { error } : {}),
147
+ ...(timedOut ? { timedOut: true } : {}),
120
148
  ...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
121
149
  ...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
122
150
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
123
151
  ...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
124
152
  ...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
153
+ ...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
125
154
  ...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
126
155
  ...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
127
156
  ...(child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: child?.structuredOutputSchemaPath ?? step?.structuredOutputSchemaPath } : {}),
@@ -131,7 +160,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
131
160
 
132
161
  export async function waitForImportedAsyncRoot(
133
162
  root: ImportedAsyncRoot,
134
- options: { pollIntervalMs?: number; terminalResultGraceMs?: number; now?: () => number } = {},
163
+ options: { pollIntervalMs?: number; terminalResultGraceMs?: number; now?: () => number; shouldAbort?: () => boolean; timeoutMessage?: string } = {},
135
164
  ): Promise<ImportedAsyncRootResult> {
136
165
  const pollIntervalMs = options.pollIntervalMs ?? 500;
137
166
  const terminalResultGraceMs = options.terminalResultGraceMs ?? 1_000;
@@ -139,6 +168,7 @@ export async function waitForImportedAsyncRoot(
139
168
  let terminalSince: number | undefined;
140
169
  for (;;) {
141
170
  const status = readStatus(root.asyncDir);
171
+ if (options.shouldAbort?.()) return outputFromTimeout(root, status, options.timeoutMessage ?? "Subagent timed out.");
142
172
  const result = readResultFile(root.resultPath);
143
173
  if (result) return buildImportedResult(root, status, result);
144
174
  if (isTerminalStatus(status, root.index)) {
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Smart completion batching with straggler handling.
3
+ *
4
+ * Holds successful async-completion notifications briefly so sibling jobs that
5
+ * finish within a short window arrive as a single grouped message. A hard
6
+ * max-wait cap (measured from the first item in a group) prevents holding
7
+ * notifications indefinitely. After a group is emitted, late-finishing
8
+ * siblings that arrive within the straggler window join a shorter "straggler"
9
+ * group with reduced debounce and max-wait timers.
10
+ *
11
+ * Failure and attention signals bypass this batcher entirely. Callers must
12
+ * flush() held items and emit those signals immediately so failures and
13
+ * needs-attention notices are never delayed.
14
+ */
15
+
16
+ import type { CompletionBatchConfig } from "../../shared/types.ts";
17
+
18
+ export type { CompletionBatchConfig };
19
+
20
+ export interface ResolvedCompletionBatchConfig {
21
+ enabled: boolean;
22
+ debounceMs: number;
23
+ maxWaitMs: number;
24
+ stragglerDebounceMs: number;
25
+ stragglerMaxWaitMs: number;
26
+ stragglerWindowMs: number;
27
+ }
28
+
29
+ export const DEFAULT_COMPLETION_BATCH_CONFIG: ResolvedCompletionBatchConfig = {
30
+ enabled: true,
31
+ debounceMs: 150,
32
+ maxWaitMs: 1000,
33
+ stragglerDebounceMs: 75,
34
+ stragglerMaxWaitMs: 400,
35
+ stragglerWindowMs: 2000,
36
+ };
37
+
38
+ function parsePositiveInt(value: unknown): number | undefined {
39
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) return undefined;
40
+ return value;
41
+ }
42
+
43
+ export function resolveCompletionBatchConfig(
44
+ globalConfig?: CompletionBatchConfig,
45
+ override?: CompletionBatchConfig,
46
+ ): ResolvedCompletionBatchConfig {
47
+ const enabled = typeof override?.enabled === "boolean"
48
+ ? override.enabled
49
+ : typeof globalConfig?.enabled === "boolean"
50
+ ? globalConfig.enabled
51
+ : DEFAULT_COMPLETION_BATCH_CONFIG.enabled;
52
+ return {
53
+ enabled,
54
+ debounceMs: parsePositiveInt(override?.debounceMs) ?? parsePositiveInt(globalConfig?.debounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.debounceMs,
55
+ maxWaitMs: parsePositiveInt(override?.maxWaitMs) ?? parsePositiveInt(globalConfig?.maxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.maxWaitMs,
56
+ stragglerDebounceMs: parsePositiveInt(override?.stragglerDebounceMs) ?? parsePositiveInt(globalConfig?.stragglerDebounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerDebounceMs,
57
+ stragglerMaxWaitMs: parsePositiveInt(override?.stragglerMaxWaitMs) ?? parsePositiveInt(globalConfig?.stragglerMaxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerMaxWaitMs,
58
+ stragglerWindowMs: parsePositiveInt(override?.stragglerWindowMs) ?? parsePositiveInt(globalConfig?.stragglerWindowMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerWindowMs,
59
+ };
60
+ }
61
+
62
+ type TimerHandle = unknown;
63
+
64
+ interface TimerApi {
65
+ setTimeout(handler: () => void, delayMs: number): TimerHandle;
66
+ clearTimeout(handle: TimerHandle): void;
67
+ }
68
+
69
+ const defaultTimers: TimerApi = {
70
+ setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
71
+ clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
72
+ };
73
+
74
+ function unrefHandle(handle: TimerHandle): void {
75
+ if (handle && typeof handle === "object" && "unref" in handle && typeof (handle as { unref: unknown }).unref === "function") {
76
+ (handle as { unref: () => void }).unref();
77
+ }
78
+ }
79
+
80
+ export interface CompletionBatcherOptions<T> {
81
+ config: ResolvedCompletionBatchConfig;
82
+ emit: (items: T[]) => void;
83
+ timers?: TimerApi;
84
+ now?: () => number;
85
+ }
86
+
87
+ export interface CompletionBatcher<T> {
88
+ /** Add a batchable item. Emits immediately when batching is disabled. */
89
+ push(item: T): void;
90
+ /** Emit any held items immediately as a single group. */
91
+ flush(): void;
92
+ /** Clear timers without emitting. */
93
+ dispose(): void;
94
+ }
95
+
96
+ /**
97
+ * Create a completion batcher. The batcher is single-use per registration: it
98
+ * holds at most one open group. `flush` forces emission; `dispose` tears down
99
+ * timers for reload/shutdown without emitting.
100
+ */
101
+ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
102
+ const timers = options.timers ?? defaultTimers;
103
+ const now = options.now ?? Date.now;
104
+ const config = options.config;
105
+
106
+ if (!config.enabled) {
107
+ return {
108
+ push(item: T) {
109
+ options.emit([item]);
110
+ },
111
+ flush() {},
112
+ dispose() {},
113
+ };
114
+ }
115
+
116
+ let pending: T[] = [];
117
+ let debounceTimer: TimerHandle | null = null;
118
+ let maxWaitTimer: TimerHandle | null = null;
119
+ let straggler = false;
120
+ let lastEmitAt: number | null = null;
121
+
122
+ const clearTimers = () => {
123
+ if (debounceTimer) {
124
+ timers.clearTimeout(debounceTimer);
125
+ debounceTimer = null;
126
+ }
127
+ if (maxWaitTimer) {
128
+ timers.clearTimeout(maxWaitTimer);
129
+ maxWaitTimer = null;
130
+ }
131
+ };
132
+
133
+ const emitGroup = () => {
134
+ clearTimers();
135
+ if (pending.length === 0) return;
136
+ const items = pending;
137
+ pending = [];
138
+ lastEmitAt = now();
139
+ options.emit(items);
140
+ };
141
+
142
+ return {
143
+ push(item: T) {
144
+ if (pending.length === 0) {
145
+ straggler = lastEmitAt !== null && (now() - lastEmitAt) < config.stragglerWindowMs;
146
+ }
147
+ pending.push(item);
148
+
149
+ if (debounceTimer) timers.clearTimeout(debounceTimer);
150
+ const debounceDelay = straggler ? config.stragglerDebounceMs : config.debounceMs;
151
+ debounceTimer = timers.setTimeout(emitGroup, debounceDelay);
152
+ unrefHandle(debounceTimer);
153
+
154
+ if (!maxWaitTimer) {
155
+ const maxWaitDelay = straggler ? config.stragglerMaxWaitMs : config.maxWaitMs;
156
+ maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);
157
+ unrefHandle(maxWaitTimer);
158
+ }
159
+ },
160
+ flush: emitGroup,
161
+ dispose() {
162
+ clearTimers();
163
+ pending = [];
164
+ },
165
+ };
166
+ }