@selesai/code 0.8.5 → 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 (231) hide show
  1. package/CHANGELOG.md +28 -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/models.json +5 -0
  16. package/dist/defaults/settings.json +1 -0
  17. package/dist/extensions/agent-browser.test.ts +178 -2
  18. package/dist/extensions/agent-browser.ts +2 -0
  19. package/dist/extensions/copy-turn.test.ts +229 -0
  20. package/dist/extensions/copy-turn.ts +11 -0
  21. package/dist/extensions/grep-app/index.test.ts +356 -1
  22. package/dist/extensions/grep-app/index.ts +6 -1
  23. package/dist/extensions/handoff-new.test.ts +351 -161
  24. package/dist/extensions/handoff-new.ts +3 -0
  25. package/dist/extensions/inline-skills.test.ts +93 -0
  26. package/dist/extensions/inline-skills.ts +3 -0
  27. package/dist/extensions/model-prompt-injector/config.json +10 -0
  28. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  29. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  30. package/dist/extensions/package.json +1 -0
  31. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  32. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  33. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  34. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  35. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  36. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  37. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  38. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  39. package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
  40. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  41. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  42. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  43. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  44. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  45. package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
  46. package/dist/extensions/pi-subagents/install.mjs +2 -2
  47. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  48. package/dist/extensions/pi-subagents/package.json +1 -1
  49. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  50. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  51. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  53. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  54. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  55. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  56. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  57. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  58. package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
  59. package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
  60. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  61. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  62. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  63. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  64. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  65. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  66. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  67. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  68. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  69. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  70. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  71. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  72. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  73. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  74. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  75. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
  76. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  77. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  80. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  82. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  83. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
  85. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  86. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  87. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
  88. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
  89. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  90. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  91. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  92. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  93. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  94. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  95. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  96. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  97. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  98. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  99. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  100. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
  101. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  102. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  104. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  105. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  106. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  112. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  113. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  114. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  115. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  116. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  117. package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  120. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  121. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  122. package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
  123. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  124. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  125. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  126. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  127. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  128. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  129. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  133. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  134. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  135. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  136. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  137. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  138. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  139. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  140. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  141. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  142. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  143. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  144. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  145. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  146. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  148. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  149. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  152. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  153. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  154. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  155. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  156. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  157. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  158. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  159. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
  160. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  161. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  162. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  163. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  164. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  165. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  166. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  167. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  168. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  169. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  170. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  171. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  172. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  173. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  174. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  175. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  176. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  177. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  178. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  179. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  180. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  181. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  182. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  183. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  193. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  194. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  195. package/dist/extensions/ponytail/index.js +11 -0
  196. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  197. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  198. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  199. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  200. package/dist/extensions/question/index.ts +33 -0
  201. package/dist/extensions/question/question-list.ts +28 -0
  202. package/dist/extensions/question/row-layout.ts +3 -0
  203. package/dist/extensions/question/tests/batch.test.ts +103 -65
  204. package/dist/extensions/question/tests/exp.test.ts +70 -0
  205. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  206. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  207. package/dist/extensions/question/tests/helpers.test.ts +75 -35
  208. package/dist/extensions/question/tests/question-list.test.ts +640 -198
  209. package/dist/extensions/question/tests/row-layout.test.ts +193 -114
  210. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  211. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  212. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  213. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  214. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  215. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  216. package/dist/extensions/rtk.test.ts +180 -1
  217. package/dist/extensions/tokenin-onboarding.ts +3 -0
  218. package/dist/extensions/undo.test.ts +720 -0
  219. package/dist/extensions/undo.ts +6 -0
  220. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  221. package/dist/extensions/workflow/extension.ts +3 -3
  222. package/dist/extensions/workflow/modes.ts +41 -19
  223. package/dist/skills/pi-subagents/SKILL.md +1 -1
  224. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  225. package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
  226. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  227. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  228. package/docs/plans/workflow-autoloop-reference.md +1 -2
  229. package/docs/plans/workflow-handoff-carryover.md +108 -0
  230. package/docs/settings.md +6 -0
  231. package/package.json +4 -1
