@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
@@ -0,0 +1,255 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { checkPidLiveness, reconcileAsyncRun } from "../../src/runs/background/stale-run-reconciler.ts";
7
+
8
+ function tempRoot(prefix: string): string {
9
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
10
+ }
11
+
12
+ function writeStatus(asyncDir: string, status: Record<string, unknown>): void {
13
+ fs.mkdirSync(asyncDir, { recursive: true });
14
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
15
+ }
16
+
17
+ function errno(code: string): NodeJS.ErrnoException {
18
+ const error = new Error(code) as NodeJS.ErrnoException;
19
+ error.code = code;
20
+ return error;
21
+ }
22
+
23
+ describe("async stale-run reconciliation", () => {
24
+ it("classifies pid liveness without treating EPERM as dead", () => {
25
+ assert.equal(checkPidLiveness(123, () => true), "alive");
26
+ assert.equal(checkPidLiveness(123, () => { throw errno("ESRCH"); }), "dead");
27
+ assert.equal(checkPidLiveness(123, () => { throw errno("EPERM"); }), "unknown");
28
+ assert.equal(checkPidLiveness(123, () => { throw new Error("boom"); }), "unknown");
29
+ });
30
+
31
+ it("marks a running async run failed when the runner pid is dead and no result exists", () => {
32
+ const root = tempRoot("pi-stale-run-");
33
+ try {
34
+ const asyncDir = path.join(root, "run-dead");
35
+ const resultsDir = path.join(root, "results");
36
+ writeStatus(asyncDir, {
37
+ runId: "run-dead",
38
+ sessionId: "session-current",
39
+ mode: "single",
40
+ state: "running",
41
+ pid: 12345,
42
+ startedAt: 1000,
43
+ lastUpdate: 1000,
44
+ currentStep: 0,
45
+ steps: [{ agent: "scout", status: "running", startedAt: 1000 }],
46
+ });
47
+
48
+ const result = reconcileAsyncRun(asyncDir, {
49
+ resultsDir,
50
+ kill: () => { throw errno("ESRCH"); },
51
+ now: () => 2000,
52
+ });
53
+
54
+ assert.equal(result.repaired, true);
55
+ assert.equal(result.status?.state, "failed");
56
+ assert.match(result.message ?? "", /process 12345 exited or disappeared/);
57
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8"));
58
+ assert.equal(status.state, "failed");
59
+ assert.equal(status.sessionId, "session-current");
60
+ assert.equal(status.steps[0].status, "failed");
61
+ assert.match(status.steps[0].error, /process 12345 exited or disappeared/);
62
+ const resultJson = JSON.parse(fs.readFileSync(path.join(resultsDir, "run-dead.json"), "utf-8"));
63
+ assert.equal(resultJson.success, false);
64
+ assert.equal(resultJson.sessionId, "session-current");
65
+ assert.equal(resultJson.state, "failed");
66
+ assert.equal(resultJson.exitCode, 1);
67
+ assert.match(resultJson.summary, /process 12345 exited or disappeared/);
68
+ assert.match(fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8"), /subagent\.run\.repaired_stale/);
69
+ } finally {
70
+ fs.rmSync(root, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ it("includes runner stderr diagnostics when repairing a stale startup crash", () => {
75
+ const root = tempRoot("pi-stale-run-stderr-");
76
+ try {
77
+ const asyncDir = path.join(root, "run-dead-stderr");
78
+ const resultsDir = path.join(root, "results");
79
+ writeStatus(asyncDir, {
80
+ runId: "run-dead-stderr",
81
+ mode: "single",
82
+ state: "running",
83
+ pid: 12345,
84
+ startedAt: 1000,
85
+ lastUpdate: 1000,
86
+ currentStep: 0,
87
+ steps: [{ agent: "scout", status: "running", startedAt: 1000 }],
88
+ });
89
+ fs.writeFileSync(path.join(asyncDir, "runner.stderr.log"), "startup failed\nmissing peer package\n", "utf-8");
90
+
91
+ const result = reconcileAsyncRun(asyncDir, {
92
+ resultsDir,
93
+ kill: () => { throw errno("ESRCH"); },
94
+ now: () => 2000,
95
+ });
96
+
97
+ assert.equal(result.repaired, true);
98
+ assert.match(result.message ?? "", /Runner stderr tail:/);
99
+ assert.match(result.message ?? "", /missing peer package/);
100
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8"));
101
+ assert.match(status.steps[0].error, /missing peer package/);
102
+ const resultJson = JSON.parse(fs.readFileSync(path.join(resultsDir, "run-dead-stderr.json"), "utf-8"));
103
+ assert.match(resultJson.summary, /missing peer package/);
104
+ } finally {
105
+ fs.rmSync(root, { recursive: true, force: true });
106
+ }
107
+ });
108
+
109
+ it("keeps stale repair successful when the event log cannot be appended", () => {
110
+ const root = tempRoot("pi-stale-event-log-collision-");
111
+ try {
112
+ const asyncDir = path.join(root, "run-dead-events-dir");
113
+ const resultsDir = path.join(root, "results");
114
+ writeStatus(asyncDir, {
115
+ runId: "run-dead-events-dir",
116
+ mode: "single",
117
+ state: "running",
118
+ pid: 12345,
119
+ startedAt: 1000,
120
+ lastUpdate: 1000,
121
+ currentStep: 0,
122
+ steps: [{ agent: "worker", status: "running", startedAt: 1000 }],
123
+ });
124
+ fs.mkdirSync(path.join(asyncDir, "events.jsonl"));
125
+
126
+ const result = reconcileAsyncRun(asyncDir, {
127
+ resultsDir,
128
+ kill: () => { throw errno("ESRCH"); },
129
+ now: () => 2000,
130
+ });
131
+
132
+ assert.equal(result.repaired, true);
133
+ assert.equal(result.status?.state, "failed");
134
+ assert.equal(JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")).state, "failed");
135
+ assert.equal(JSON.parse(fs.readFileSync(path.join(resultsDir, "run-dead-events-dir.json"), "utf-8")).success, false);
136
+ assert.equal(fs.statSync(path.join(asyncDir, "events.jsonl")).isDirectory(), true);
137
+ } finally {
138
+ fs.rmSync(root, { recursive: true, force: true });
139
+ }
140
+ });
141
+
142
+ it("repairs stale status with per-child result outcomes", () => {
143
+ const root = tempRoot("pi-stale-mixed-result-");
144
+ try {
145
+ const asyncDir = path.join(root, "run-mixed");
146
+ const resultsDir = path.join(root, "results");
147
+ fs.mkdirSync(resultsDir, { recursive: true });
148
+ writeStatus(asyncDir, {
149
+ runId: "run-mixed",
150
+ mode: "chain",
151
+ state: "running",
152
+ pid: 12345,
153
+ startedAt: 1000,
154
+ lastUpdate: 1000,
155
+ steps: [
156
+ { agent: "scout", status: "running", startedAt: 1000 },
157
+ { agent: "worker", status: "running", startedAt: 1100 },
158
+ ],
159
+ });
160
+ const scoutSession = path.join(root, "scout.jsonl");
161
+ const workerSession = path.join(root, "worker.jsonl");
162
+ fs.writeFileSync(path.join(resultsDir, "run-mixed.json"), JSON.stringify({
163
+ id: "run-mixed",
164
+ success: false,
165
+ state: "failed",
166
+ results: [
167
+ { agent: "scout", success: true, sessionFile: scoutSession, model: "fast" },
168
+ { agent: "worker", success: false, error: "boom", sessionFile: workerSession, model: "careful" },
169
+ ],
170
+ }, null, 2), "utf-8");
171
+
172
+ const result = reconcileAsyncRun(asyncDir, {
173
+ resultsDir,
174
+ kill: () => { throw errno("ESRCH"); },
175
+ now: () => 2000,
176
+ });
177
+
178
+ assert.equal(result.repaired, true);
179
+ assert.equal(result.status?.state, "failed");
180
+ assert.equal(result.status?.steps?.[0]?.status, "complete");
181
+ assert.equal(result.status?.steps?.[0]?.exitCode, 0);
182
+ assert.equal(result.status?.steps?.[0]?.model, "fast");
183
+ assert.equal(result.status?.steps?.[0]?.sessionFile, scoutSession);
184
+ assert.equal(result.status?.steps?.[1]?.status, "failed");
185
+ assert.equal(result.status?.steps?.[1]?.exitCode, 1);
186
+ assert.equal(result.status?.steps?.[1]?.error, "boom");
187
+ assert.equal(result.status?.steps?.[1]?.model, "careful");
188
+ assert.equal(result.status?.steps?.[1]?.sessionFile, workerSession);
189
+ } finally {
190
+ fs.rmSync(root, { recursive: true, force: true });
191
+ }
192
+ });
193
+
194
+ it("fails a stale run when a live pid has not updated beyond the stale threshold", () => {
195
+ const root = tempRoot("pi-stale-live-pid-");
196
+ try {
197
+ const asyncDir = path.join(root, "run-reused-pid");
198
+ const resultsDir = path.join(root, "results");
199
+ writeStatus(asyncDir, {
200
+ runId: "run-reused-pid",
201
+ mode: "single",
202
+ state: "running",
203
+ pid: 12345,
204
+ startedAt: 1000,
205
+ lastUpdate: 1000,
206
+ steps: [{ agent: "worker", status: "running", startedAt: 1000 }],
207
+ });
208
+
209
+ const result = reconcileAsyncRun(asyncDir, {
210
+ resultsDir,
211
+ kill: () => true,
212
+ now: () => 5000,
213
+ staleAlivePidMs: 1000,
214
+ });
215
+
216
+ assert.equal(result.repaired, true);
217
+ assert.equal(result.status?.state, "failed");
218
+ assert.match(result.message ?? "", /live PID, but status has not updated/);
219
+ } finally {
220
+ fs.rmSync(root, { recursive: true, force: true });
221
+ }
222
+ });
223
+
224
+ it("preserves an existing result instead of overwriting it with stale-run failure", () => {
225
+ const root = tempRoot("pi-stale-existing-result-");
226
+ try {
227
+ const asyncDir = path.join(root, "run-result");
228
+ const resultsDir = path.join(root, "results");
229
+ fs.mkdirSync(resultsDir, { recursive: true });
230
+ writeStatus(asyncDir, {
231
+ runId: "run-result",
232
+ mode: "single",
233
+ state: "running",
234
+ pid: 12345,
235
+ startedAt: 1000,
236
+ lastUpdate: 1000,
237
+ steps: [{ agent: "worker", status: "running", startedAt: 1000 }],
238
+ });
239
+ const resultPath = path.join(resultsDir, "run-result.json");
240
+ fs.writeFileSync(resultPath, JSON.stringify({ id: "run-result", success: true, state: "complete", summary: "already done" }, null, 2), "utf-8");
241
+
242
+ const result = reconcileAsyncRun(asyncDir, {
243
+ resultsDir,
244
+ kill: () => { throw errno("ESRCH"); },
245
+ now: () => 2000,
246
+ });
247
+
248
+ assert.equal(result.repaired, true);
249
+ assert.equal(result.status?.state, "complete");
250
+ assert.equal(JSON.parse(fs.readFileSync(resultPath, "utf-8")).summary, "already done");
251
+ } finally {
252
+ fs.rmSync(root, { recursive: true, force: true });
253
+ }
254
+ });
255
+ });
@@ -0,0 +1,19 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { aggregateStepStatus, formatActivityLabel, formatParallelOutcome } from "../../src/shared/status-format.ts";
4
+ import type { AsyncJobStep } from "../../src/shared/types.ts";
5
+
6
+ describe("status format helpers", () => {
7
+ it("formats activity labels consistently", () => {
8
+ assert.equal(formatActivityLabel(1_000, undefined, 1_500), "active now");
9
+ assert.equal(formatActivityLabel(1_000, "needs_attention", 4_000), "no activity for 3s");
10
+ assert.equal(formatActivityLabel(undefined, "active_long_running", 4_000), "active but long-running");
11
+ });
12
+
13
+ it("aggregates step status and parallel outcomes", () => {
14
+ const steps = [{ status: "complete" }, { status: "running" }, { status: "failed" }] satisfies Array<Pick<AsyncJobStep, "status">>;
15
+ assert.equal(aggregateStepStatus(steps), "running");
16
+ assert.equal(formatParallelOutcome(steps, 3), "1 agent running · 1/3 done · 1 failed");
17
+ assert.equal(formatParallelOutcome(steps, 3, { showRunning: false }), "1/3 done · 1 failed");
18
+ });
19
+ });
@@ -0,0 +1,245 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ buildControlEvent,
5
+ claimControlNotification,
6
+ controlNotificationKey,
7
+ deriveActivityState,
8
+ formatControlIntercomMessage,
9
+ formatControlNoticeMessage,
10
+ resolveControlConfig,
11
+ shouldNotifyControlEvent,
12
+ } from "../../src/runs/shared/subagent-control.ts";
13
+ import { nextLongRunningTrigger } from "../../src/runs/shared/long-running-guard.ts";
14
+
15
+ const config = resolveControlConfig(undefined, {
16
+ needsAttentionAfterMs: 300,
17
+ });
18
+
19
+ describe("subagent control attention state", () => {
20
+ it("marks a run as needing attention only after the idle threshold", () => {
21
+ assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, now: 50 }), undefined);
22
+ assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, now: 400 }), "needs_attention");
23
+ assert.equal(deriveActivityState({ config, startedAt: 0, now: 400 }), "needs_attention");
24
+ });
25
+
26
+
27
+ it("builds compact needs-attention control events", () => {
28
+ const event = buildControlEvent({
29
+ to: "needs_attention",
30
+ runId: "run-1",
31
+ agent: "worker",
32
+ index: 2,
33
+ ts: 1_000,
34
+ lastActivityAt: 100,
35
+ });
36
+ assert.deepEqual(event, {
37
+ type: "needs_attention",
38
+ to: "needs_attention",
39
+ ts: 1_000,
40
+ runId: "run-1",
41
+ agent: "worker",
42
+ index: 2,
43
+ message: "worker needs attention (no observed activity for 0s)",
44
+ reason: "idle",
45
+ elapsedMs: 900,
46
+ });
47
+ });
48
+
49
+ it("supports a specific attention message", () => {
50
+ const event = buildControlEvent({
51
+ to: "needs_attention",
52
+ runId: "run-1",
53
+ agent: "worker",
54
+ message: "worker completed without making edits for an implementation task",
55
+ });
56
+
57
+ assert.equal(event.message, "worker completed without making edits for an implementation task");
58
+ });
59
+
60
+ it("builds terminal completion guard control events", () => {
61
+ const event = buildControlEvent({
62
+ to: "needs_attention",
63
+ runId: "run-1",
64
+ agent: "worker",
65
+ message: "worker completed without making edits for an implementation task",
66
+ reason: "completion_guard",
67
+ });
68
+
69
+ assert.equal(event.reason, "completion_guard");
70
+ });
71
+
72
+ it("defaults notifications to active-long-running and needs attention", () => {
73
+ const event = buildControlEvent({ to: "needs_attention", runId: "run-1", agent: "worker" });
74
+ const activeEvent = buildControlEvent({ type: "active_long_running", to: "active_long_running", runId: "run-1", agent: "worker" });
75
+ assert.equal(shouldNotifyControlEvent(config, event), true);
76
+ assert.equal(shouldNotifyControlEvent(config, activeEvent), true);
77
+ assert.deepEqual(config.notifyOn, ["active_long_running", "needs_attention"]);
78
+ assert.deepEqual(config.notifyChannels, ["event", "async", "intercom"]);
79
+ });
80
+
81
+ it("defaults active-long-running notices to elapsed time only", () => {
82
+ const defaults = resolveControlConfig();
83
+
84
+ assert.equal(defaults.activeNoticeAfterMs, 240_000);
85
+ assert.equal(defaults.activeNoticeAfterTurns, undefined);
86
+ assert.equal(defaults.activeNoticeAfterTokens, undefined);
87
+ assert.equal(nextLongRunningTrigger(defaults, {
88
+ startedAt: 0,
89
+ now: 77_000,
90
+ turns: 50,
91
+ tokens: 800_000,
92
+ }), undefined);
93
+ assert.equal(nextLongRunningTrigger(defaults, {
94
+ startedAt: 0,
95
+ now: 240_000,
96
+ turns: 1,
97
+ tokens: 1,
98
+ }), "time_threshold");
99
+ });
100
+
101
+ it("supports opt-in turn and token long-running thresholds", () => {
102
+ const tokenBudget = resolveControlConfig(undefined, { activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 500_000 });
103
+ const turnBudget = resolveControlConfig(undefined, { activeNoticeAfterMs: 999_999, activeNoticeAfterTurns: 5 });
104
+
105
+ assert.equal(nextLongRunningTrigger(tokenBudget, {
106
+ startedAt: 0,
107
+ now: 77_000,
108
+ turns: 1,
109
+ tokens: 500_000,
110
+ }), "token_threshold");
111
+ assert.equal(nextLongRunningTrigger(turnBudget, {
112
+ startedAt: 0,
113
+ now: 77_000,
114
+ turns: 5,
115
+ tokens: 1,
116
+ }), "turn_threshold");
117
+ });
118
+
119
+ it("resolves custom notification config", () => {
120
+ const custom = resolveControlConfig(undefined, {
121
+ needsAttentionAfterMs: 1234,
122
+ activeNoticeAfterMs: 2345,
123
+ activeNoticeAfterTurns: 7,
124
+ activeNoticeAfterTokens: 8000,
125
+ failedToolAttemptsBeforeAttention: 4,
126
+ notifyOn: ["active_long_running", "needs_attention", "nope" as never],
127
+ notifyChannels: ["event", "intercom", "bad" as never],
128
+ });
129
+ assert.equal(custom.needsAttentionAfterMs, 1234);
130
+ assert.equal(custom.activeNoticeAfterMs, 2345);
131
+ assert.equal(custom.activeNoticeAfterTurns, 7);
132
+ assert.equal(custom.activeNoticeAfterTokens, 8000);
133
+ assert.equal(custom.failedToolAttemptsBeforeAttention, 4);
134
+ assert.deepEqual(custom.notifyOn, ["active_long_running", "needs_attention"]);
135
+ assert.deepEqual(custom.notifyChannels, ["event", "intercom"]);
136
+ });
137
+
138
+ it("falls back to defaults for invalid non-empty notification arrays", () => {
139
+ const custom = resolveControlConfig(undefined, {
140
+ notifyOn: ["bogus" as never],
141
+ notifyChannels: ["bogus" as never],
142
+ });
143
+ assert.deepEqual(custom.notifyOn, ["active_long_running", "needs_attention"]);
144
+ assert.deepEqual(custom.notifyChannels, ["event", "async", "intercom"]);
145
+ });
146
+
147
+ it("allows empty notification arrays to disable notifications", () => {
148
+ const custom = resolveControlConfig(undefined, {
149
+ notifyOn: [],
150
+ notifyChannels: [],
151
+ });
152
+ const event = buildControlEvent({ to: "needs_attention", runId: "run-1", agent: "worker" });
153
+ assert.deepEqual(custom.notifyOn, []);
154
+ assert.deepEqual(custom.notifyChannels, []);
155
+ assert.equal(shouldNotifyControlEvent(custom, event), false);
156
+ });
157
+
158
+ it("formats control notices with a proactive hint and concrete commands", () => {
159
+ const event = buildControlEvent({ to: "needs_attention", runId: "78f659a3", agent: "worker" });
160
+
161
+ const message = formatControlNoticeMessage(event, "subagent-worker-78f659a3");
162
+
163
+ assert.match(message, /Subagent needs attention: worker/);
164
+ assert.match(message, /Hint: Inspect status first unless the run is clearly blocked/);
165
+ assert.match(message, /Live async nudges interrupt the child before sending the follow-up/);
166
+ assert.match(message, /Nudge: subagent\(\{ action: "resume", id: "78f659a3", message: "What are you blocked on\?/);
167
+ assert.match(message, /Direct intercom target: subagent-worker-78f659a3/);
168
+ assert.match(message, /Status: subagent\(\{ action: "status", id: "78f659a3" \}\)/);
169
+ assert.match(message, /Interrupt: subagent\(\{ action: "interrupt", id: "78f659a3" \}\)/);
170
+ assert.doesNotMatch(message, /Wait:/);
171
+ });
172
+
173
+ it("formats active-long-running notices as informational", () => {
174
+ const event = buildControlEvent({
175
+ type: "active_long_running",
176
+ to: "active_long_running",
177
+ runId: "78f659a3",
178
+ agent: "worker",
179
+ turns: 15,
180
+ tokens: 160000,
181
+ toolCount: 42,
182
+ currentTool: "edit",
183
+ currentPath: "src/runs/background/async-status.ts",
184
+ reason: "turn_threshold",
185
+ });
186
+
187
+ const message = formatControlNoticeMessage(event, "subagent-worker-78f659a3-1");
188
+
189
+ assert.match(message, /Subagent active but long-running: worker/);
190
+ assert.match(message, /Inspect status/);
191
+ assert.match(message, /Nudge: subagent\(\{ action: "resume", id: "78f659a3", message: "What are you blocked on\?/);
192
+ assert.match(message, /15 turns/);
193
+ assert.match(message, /160000 tokens/);
194
+ assert.match(message, /path src\/runs\/background\/async-status\.ts/);
195
+ assert.doesNotMatch(message, /Subagent needs attention/);
196
+ });
197
+
198
+ it("formats terminal completion guard notices without live-run commands", () => {
199
+ const event = buildControlEvent({
200
+ to: "needs_attention",
201
+ runId: "78f659a3",
202
+ agent: "worker",
203
+ index: 0,
204
+ message: "worker completed without making edits for an implementation task",
205
+ reason: "completion_guard",
206
+ });
207
+
208
+ const message = formatControlNoticeMessage(event, "subagent-worker-78f659a3-1");
209
+
210
+ assert.match(message, /Subagent failed: worker/);
211
+ assert.match(message, /read the output artifact or session/);
212
+ assert.match(message, /Run intercom target \(may be inactive\): subagent-worker-78f659a3-1/);
213
+ assert.doesNotMatch(message, /Status:/);
214
+ assert.doesNotMatch(message, /Interrupt:/);
215
+ assert.doesNotMatch(message, /What are you blocked on/);
216
+ });
217
+
218
+ it("formats intercom notifications with the same control commands", () => {
219
+ const event = buildControlEvent({ to: "needs_attention", runId: "78f659a3", agent: "worker" });
220
+
221
+ const message = formatControlIntercomMessage(event, "subagent-worker-78f659a3");
222
+
223
+ assert.match(message, /worker needs attention in run 78f659a3/);
224
+ assert.match(message, /Nudge: subagent\(\{ action: "resume", id: "78f659a3", message: "What are you blocked on\?/);
225
+ });
226
+
227
+ it("dedupes notifications once per child target and attention state", () => {
228
+ const event = buildControlEvent({ to: "needs_attention", runId: "run-1", agent: "worker", index: 0 });
229
+ const seen = new Set<string>();
230
+
231
+ assert.equal(controlNotificationKey(event, "subagent-worker-run-1-1"), "subagent-worker-run-1-1:needs_attention:idle");
232
+ assert.equal(claimControlNotification(resolveControlConfig(), event, seen, "subagent-worker-run-1-1"), true);
233
+ assert.equal(claimControlNotification(resolveControlConfig(), event, seen, "subagent-worker-run-1-1"), false);
234
+
235
+ const terminalEvent = buildControlEvent({
236
+ to: "needs_attention",
237
+ runId: "run-1",
238
+ agent: "worker",
239
+ index: 0,
240
+ message: "worker completed without making edits for an implementation task",
241
+ reason: "completion_guard",
242
+ });
243
+ assert.equal(claimControlNotification(resolveControlConfig(), terminalEvent, seen, "subagent-worker-run-1-1"), true);
244
+ });
245
+ });