@selesai/code 0.8.6 → 0.8.7

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 (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/settings.json +1 -0
  16. package/dist/extensions/model-prompt-injector/config.json +10 -0
  17. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  18. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  19. package/dist/extensions/package.json +1 -0
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  21. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  22. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  23. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  24. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  25. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  26. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  27. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  28. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  29. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  30. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  31. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  32. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  33. package/dist/extensions/pi-subagents/install.mjs +2 -2
  34. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  35. package/dist/extensions/pi-subagents/package.json +1 -1
  36. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  37. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  38. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  39. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  40. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  41. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  42. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  43. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  45. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  46. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  47. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  48. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  49. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  50. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  51. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  52. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  53. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  54. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  55. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  56. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  57. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  58. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  59. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  60. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  61. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  62. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  64. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  66. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  67. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  70. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
  71. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  72. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  73. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  74. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  75. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  76. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  80. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  81. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  83. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  84. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  85. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  86. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  88. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  89. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  90. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  96. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  97. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  98. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  99. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  100. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  101. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  102. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  104. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  105. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  106. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  107. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  108. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  109. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  110. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  111. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  112. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  113. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  114. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  115. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  116. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  117. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  118. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  119. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  120. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  121. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  122. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  123. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  124. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  125. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  126. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  127. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  128. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  129. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  130. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  131. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  133. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  134. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  135. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  136. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  137. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  138. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  139. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  140. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  141. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  142. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  143. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  144. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  145. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  146. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  147. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  148. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  149. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  150. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  151. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  152. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  153. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  154. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  155. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  156. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  157. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  158. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  159. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  160. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  161. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  162. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  163. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  164. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  165. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  166. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  167. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  168. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  169. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  170. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  171. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  172. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  173. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  174. package/dist/extensions/question/index.ts +21 -6
  175. package/dist/extensions/question/question-list.ts +7 -1
  176. package/dist/extensions/question/row-layout.ts +3 -0
  177. package/dist/extensions/question/tests/exp.test.ts +70 -0
  178. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  179. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  180. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  181. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  182. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  183. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  184. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  185. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  186. package/dist/skills/pi-subagents/SKILL.md +1 -1
  187. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  188. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  189. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  190. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  191. package/docs/settings.md +6 -0
  192. package/package.json +3 -3
@@ -33,6 +33,7 @@ describe("async run status inspection", () => {
33
33
  fs.writeFileSync(sessionFile, "", "utf-8");
34
34
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
35
35
  runId: "run-stale",
36
+ sessionId: "session-current",
36
37
  mode: "single",
37
38
  state: "running",
38
39
  pid: 12345,
@@ -206,6 +207,41 @@ describe("async run status inspection", () => {
206
207
  }
207
208
  });
208
209
 
210
+ it("does not advertise an output artifact that was never written", () => {
211
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-phantom-"));
212
+ try {
213
+ const asyncRoot = path.join(root, "runs");
214
+ const asyncDir = path.join(asyncRoot, "run-phantom-output");
215
+ fs.mkdirSync(asyncDir, { recursive: true });
216
+ // A workflow run orchestrates children elsewhere and never writes output-<index>.log itself.
217
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
218
+ runId: "run-phantom-output",
219
+ mode: "workflow",
220
+ state: "running",
221
+ startedAt: 100,
222
+ lastUpdate: 200,
223
+ currentStep: 0,
224
+ steps: [{ agent: "delegate", status: "running", startedAt: 100, recentOutput: ["CHILD_RECENT"] }],
225
+ }, null, 2), "utf-8");
226
+
227
+ const result = inspectSubagentStatus({ id: "run-phantom-output", view: "transcript" }, {
228
+ asyncDirRoot: asyncRoot,
229
+ resultsDir: path.join(root, "results"),
230
+ kill: () => true,
231
+ now: () => 250,
232
+ });
233
+
234
+ const text = textContent(result);
235
+ assert.equal(result.isError, undefined);
236
+ assert.doesNotMatch(text, /Output: .*output-0\.log/);
237
+ // The status.json fallback still supplies the transcript body.
238
+ assert.match(text, /Recent output from status\.json:/);
239
+ assert.match(text, /CHILD_RECENT/);
240
+ } finally {
241
+ fs.rmSync(root, { recursive: true, force: true });
242
+ }
243
+ });
244
+
209
245
  it("refuses to tail status outputFile paths outside the async directory", () => {
210
246
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-escape-"));
211
247
  try {
@@ -238,6 +274,69 @@ describe("async run status inspection", () => {
238
274
  }
239
275
  });
240
276
 
277
+ it("escapes terminal control sequences in async output transcripts", () => {
278
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-unsafe-"));
279
+ try {
280
+ const asyncRoot = path.join(root, "runs");
281
+ const asyncDir = path.join(asyncRoot, "run-unsafe-output");
282
+ fs.mkdirSync(asyncDir, { recursive: true });
283
+ fs.writeFileSync(path.join(asyncDir, "output-0.log"), "safe \u001b]8;;https://attacker.invalid\u0007link\u001b]8;;\u0007 bidi \u202e", "utf-8");
284
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
285
+ runId: "run-unsafe-output",
286
+ mode: "single",
287
+ state: "complete",
288
+ startedAt: 100,
289
+ lastUpdate: 200,
290
+ steps: [{ agent: "worker", status: "complete" }],
291
+ }, null, 2), "utf-8");
292
+
293
+ const result = inspectSubagentStatus({ id: "run-unsafe-output", view: "transcript" }, {
294
+ asyncDirRoot: asyncRoot,
295
+ resultsDir: path.join(root, "results"),
296
+ });
297
+
298
+ const text = textContent(result);
299
+ assert.doesNotMatch(text, /[\u001b\u0007\u202e]/u);
300
+ assert.match(text, /U\+001B/);
301
+ assert.match(text, /U\+202E/);
302
+ } finally {
303
+ fs.rmSync(root, { recursive: true, force: true });
304
+ }
305
+ });
306
+
307
+ it("keeps run metadata when child output contains binary content", () => {
308
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-binary-"));
309
+ try {
310
+ const asyncRoot = path.join(root, "runs");
311
+ const asyncDir = path.join(asyncRoot, "run-binary-output");
312
+ fs.mkdirSync(asyncDir, { recursive: true });
313
+ fs.writeFileSync(path.join(asyncDir, "output-0.log"), "useful line A\nuseful line B\n\u0000\n", "utf-8");
314
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
315
+ runId: "run-binary-output",
316
+ mode: "single",
317
+ state: "complete",
318
+ startedAt: 100,
319
+ lastUpdate: 200,
320
+ steps: [{ agent: "worker", status: "complete" }],
321
+ }, null, 2), "utf-8");
322
+
323
+ const result = inspectSubagentStatus({ id: "run-binary-output", view: "transcript" }, {
324
+ asyncDirRoot: asyncRoot,
325
+ resultsDir: path.join(root, "results"),
326
+ });
327
+
328
+ // One malformed line must not erase run state, artifacts, or the safe lines.
329
+ const text = textContent(result);
330
+ assert.match(text, /Run: run-binary-output/);
331
+ assert.match(text, /State: complete/);
332
+ assert.match(text, /useful line A/);
333
+ assert.match(text, /useful line B/);
334
+ assert.match(text, /\[binary content omitted for safe display\]/);
335
+ } finally {
336
+ fs.rmSync(root, { recursive: true, force: true });
337
+ }
338
+ });
339
+
241
340
  it("refuses symlink session transcript paths even under trusted roots", () => {
242
341
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-session-"));
243
342
  try {
@@ -382,6 +481,40 @@ describe("async run status inspection", () => {
382
481
  }
383
482
  });
384
483
 
484
+ it("escapes terminal control sequences in remembered foreground transcripts", () => {
485
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-foreground-transcript-unsafe-"));
486
+ try {
487
+ const state = {
488
+ currentSessionId: "session-current",
489
+ asyncJobs: new Map(),
490
+ foregroundControls: new Map(),
491
+ foregroundRuns: new Map([["foreground-unsafe", {
492
+ runId: "foreground-unsafe",
493
+ mode: "single",
494
+ cwd: root,
495
+ sessionId: "session-current",
496
+ updatedAt: 100,
497
+ children: [{ agent: "worker", index: 0, status: "completed", finalOutput: "safe \u001b]8;;https://attacker.invalid\u0007link\u001b]8;;\u0007 bidi \u202e" }],
498
+ }]]),
499
+ } as unknown as SubagentState;
500
+
501
+ const result = inspectSubagentStatus({ id: "foreground-unsafe", view: "transcript" }, {
502
+ asyncDirRoot: path.join(root, "runs"),
503
+ resultsDir: path.join(root, "results"),
504
+ state,
505
+ });
506
+
507
+ const text = textContent(result);
508
+ assert.equal(result.isError, undefined);
509
+ assert.doesNotMatch(text, /[\u001b\u0007\u202e]/u);
510
+ assert.match(text, /U\+001B/);
511
+ assert.match(text, /U\+0007/);
512
+ assert.match(text, /U\+202E/);
513
+ } finally {
514
+ fs.rmSync(root, { recursive: true, force: true });
515
+ }
516
+ });
517
+
385
518
  it("refuses transcript reads for async runs owned by another session", () => {
386
519
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-session-scope-"));
387
520
  try {
@@ -571,6 +704,7 @@ describe("async run status inspection", () => {
571
704
  }, null, 2), "utf-8");
572
705
  fs.writeFileSync(path.join(nestedAsyncDir, "status.json"), JSON.stringify({
573
706
  runId: "nested-stale",
707
+ sessionId: "session-nested",
574
708
  mode: "single",
575
709
  state: "running",
576
710
  pid: 54321,
@@ -675,6 +809,48 @@ describe("async run status inspection", () => {
675
809
  }
676
810
  });
677
811
 
812
+ it("keeps safe nested session content parts around binary content", () => {
813
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-nested-session-binary-"));
814
+ const route = createNestedRoute("run-nested-session-binary-root");
815
+ try {
816
+ const sessionFile = path.join(root, "session.jsonl");
817
+ fs.writeFileSync(sessionFile, `${JSON.stringify({ message: { role: "assistant", content: [{ type: "text", text: "safe before" }, { type: "text", text: "\0" }, { type: "text", text: "safe after" }] } })}\n`, "utf-8");
818
+ writeNestedEvent(route, {
819
+ type: "subagent.nested.updated",
820
+ ts: 150,
821
+ parentRunId: "run-nested-session-binary-root",
822
+ parentStepIndex: 0,
823
+ child: {
824
+ id: "nested-session-binary-child",
825
+ parentRunId: "run-nested-session-binary-root",
826
+ parentStepIndex: 0,
827
+ depth: 1,
828
+ path: [{ runId: "run-nested-session-binary-root", stepIndex: 0, agent: "orchestrator" }],
829
+ state: "complete",
830
+ mode: "single",
831
+ agent: "worker",
832
+ sessionFile,
833
+ lastUpdate: 150,
834
+ },
835
+ });
836
+
837
+ const result = inspectSubagentStatus({ id: "nested-session-binary-child", view: "transcript" }, {
838
+ asyncDirRoot: path.join(root, "runs"),
839
+ resultsDir: path.join(root, "results"),
840
+ sessionRoots: [root],
841
+ });
842
+
843
+ const text = textContent(result);
844
+ assert.equal(result.isError, undefined);
845
+ assert.match(text, /safe before/);
846
+ assert.match(text, /\[binary content omitted for safe display\]/);
847
+ assert.match(text, /safe after/);
848
+ } finally {
849
+ fs.rmSync(root, { recursive: true, force: true });
850
+ fs.rmSync(path.dirname(route.eventSink), { recursive: true, force: true });
851
+ }
852
+ });
853
+
678
854
  it("resolves exact nested run ids from the nested registry", () => {
679
855
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-nested-exact-"));
680
856
  const route = createNestedRoute("run-nested-exact-root");
@@ -753,6 +929,77 @@ describe("async run status inspection", () => {
753
929
  }
754
930
  });
755
931
 
932
+ it("shows direct run-id recovery for workflow children", () => {
933
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-workflow-resume-"));
934
+ try {
935
+ const asyncRoot = path.join(root, "runs");
936
+ const asyncDir = path.join(asyncRoot, "workflow-parent");
937
+ const firstSession = path.join(root, "review.jsonl");
938
+ const secondSession = path.join(root, "writer.jsonl");
939
+ fs.mkdirSync(asyncDir, { recursive: true });
940
+ fs.writeFileSync(firstSession, "", "utf-8");
941
+ fs.writeFileSync(secondSession, "", "utf-8");
942
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
943
+ runId: "workflow-parent",
944
+ mode: "workflow",
945
+ state: "failed",
946
+ startedAt: 100,
947
+ lastUpdate: 200,
948
+ steps: [
949
+ { agent: "reviewer", workflowKey: "review", runId: "child-review", status: "failed", sessionFile: firstSession },
950
+ { agent: "worker", workflowKey: "write", runId: "child-write", status: "paused", sessionFile: secondSession },
951
+ ],
952
+ }, null, 2), "utf-8");
953
+
954
+ const result = inspectSubagentStatus({ id: "workflow-parent" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") });
955
+ const text = textContent(result);
956
+ assert.match(text, /Revive workflow child 'review': subagent\(\{ action: "resume", id: "child-review", message: "\.\.\." \}\)/);
957
+ assert.match(text, /Revive workflow child 'write': subagent\(\{ action: "resume", id: "child-write", message: "\.\.\." \}\)/);
958
+ assert.doesNotMatch(text, /id: "workflow-parent", index:/);
959
+ } finally {
960
+ fs.rmSync(root, { recursive: true, force: true });
961
+ }
962
+ });
963
+
964
+ it("keeps supervisor-detached workflow children out of generic revive guidance", () => {
965
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-workflow-detached-"));
966
+ try {
967
+ const asyncRoot = path.join(root, "runs");
968
+ const asyncDir = path.join(asyncRoot, "workflow-detached");
969
+ const sessionFile = path.join(root, "detached.jsonl");
970
+ fs.mkdirSync(asyncDir, { recursive: true });
971
+ fs.writeFileSync(sessionFile, "", "utf-8");
972
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
973
+ runId: "workflow-detached",
974
+ mode: "workflow",
975
+ state: "paused",
976
+ activityState: "needs_attention",
977
+ error: "Run 'detaches' detached for intercom coordination. Reply to the supervisor request first, then wait with subagent_wait({ id: \"child-detached\" }). Use subagent({ action: \"status\", id: \"child-detached\" }) to recover the result; do not resume or launch a replacement while it remains detached.",
978
+ startedAt: 100,
979
+ lastUpdate: 200,
980
+ steps: [{
981
+ agent: "worker",
982
+ workflowKey: "detaches",
983
+ runId: "child-detached",
984
+ status: "paused",
985
+ activityState: "needs_attention",
986
+ sessionFile,
987
+ }],
988
+ }, null, 2), "utf-8");
989
+
990
+ const result = inspectSubagentStatus({ id: "workflow-detached" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") });
991
+ const text = textContent(result);
992
+ assert.match(text, /Reply to the supervisor request first/);
993
+ assert.match(text, /wait with subagent_wait\(\{ id: "child-detached" \}\)/);
994
+ assert.match(text, /do not resume or launch a replacement while it remains detached/);
995
+ assert.match(text, /Recovery workflow child 'detaches'/);
996
+ assert.doesNotMatch(text, /Revive workflow child 'detaches'/);
997
+ assert.doesNotMatch(text, /action: "resume", id: "child-detached"/);
998
+ } finally {
999
+ fs.rmSync(root, { recursive: true, force: true });
1000
+ }
1001
+ });
1002
+
756
1003
  it("uses original child indexes when result metadata contains invalid children", () => {
757
1004
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-original-index-"));
758
1005
  try {
@@ -66,7 +66,7 @@ function context(cwd: string, sessionId = "session-a"): ExtensionContext {
66
66
  } as unknown as ExtensionContext;
67
67
  }
68
68
 
69
- function harness(options: { cwd?: string; sessionId?: string; now?: number; config?: ExtensionConfig } = {}): Harness {
69
+ function harness(options: { cwd?: string; sessionId?: string; now?: number; config?: ExtensionConfig; randomId?: () => string } = {}): Harness {
70
70
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-schedule-test-"));
71
71
  roots.push(root);
72
72
  const project = options.cwd ?? path.join(root, "project");
@@ -80,7 +80,7 @@ function harness(options: { cwd?: string; sessionId?: string; now?: number; conf
80
80
  config: options.config ?? { scheduledRuns: { enabled: true } },
81
81
  storeRoot: path.join(root, "stores"),
82
82
  now: () => clock.now,
83
- randomId: () => `id-${++id}`,
83
+ randomId: options.randomId ?? (() => `id-${++id}`),
84
84
  timers,
85
85
  launch: (params, launchCtx) => new Promise((resolve) => launches.push({ params: params as Record<string, unknown>, ctx: launchCtx, resolve: resolve as Launch["resolve"] })) as never,
86
86
  });
@@ -237,6 +237,41 @@ describe("project schedule management", () => {
237
237
  assert.match(text(await h.manager.handleToolCall({ action: "schedule.list" }, h.ctx)), /No project schedules/);
238
238
  });
239
239
 
240
+ it("ignores schedules deleted by another session while a timer is still armed", async () => {
241
+ const first = harness();
242
+ await first.manager.handleToolCall({ action: "schedule.create", id: "stale", every: "1h", workflowScript: "return runs.run('main', { agent: 'worker' })" }, first.ctx);
243
+ const secondTimers = new FakeTimers();
244
+ const second = createScheduledRunManager({
245
+ config: { scheduledRuns: { enabled: true } },
246
+ storeRoot: path.join(first.root, "stores"),
247
+ now: () => first.clock.now,
248
+ timers: secondTimers,
249
+ launch: async () => ({ content: [{ type: "text", text: "unused" }], details: { mode: "management", results: [] } }),
250
+ });
251
+ const secondCtx = context(first.ctx.cwd, "session-b");
252
+ second.bindSession(secondCtx);
253
+ assert.equal(first.timers.values.size, 1);
254
+ assert.equal(secondTimers.values.size, 1);
255
+
256
+ assert.match(text(await second.handleToolCall({ action: "schedule.delete", id: "stale" }, secondCtx)), /Deleted/);
257
+ assert.equal(secondTimers.values.size, 0);
258
+ assert.equal(first.timers.values.size, 1);
259
+ first.clock.now += 3_600_000;
260
+ const warnings: string[] = [];
261
+ const originalWarn = console.warn;
262
+ console.warn = (message?: unknown) => void warnings.push(String(message));
263
+ try {
264
+ first.timers.fireAll();
265
+ await flush();
266
+ } finally {
267
+ console.warn = originalWarn;
268
+ }
269
+ assert.deepEqual(warnings, []);
270
+ assert.equal(first.launches.length, 0);
271
+ assert.equal(first.timers.values.size, 0);
272
+ assert.match(text(await first.manager.handleToolCall({ action: "schedule.list" }, first.ctx)), /No project schedules/);
273
+ });
274
+
240
275
  it("reports corrupt project schedule records instead of dropping them", async () => {
241
276
  const h = harness();
242
277
  const root = scheduledRunStorePath(h.ctx.cwd, undefined, path.join(h.root, "stores"));
@@ -294,6 +329,7 @@ describe("recurring schedule execution", () => {
294
329
  assert.deepEqual(h.launches[0]?.params, { workflowScript: "return runs.run('main', { agent: 'worker', task: 'Maintain backlog' })", async: true, context: "fresh", cwd: h.ctx.cwd, mission: false });
295
330
  h.launches[0]!.resolve({ content: [{ type: "text", text: "Async worker" }], details: { mode: "single", results: [], asyncId: "async-1", asyncDir: "/tmp/async-1" } });
296
331
  await flush();
332
+ assert.deepEqual([...h.manager.observedCompletionRunIds()], ["async-1"]);
297
333
 
298
334
  let history = await h.manager.handleToolCall({ action: "schedule.history", id: "hourly" }, h.ctx);
299
335
  assert.match(text(history), /running.*async async-1/);
@@ -304,6 +340,7 @@ describe("recurring schedule execution", () => {
304
340
  assert.equal(fs.readdirSync(path.join(dir, "runs")).length, 1);
305
341
 
306
342
  h.manager.handleAsyncCompletion({ runId: "async-1", success: true, summary: "Done" });
343
+ assert.deepEqual([...h.manager.observedCompletionRunIds()], []);
307
344
  history = await h.manager.handleToolCall({ action: "schedule.history", id: "hourly" }, h.ctx);
308
345
  assert.match(text(history), /completed.*async async-1/);
309
346
  assert.equal(fs.existsSync(path.join(dir, "active.lock")), false);
@@ -311,6 +348,74 @@ describe("recurring schedule execution", () => {
311
348
  assert.match(text(shown), /2030-01-01T02:00:00.000Z/, "next occurrence advances from the planned time without completion drift");
312
349
  });
313
350
 
351
+ it("delays retrying an overdue recurring schedule after an unexpected timer failure", async () => {
352
+ const h = harness({ randomId: () => { throw new Error("persistent id failure"); } });
353
+ await h.manager.handleToolCall({ action: "schedule.create", id: "hourly", every: "1h", workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
354
+ h.clock.now += 3_600_000;
355
+ const warnings: string[] = [];
356
+ const originalWarn = console.warn;
357
+ console.warn = (message?: unknown) => void warnings.push(String(message));
358
+ try {
359
+ h.timers.fireAll();
360
+ await flush();
361
+ } finally {
362
+ console.warn = originalWarn;
363
+ }
364
+
365
+ assert.equal(h.launches.length, 0);
366
+ assert.equal(h.timers.values.size, 1);
367
+ assert.equal([...h.timers.values.values()][0]?.delay, 3_600_000);
368
+ assert.match(warnings[0] ?? "", /failed to fire: persistent id failure/);
369
+ const shown = await h.manager.handleToolCall({ action: "schedule.show", id: "hourly" }, h.ctx);
370
+ assert.match(text(shown), /State: scheduled/);
371
+ assert.match(text(shown), /2030-01-01T01:00:00.000Z/);
372
+ });
373
+
374
+ it("re-arms overdue catch-up-none schedules when missed-run recovery fails", async () => {
375
+ const h = harness({ randomId: () => { throw new Error("persistent id failure"); } });
376
+ await h.manager.handleToolCall({ action: "schedule.create", id: "none", every: "1h", catchUp: "none", workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
377
+ h.clock.now += 2 * 3_600_000;
378
+ const warnings: string[] = [];
379
+ const originalWarn = console.warn;
380
+ console.warn = (message?: unknown) => void warnings.push(String(message));
381
+ try {
382
+ h.timers.fireAll();
383
+ await flush();
384
+ } finally {
385
+ console.warn = originalWarn;
386
+ }
387
+
388
+ assert.equal(h.launches.length, 0);
389
+ assert.equal(h.timers.values.size, 1);
390
+ assert.equal([...h.timers.values.values()][0]?.delay, 3_600_000);
391
+ assert.match(warnings[0] ?? "", /failed to fire: persistent id failure/);
392
+ assert.match(warnings[1] ?? "", /could not be restored after fire failure: persistent id failure/);
393
+ const shown = await h.manager.handleToolCall({ action: "schedule.show", id: "none" }, h.ctx);
394
+ assert.match(text(shown), /2030-01-01T01:00:00.000Z/);
395
+ });
396
+
397
+ it("does not immediately retry an overdue one-shot after an unexpected timer failure", async () => {
398
+ const h = harness({ randomId: () => { throw new Error("persistent id failure"); } });
399
+ await h.manager.handleToolCall({ action: "schedule.create", id: "once", at: "+1h", workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
400
+ h.clock.now += 3_600_000;
401
+ const warnings: string[] = [];
402
+ const originalWarn = console.warn;
403
+ console.warn = (message?: unknown) => void warnings.push(String(message));
404
+ try {
405
+ h.timers.fireAll();
406
+ await flush();
407
+ } finally {
408
+ console.warn = originalWarn;
409
+ }
410
+
411
+ assert.equal(h.launches.length, 0);
412
+ assert.equal(h.timers.values.size, 0);
413
+ assert.match(warnings[0] ?? "", /failed to fire: persistent id failure/);
414
+ const shown = await h.manager.handleToolCall({ action: "schedule.show", id: "once" }, h.ctx);
415
+ assert.match(text(shown), /State: scheduled/);
416
+ assert.match(text(shown), /2030-01-01T01:00:00.000Z/);
417
+ });
418
+
314
419
  it("run-due launches the latest missed occurrence while catchUp none records a miss", async () => {
315
420
  const latest = harness();
316
421
  await latest.manager.handleToolCall({ action: "schedule.create", id: "latest", every: "1h", catchUp: "latest", workflowScript: "return runs.run('main', { agent: 'worker' })" }, latest.ctx);
@@ -221,7 +221,7 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
221
221
  assert.equal(actionSchema.enum, undefined);
222
222
  const description = String(actionSchema.description ?? "");
223
223
  assert.match(description, /Optional management\/control action/);
224
- assert.match(description, /Omit this field for workflowScript execution/);
224
+ assert.match(description, /Omit this field for structured single-child or workflowScript execution/);
225
225
  assert.doesNotMatch(description, /\{agent, task\}/);
226
226
  assert.match(description, /use it only for management\/control actions/);
227
227
  assert.doesNotMatch(description, /orchestration\./);
@@ -236,14 +236,11 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
236
236
  assert.ok(maxRuntimeSchema, "maxRuntimeMs schema should exist");
237
237
  assert.equal(timeoutSchema.minimum, 1);
238
238
  assert.equal(maxRuntimeSchema.minimum, 1);
239
- assert.match(String(timeoutSchema.description ?? ""), /foreground and single async runs/i);
240
- assert.match(String(timeoutSchema.description ?? ""), /use config timeoutMs, else 30m/i);
241
- assert.match(String(timeoutSchema.description ?? ""), /async composites have no default parent deadline/i);
239
+ assert.match(String(timeoutSchema.description ?? ""), /foreground\/single async runs/i);
240
+ assert.match(String(timeoutSchema.description ?? ""), /config timeoutMs or 30m/i);
242
241
  assert.doesNotMatch(String(timeoutSchema.description ?? ""), /foreground-only/i);
243
242
  assert.match(String(maxRuntimeSchema.description ?? ""), /timeoutMs/i);
244
- assert.match(String(maxRuntimeSchema.description ?? ""), /foreground and single async runs/i);
245
- assert.match(String(maxRuntimeSchema.description ?? ""), /use config timeoutMs, else 30m/i);
246
- assert.match(String(maxRuntimeSchema.description ?? ""), /async composites have no default parent deadline/i);
243
+ assert.match(String(maxRuntimeSchema.description ?? ""), /alias of timeoutMs/i);
247
244
  assert.equal(turnBudgetSchema?.properties?.maxTurns?.minimum, 1);
248
245
  assert.equal(turnBudgetSchema?.properties?.graceTurns?.minimum, 0);
249
246
  assert.equal(toolBudgetSchema?.properties?.soft?.minimum, 1);
@@ -494,7 +491,7 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
494
491
  const reviewedRecoveryBranch = acceptanceStringBranches.find((branch) => Array.isArray(branch.enum) && branch.enum.includes("reviewed"));
495
492
  assert.deepEqual(reviewedRecoveryBranch?.enum, ["reviewed"]);
496
493
  assert.equal(reviewedRecoveryBranch?.deprecated, true);
497
- assert.match(String(acceptanceSchema.description ?? ""), /reviewer\/read-only calls, omit acceptance/i);
494
+ assert.match(String(acceptanceSchema.description ?? ""), /omit for read-only calls/i);
498
495
  assert.match(String(acceptanceSchema.description ?? ""), /acceptance\.review\.required/);
499
496
  const acceptanceObjectBranch = anyOfBranches(acceptanceSchema).find((branch) => branch.type === "object");
500
497
  assert.ok(acceptanceObjectBranch, "acceptance should support object config");