@@ -135,6 +135,20 @@ describe("scripted workflow runtime", () => {
135
135
  assert.deepEqual(emitSnapshots, [1]);
136
136
  });
137
137
 
138
+ it("passes a per-run intercom bridge override to the host launch", async () => {
139
+ let launchParams: Record<string, unknown> | undefined;
140
+ await runWorkflowScript({
141
+ script: `return runs.run("isolated", { agent: "worker", task: "Run", intercomBridge: { mode: "off" } });`,
142
+ async launch(key, params) {
143
+ launchParams = params;
144
+ return { key, ok: true, output: "done", artifactPaths: [] };
145
+ },
146
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
147
+ });
148
+
149
+ assert.deepEqual(launchParams?.intercomBridge, { mode: "off" });
150
+ });
151
+
138
152
  it("renders prompt text before passing it explicitly to a child", async () => {
139
153
  let launchTask: unknown;
140
154
  const result = await runWorkflowScript({
@@ -277,6 +291,34 @@ describe("scripted workflow runtime", () => {
277
291
  );
278
292
  });
279
293
 
294
+ it("tags only fail-fast detached child errors as detached-child", async () => {
295
+ await assert.rejects(
296
+ runWorkflowScript({
297
+ script: `return await runs.run("detaches", { agent: "worker", task: "ask" });`,
298
+ timeoutMs: 2_000,
299
+ async launch(key) { return { key, ok: false, detached: true, output: "reply first", error: "reply first", artifactPaths: [], results: [] }; },
300
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
301
+ }),
302
+ (error: unknown) => error instanceof WorkflowScriptError && error.errorKind === "detached-child" && /Run 'detaches' detached/.test(error.message),
303
+ );
304
+
305
+ await assert.rejects(
306
+ runWorkflowScript({
307
+ script: `
308
+ await runs.all([{ key: "detaches", agent: "worker", task: "ask" }]);
309
+ throw new Error("manual hard failure");
310
+ `,
311
+ timeoutMs: 2_000,
312
+ async launch(key) { return { key, ok: false, detached: true, output: "reply first", error: "reply first", artifactPaths: [], results: [] }; },
313
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
314
+ }),
315
+ (error: unknown) => error instanceof WorkflowScriptError
316
+ && error.errorKind === undefined
317
+ && /manual hard failure/.test(error.message)
318
+ && error.partial.children[0]?.detached === true,
319
+ );
320
+ });
321
+
280
322
  it("validates every runs.all item before launching children", async () => {
281
323
  const malformedScripts = [
282
324
  `return await runs.all([{ key: "valid", agent: "worker", task: "run" }, null]);`,
@@ -577,6 +619,133 @@ describe("scripted workflow runtime", () => {
577
619
  );
578
620
  });
579
621
 
622
+ it("rejects an unawaited new Promise wrapper over runs.run", async () => {
623
+ await assert.rejects(
624
+ runWorkflowScript({
625
+ script: `new Promise((resolve) => resolve(runs.run("wrapped", { agent: "worker", task: "fire" }))); return "done";`,
626
+ timeoutMs: 2_000,
627
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
628
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
629
+ }),
630
+ (error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'wrapped'"),
631
+ );
632
+ });
633
+
634
+ it("rejects an unawaited async helper wrapper over runs.run", async () => {
635
+ await assert.rejects(
636
+ runWorkflowScript({
637
+ script: `async function helper() { return runs.run("async-bare", { agent: "worker", task: "run" }); } helper(); return "done";`,
638
+ timeoutMs: 2_000,
639
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
640
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
641
+ }),
642
+ (error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'async-bare'"),
643
+ );
644
+ });
645
+
646
+ it("accepts an awaited async helper wrapper over runs.run", async () => {
647
+ const result = await runWorkflowScript({
648
+ script: `async function helper() { return runs.run("async-awaited", { agent: "worker", task: "run" }); } const child = await helper(); return child.output;`,
649
+ timeoutMs: 2_000,
650
+ async launch(key) { return { key, ok: true, output: "helper output", artifactPaths: [] }; },
651
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
652
+ });
653
+ assert.equal(result.value, "helper output");
654
+ });
655
+
656
+ it("accepts awaited and returned handlers on async helper wrappers", async () => {
657
+ const handlers = [
658
+ { name: "then", chain: "then((value) => value)" },
659
+ { name: "catch", chain: "catch(() => ({ output: 'fallback' }))" },
660
+ { name: "finally", chain: "finally(() => {})" },
661
+ ];
662
+ for (const mode of ["await", "return"] as const) {
663
+ for (const { name, chain } of handlers) {
664
+ const key = `${mode}-${name}`;
665
+ const expression = `helper().${chain}`;
666
+ const result = await runWorkflowScript({
667
+ script: `async function helper() { return runs.run("${key}", { agent: "worker", task: "run" }); } ${mode === "await" ? `const child = await ${expression}; return child.output;` : `return ${expression};`}`,
668
+ timeoutMs: 2_000,
669
+ async launch(key) { return { key, ok: true, output: "helper chain output", artifactPaths: [] }; },
670
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
671
+ });
672
+ assert.equal(mode === "await" ? result.value : (result.value as { output: string }).output, "helper chain output");
673
+ }
674
+ }
675
+ });
676
+
677
+ it("rejects a nested dropped async helper propagation wrapper", async () => {
678
+ await assert.rejects(
679
+ runWorkflowScript({
680
+ script: `async function inner() { return runs.run("nested-bare", { agent: "worker", task: "run" }); } async function outer() { return inner(); } outer(); return "done";`,
681
+ timeoutMs: 2_000,
682
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
683
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
684
+ }),
685
+ (error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'nested-bare'"),
686
+ );
687
+ });
688
+
689
+ it("accepts awaited and returned nested async helper wrappers", async () => {
690
+ for (const mode of ["await", "return"] as const) {
691
+ const key = `nested-${mode}`;
692
+ const result = await runWorkflowScript({
693
+ script: `async function inner() { return runs.run("${key}", { agent: "worker", task: "run" }); } async function outer() { return inner(); } ${mode === "await" ? "const child = await outer(); return child.output;" : "return outer();"}`,
694
+ timeoutMs: 2_000,
695
+ async launch(key) { return { key, ok: true, output: "nested output", artifactPaths: [] }; },
696
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
697
+ });
698
+ assert.equal(mode === "await" ? result.value : (result.value as { output: string }).output, "nested output");
699
+ }
700
+ });
701
+
702
+ it("counts an explicit internal helper await as observed", async () => {
703
+ const result = await runWorkflowScript({
704
+ script: `async function helper() { return await runs.run("internal-await", { agent: "worker", task: "run" }); } helper(); return "done";`,
705
+ timeoutMs: 2_000,
706
+ async launch(key) { return { key, ok: true, output: "helper output", artifactPaths: [] }; },
707
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
708
+ });
709
+ assert.equal(result.value, "done");
710
+ });
711
+
712
+ it("rejects detached handlers on async helper wrappers", async () => {
713
+ const handlers = [
714
+ { key: "async-then", chain: "then(() => {})" },
715
+ { key: "async-catch", chain: "catch(() => {})" },
716
+ { key: "async-finally", chain: "finally(() => {})" },
717
+ ];
718
+ for (const { key, chain } of handlers) {
719
+ await assert.rejects(
720
+ runWorkflowScript({
721
+ script: `async function helper() { return runs.run("${key}", { agent: "worker", task: "run" }); } helper().${chain}; return "done";`,
722
+ timeoutMs: 2_000,
723
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
724
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
725
+ }),
726
+ (error: unknown) => error instanceof WorkflowScriptError && error.message.includes(`unawaited runs.run launch(es): '${key}'`),
727
+ );
728
+ }
729
+ });
730
+
731
+ it("rejects a launch passed to an ignored repeated Promise resolution", async () => {
732
+ await assert.rejects(
733
+ runWorkflowScript({
734
+ script: `
735
+ await new Promise((resolve) => {
736
+ resolve("done");
737
+ Promise.resolve().then(() => resolve(runs.run("ignored", { agent: "worker", task: "fire" })));
738
+ });
739
+ return "done";
740
+ `,
741
+ timeoutMs: 2_000,
742
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
743
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
744
+ }),
745
+ (error: unknown) => error instanceof WorkflowScriptError && error.message.includes("unawaited runs.run launch(es): 'ignored'"),
746
+ );
747
+ });
748
+
580
749
  it("rejects fire-and-forget callbacks on a runs.run promise", async () => {
581
750
  await assert.rejects(
582
751
  runWorkflowScript({
@@ -589,6 +758,25 @@ describe("scripted workflow runtime", () => {
589
758
  );
590
759
  });
591
760
 
761
+ it("rejects detached promise handlers after an await", async () => {
762
+ const handlers = [
763
+ { key: "bg-then", chain: "then(() => {})" },
764
+ { key: "bg-catch", chain: "catch(() => {})" },
765
+ { key: "bg-finally", chain: "finally(() => {})" },
766
+ ];
767
+ for (const { key, chain } of handlers) {
768
+ await assert.rejects(
769
+ runWorkflowScript({
770
+ script: `await Promise.resolve(); runs.run("${key}", { agent: "worker", task: "fire" }).${chain}; return "done";`,
771
+ timeoutMs: 2_000,
772
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
773
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
774
+ }),
775
+ (error: unknown) => error instanceof WorkflowScriptError && error.message.includes(`unawaited runs.run launch(es): '${key}'`),
776
+ );
777
+ }
778
+ });
779
+
592
780
  it("rejects reading output from an unawaited runs.run promise", async () => {
593
781
  await assert.rejects(
594
782
  runWorkflowScript({
@@ -629,9 +817,9 @@ describe("scripted workflow runtime", () => {
629
817
  assert.equal((result.value as { output?: string }).output, "direct output");
630
818
  });
631
819
 
632
- it("accepts output from an explicitly awaited runs.run promise", async () => {
820
+ it("accepts a docs-style awaited runs.run launch", async () => {
633
821
  const result = await runWorkflowScript({
634
- script: `return (await runs.run("awaited", { agent: "worker", task: "run" })).output;`,
822
+ script: `const child = await runs.run("awaited", { agent: "worker", task: "run" }); return child.output;`,
635
823
  timeoutMs: 2_000,
636
824
  async launch(key) { return { key, ok: true, output: "awaited output", artifactPaths: [] }; },
637
825
  async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
@@ -639,6 +827,103 @@ describe("scripted workflow runtime", () => {
639
827
  assert.equal(result.value, "awaited output");
640
828
  });
641
829
 
830
+ it("accepts a docs-style awaited runs.all launch group", async () => {
831
+ const result = await runWorkflowScript({
832
+ script: `const children = await runs.all([{ key: "one", agent: "worker", task: "run" }]); return children[0].output;`,
833
+ timeoutMs: 2_000,
834
+ async launch(key) { return { key, ok: true, output: "group output", artifactPaths: [] }; },
835
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
836
+ });
837
+ assert.equal(result.value, "group output");
838
+ });
839
+
840
+ it("accepts sequential awaited launches that use the first output", async () => {
841
+ const tasks: unknown[] = [];
842
+ const result = await runWorkflowScript({
843
+ script: `
844
+ const first = await runs.run("first", { agent: "worker", task: "plan" });
845
+ const second = await runs.run("second", { agent: "worker", task: first.output });
846
+ return second.output;
847
+ `,
848
+ timeoutMs: 2_000,
849
+ async launch(key, params) {
850
+ tasks.push(params.task);
851
+ return { key, ok: true, output: key === "first" ? "first output" : "second output", artifactPaths: [] };
852
+ },
853
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
854
+ });
855
+ assert.deepEqual(tasks, ["plan", "first output"]);
856
+ assert.equal(result.value, "second output");
857
+ });
858
+
859
+ it("accepts an awaited native Promise combinator over launches", async () => {
860
+ const result = await runWorkflowScript({
861
+ script: `const children = await Promise.all([runs.run("native", { agent: "worker", task: "run" })]); return children[0].output;`,
862
+ timeoutMs: 2_000,
863
+ async launch(key) { return { key, ok: true, output: "native output", artifactPaths: [] }; },
864
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
865
+ });
866
+ assert.equal(result.value, "native output");
867
+ });
868
+
869
+ it("accepts Promise.resolve over a pending helper wrapper", async () => {
870
+ const result = await runWorkflowScript({
871
+ script: `
872
+ const helper = new Promise((resolve) => Promise.resolve().then(() =>
873
+ resolve(runs.run("resolve-helper", { agent: "worker", task: "run" }))
874
+ ));
875
+ const child = await Promise.resolve(helper);
876
+ return child.output;
877
+ `,
878
+ timeoutMs: 2_000,
879
+ async launch(key) { return { key, ok: true, output: "resolved helper output", artifactPaths: [] }; },
880
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
881
+ });
882
+ assert.equal(result.value, "resolved helper output");
883
+ });
884
+
885
+ it("accepts Promise.all over a pending helper wrapper", async () => {
886
+ const result = await runWorkflowScript({
887
+ script: `
888
+ const helper = new Promise((resolve) => Promise.resolve().then(() =>
889
+ resolve(runs.run("combo-later", { agent: "worker", task: "run" }))
890
+ ));
891
+ const children = await Promise.all([helper]);
892
+ return children[0].output;
893
+ `,
894
+ timeoutMs: 2_000,
895
+ async launch(key) { return { key, ok: true, output: "pending helper output", artifactPaths: [] }; },
896
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
897
+ });
898
+ assert.equal(result.value, "pending helper output");
899
+ });
900
+
901
+ it("accepts an awaited then chain over a pending helper wrapper", async () => {
902
+ const result = await runWorkflowScript({
903
+ script: `
904
+ const helper = new Promise((resolve) => Promise.resolve().then(() =>
905
+ resolve(runs.run("chain-later", { agent: "worker", task: "run" }))
906
+ ));
907
+ const child = await helper.then((value) => value);
908
+ return child.output;
909
+ `,
910
+ timeoutMs: 2_000,
911
+ async launch(key) { return { key, ok: true, output: "chain output", artifactPaths: [] }; },
912
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
913
+ });
914
+ assert.equal(result.value, "chain output");
915
+ });
916
+
917
+ it("accepts an awaited new Promise wrapper over runs.run", async () => {
918
+ const result = await runWorkflowScript({
919
+ script: `const child = await new Promise((resolve) => resolve(runs.run("wrapped", { agent: "worker", task: "run" }))); return child.output;`,
920
+ timeoutMs: 2_000,
921
+ async launch(key) { return { key, ok: true, output: "wrapped output", artifactPaths: [] }; },
922
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
923
+ });
924
+ assert.equal(result.value, "wrapped output");
925
+ });
926
+
642
927
  it("rejects non-JSON-safe emitted values without persisting them", async () => {
643
928
  const invalidScripts = [
644
929
  `emit(undefined);`,
@@ -748,6 +1033,113 @@ describe("scripted workflow runtime", () => {
748
1033
  assert.equal(childAborted, true);
749
1034
  });
750
1035
 
1036
+ it("ignores a queued child launch message after workflow abort", async () => {
1037
+ const originalOn = Worker.prototype.on;
1038
+ const controller = new AbortController();
1039
+ let launchCount = 0;
1040
+ let deliverCapturedRunMessage: (() => void) | undefined;
1041
+ let markRunMessageCaptured!: () => void;
1042
+ const runMessageCaptured = new Promise<void>((resolve) => { markRunMessageCaptured = resolve; });
1043
+
1044
+ (Worker.prototype as unknown as { on: typeof Worker.prototype.on }).on = function (event: string | symbol, listener: (...args: unknown[]) => void) {
1045
+ if (event !== "message") return originalOn.call(this, event, listener);
1046
+ const wrapped = (message: Record<string, unknown>) => {
1047
+ if (message.type === "call" && message.method === "run") {
1048
+ deliverCapturedRunMessage = () => listener.call(this, message);
1049
+ markRunMessageCaptured();
1050
+ return;
1051
+ }
1052
+ listener.call(this, message);
1053
+ };
1054
+ return originalOn.call(this, event, wrapped);
1055
+ };
1056
+
1057
+ try {
1058
+ const workflow = runWorkflowScript({
1059
+ script: `await runs.run("late", { agent: "worker", task: "wait" });`,
1060
+ signal: controller.signal,
1061
+ async launch(key) {
1062
+ launchCount += 1;
1063
+ return { key, ok: true, output: "too late", artifactPaths: [] };
1064
+ },
1065
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
1066
+ });
1067
+
1068
+ await runMessageCaptured;
1069
+ controller.abort(new Error("Workflow stopped by user."));
1070
+ await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError && error.message === "Workflow stopped by user.");
1071
+ deliverCapturedRunMessage?.();
1072
+ await new Promise((resolve) => queueMicrotask(resolve));
1073
+ assert.equal(launchCount, 0);
1074
+ } finally {
1075
+ Worker.prototype.on = originalOn;
1076
+ }
1077
+ });
1078
+
1079
+ it("marks a child stopped when abort fires during the started trace callback", async () => {
1080
+ const controller = new AbortController();
1081
+ let admitCount = 0;
1082
+ let launchCount = 0;
1083
+
1084
+ await assert.rejects(
1085
+ runWorkflowScript({
1086
+ script: `await runs.run("slow", { agent: "worker", task: "wait" });`,
1087
+ signal: controller.signal,
1088
+ onTrace(trace) {
1089
+ const started = trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "started");
1090
+ if (started && !controller.signal.aborted) controller.abort(new Error("Workflow stopped by user."));
1091
+ },
1092
+ admit() {
1093
+ admitCount += 1;
1094
+ },
1095
+ async launch(key) {
1096
+ launchCount += 1;
1097
+ return { key, ok: true, output: "done", artifactPaths: [] };
1098
+ },
1099
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
1100
+ }),
1101
+ (error: unknown) => error instanceof WorkflowScriptError
1102
+ && error.message === "Workflow stopped by user."
1103
+ && error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "stopped")
1104
+ && !error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "failed"),
1105
+ );
1106
+ await new Promise((resolve) => queueMicrotask(resolve));
1107
+ assert.equal(admitCount, 0);
1108
+ assert.equal(launchCount, 0);
1109
+ });
1110
+
1111
+ it("does not launch a child after admission settles following workflow abort", async () => {
1112
+ const controller = new AbortController();
1113
+ let launchCount = 0;
1114
+ let resolveAdmission!: () => void;
1115
+ let markAdmissionStarted!: () => void;
1116
+ const admissionStarted = new Promise<void>((resolve) => { markAdmissionStarted = resolve; });
1117
+
1118
+ const workflow = runWorkflowScript({
1119
+ script: `await runs.run("slow", { agent: "worker", task: "wait" });`,
1120
+ signal: controller.signal,
1121
+ admit() {
1122
+ markAdmissionStarted();
1123
+ return new Promise<void>((resolve) => { resolveAdmission = resolve; });
1124
+ },
1125
+ async launch(key) {
1126
+ launchCount += 1;
1127
+ return { key, ok: true, output: "done", artifactPaths: [] };
1128
+ },
1129
+ async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
1130
+ });
1131
+
1132
+ await admissionStarted;
1133
+ controller.abort(new Error("Workflow stopped by user."));
1134
+ await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError
1135
+ && error.message === "Workflow stopped by user."
1136
+ && error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "stopped")
1137
+ && !error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "failed"));
1138
+ resolveAdmission();
1139
+ await new Promise((resolve) => queueMicrotask(resolve));
1140
+ assert.equal(launchCount, 0);
1141
+ });
1142
+
751
1143
  it("drops a child response that settles after the workflow aborts", async () => {
752
1144
  const workerPrototype = Worker.prototype as unknown as { postMessage(value: unknown, ...args: unknown[]): void };
753
1145
  const originalPostMessage = workerPrototype.postMessage;
@@ -773,8 +1165,11 @@ describe("scripted workflow runtime", () => {
773
1165
  async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
774
1166
  });
775
1167
  await launchStarted;
776
- controller.abort();
777
- await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError && /aborted/.test(error.message));
1168
+ controller.abort(new Error("Workflow stopped by user."));
1169
+ await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError
1170
+ && error.message === "Workflow stopped by user."
1171
+ && error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "stopped" && entry.error === "Workflow stopped by user.")
1172
+ && !error.partial.trace.some((entry) => entry.operation === "run" && entry.key === "slow" && entry.state === "failed"));
778
1173
  workflowSettled = true;
779
1174
  resolveLaunch({ key: "slow", ok: true, output: "done", artifactPaths: [], results: [] });
780
1175
  await new Promise((resolve) => queueMicrotask(resolve));
@@ -783,4 +1178,61 @@ describe("scripted workflow runtime", () => {
783
1178
  workerPrototype.postMessage = originalPostMessage;
784
1179
  }
785
1180
  });
1181
+
1182
+ it("does not dispatch status after abort fires during the status started trace callback", async () => {
1183
+ const controller = new AbortController();
1184
+ let statusCount = 0;
1185
+
1186
+ await assert.rejects(
1187
+ runWorkflowScript({
1188
+ script: `await runs.status("probe");`,
1189
+ signal: controller.signal,
1190
+ onTrace(trace) {
1191
+ const started = trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "started");
1192
+ if (started && !controller.signal.aborted) controller.abort(new Error("Workflow stopped by user."));
1193
+ },
1194
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
1195
+ async status(key) {
1196
+ statusCount += 1;
1197
+ return { key, ok: true, output: "done", artifactPaths: [] };
1198
+ },
1199
+ }),
1200
+ (error: unknown) => error instanceof WorkflowScriptError
1201
+ && error.message === "Workflow stopped by user."
1202
+ && error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "started")
1203
+ && !error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "completed"),
1204
+ );
1205
+ await new Promise((resolve) => queueMicrotask(resolve));
1206
+ assert.equal(statusCount, 0);
1207
+ });
1208
+
1209
+ it("drops a status response that settles after the workflow aborts", async () => {
1210
+ const controller = new AbortController();
1211
+ let traceLengths: number[] = [];
1212
+ let resolveStatus!: (result: { key: string; ok: true; output: string; artifactPaths: string[] }) => void;
1213
+ let markStatusStarted!: () => void;
1214
+ const statusStarted = new Promise<void>((resolve) => { markStatusStarted = resolve; });
1215
+
1216
+ const workflow = runWorkflowScript({
1217
+ script: `await runs.status("probe");`,
1218
+ signal: controller.signal,
1219
+ onTrace(trace) { traceLengths.push(trace.length); },
1220
+ async launch(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
1221
+ status(key) {
1222
+ markStatusStarted();
1223
+ return new Promise((resolve) => { resolveStatus = resolve; });
1224
+ },
1225
+ });
1226
+
1227
+ await statusStarted;
1228
+ controller.abort(new Error("Workflow stopped by user."));
1229
+ await assert.rejects(workflow, (error: unknown) => error instanceof WorkflowScriptError
1230
+ && error.message === "Workflow stopped by user."
1231
+ && error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "started")
1232
+ && !error.partial.trace.some((entry) => entry.operation === "status" && entry.key === "probe" && entry.state === "completed"));
1233
+ const finalTraceLength = traceLengths.at(-1);
1234
+ resolveStatus({ key: "probe", ok: true, output: "done", artifactPaths: [] });
1235
+ await new Promise((resolve) => queueMicrotask(resolve));
1236
+ assert.equal(traceLengths.at(-1), finalTraceLength);
1237
+ });
786
1238
  });
@@ -0,0 +1,73 @@
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 { resolveSessionLineage } from "../../src/shared/session-lineage.ts";
7
+
8
+ function sessionFile(dir: string, name: string, parentSession?: string): string {
9
+ const file = path.join(dir, name);
10
+ const header = {
11
+ type: "session",
12
+ version: 1,
13
+ id: name,
14
+ timestamp: new Date().toISOString(),
15
+ cwd: dir,
16
+ ...(parentSession ? { parentSession } : {}),
17
+ };
18
+ fs.writeFileSync(file, `${JSON.stringify(header)}\n`);
19
+ return file;
20
+ }
21
+
22
+ describe("session lineage", () => {
23
+ it("resolves self plus the parent chain from session headers", () => {
24
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-lineage-"));
25
+ const grandparent = sessionFile(dir, "a.jsonl");
26
+ const parent = sessionFile(dir, "b.jsonl", grandparent);
27
+ const current = sessionFile(dir, "c.jsonl", parent);
28
+ const lineage = resolveSessionLineage({ sessionManager: { getSessionFile: () => current, getHeader: () => ({ parentSession: parent }) } });
29
+ assert.deepEqual(lineage, [current, parent, grandparent]);
30
+ });
31
+
32
+ it("prefers an explicit parent hint over the header", () => {
33
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-lineage-"));
34
+ const hinted = sessionFile(dir, "hinted.jsonl");
35
+ const headed = sessionFile(dir, "headed.jsonl");
36
+ const current = sessionFile(dir, "current.jsonl", headed);
37
+ const lineage = resolveSessionLineage({ sessionManager: { getSessionFile: () => current, getHeader: () => ({ parentSession: headed }) } }, { parentHint: hinted });
38
+ assert.deepEqual(lineage, [current, hinted]);
39
+ });
40
+
41
+ it("bounds the walk depth", () => {
42
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-lineage-"));
43
+ const files: string[] = [];
44
+ for (let i = 0; i < 12; i++) files.push(sessionFile(dir, `s${i}.jsonl`, i > 0 ? files[i - 1]! : undefined));
45
+ const current = sessionFile(dir, "current.jsonl", files[11]);
46
+ const lineage = resolveSessionLineage({ sessionManager: { getSessionFile: () => current } }, { depth: 3 });
47
+ assert.equal(lineage.length, 3);
48
+ assert.equal(lineage[0], current);
49
+ assert.equal(lineage[1], files[11]);
50
+ assert.equal(lineage[2], files[10]);
51
+ });
52
+
53
+ it("stops at missing parent files", () => {
54
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-lineage-"));
55
+ const missing = path.join(dir, "missing.jsonl");
56
+ const current = sessionFile(dir, "current.jsonl", missing);
57
+ const lineage = resolveSessionLineage({ sessionManager: { getSessionFile: () => current, getHeader: () => ({ parentSession: missing }) } });
58
+ assert.deepEqual(lineage, [current]);
59
+ });
60
+
61
+ it("does not loop on cyclic parent chains", () => {
62
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-lineage-"));
63
+ const a = sessionFile(dir, "a.jsonl");
64
+ const b = sessionFile(dir, "b.jsonl", a);
65
+ fs.writeFileSync(a, `${JSON.stringify({ type: "session", version: 1, id: "a", timestamp: new Date().toISOString(), cwd: dir, parentSession: b })}\n`);
66
+ const lineage = resolveSessionLineage({ sessionManager: { getSessionFile: () => b, getHeader: () => ({ parentSession: a }) } });
67
+ assert.deepEqual(lineage, [b, a]);
68
+ });
69
+
70
+ it("handles session managers without getHeader or session file", () => {
71
+ assert.deepEqual(resolveSessionLineage({ sessionManager: { getSessionFile: () => null } }, { parentHint: "/nonexistent/parent.jsonl" }), []);
72
+ });
73
+ });