@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,415 @@
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 { SessionManager } from "@selesai/code";
7
+ import { createForkContextResolver, resolveSubagentContext } from "../../src/shared/fork-context.ts";
8
+
9
+ function writeMinimalSessionFile(filePath: string, id = "session"): void {
10
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
+ fs.writeFileSync(filePath, `{"type":"session","version":1,"id":"${id}","timestamp":"2026-04-16T00:00:00.000Z","cwd":"/tmp"}\n`, "utf-8");
12
+ }
13
+
14
+ function writeSessionJsonl(filePath: string, entries: unknown[]): void {
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ fs.writeFileSync(filePath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
17
+ }
18
+
19
+ describe("resolveSubagentContext", () => {
20
+ it("defaults to fresh", () => {
21
+ assert.equal(resolveSubagentContext(undefined), "fresh");
22
+ assert.equal(resolveSubagentContext("anything"), "fresh");
23
+ });
24
+
25
+ it("accepts fork", () => {
26
+ assert.equal(resolveSubagentContext("fork"), "fork");
27
+ });
28
+ });
29
+
30
+ describe("createForkContextResolver", () => {
31
+ it("fresh mode never calls createBranchedSession", () => {
32
+ let calls = 0;
33
+ const resolver = createForkContextResolver({
34
+ getSessionFile: () => "/tmp/parent.jsonl",
35
+ getLeafId: () => "leaf-123",
36
+ }, "fresh", {
37
+ openSession: () => ({
38
+ createBranchedSession: () => {
39
+ calls++;
40
+ return "/tmp/child.jsonl";
41
+ },
42
+ }),
43
+ });
44
+
45
+ assert.equal(resolver.sessionFileForIndex(0), undefined);
46
+ assert.equal(resolver.thinkingOverrideForIndex(0), undefined);
47
+ assert.equal(calls, 0);
48
+ });
49
+
50
+ it("fails fast when parent session file is missing", () => {
51
+ assert.throws(
52
+ () => createForkContextResolver({
53
+ getSessionFile: () => undefined,
54
+ getLeafId: () => "leaf-123",
55
+ }, "fork", { openSession: () => ({ createBranchedSession: () => "/tmp/child.jsonl" }) }),
56
+ /Forked subagent context requires a persisted parent session\./,
57
+ );
58
+ });
59
+
60
+ it("fails fast when leaf id is missing", () => {
61
+ assert.throws(
62
+ () => createForkContextResolver({
63
+ getSessionFile: () => "/tmp/parent.jsonl",
64
+ getLeafId: () => null,
65
+ }, "fork", { openSession: () => ({ createBranchedSession: () => "/tmp/child.jsonl" }) }),
66
+ /Forked subagent context requires a current leaf to fork from\./,
67
+ );
68
+ });
69
+
70
+ it("opens a throwaway manager from the persisted parent session file", () => {
71
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-open-"));
72
+ try {
73
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
74
+ writeMinimalSessionFile(parentSessionFile, "parent");
75
+ const openedPaths: string[] = [];
76
+ const seenLeafIds: string[] = [];
77
+ const resolver = createForkContextResolver({
78
+ getSessionFile: () => parentSessionFile,
79
+ getLeafId: () => "leaf-xyz",
80
+ }, "fork", {
81
+ openSession: (sessionFile: string) => {
82
+ openedPaths.push(sessionFile);
83
+ return {
84
+ createBranchedSession: (leafId: string) => {
85
+ seenLeafIds.push(leafId);
86
+ const childSessionFile = path.join(tempDir, `child-${seenLeafIds.length}.jsonl`);
87
+ writeMinimalSessionFile(childSessionFile, `child-${seenLeafIds.length}`);
88
+ return childSessionFile;
89
+ },
90
+ };
91
+ },
92
+ });
93
+
94
+ resolver.sessionFileForIndex(0);
95
+ resolver.sessionFileForIndex(1);
96
+ resolver.sessionFileForIndex(2);
97
+
98
+ assert.deepEqual(openedPaths, [parentSessionFile, parentSessionFile, parentSessionFile]);
99
+ assert.deepEqual(seenLeafIds, ["leaf-xyz", "leaf-xyz", "leaf-xyz"]);
100
+ } finally {
101
+ fs.rmSync(tempDir, { recursive: true, force: true });
102
+ }
103
+ });
104
+
105
+ it("creates forked sessions through the default package opener", () => {
106
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-default-"));
107
+ try {
108
+ const sessionDir = path.join(tempDir, "sessions");
109
+ const parent = SessionManager.create(tempDir, sessionDir);
110
+ parent.appendMessage({ role: "user", content: "parent prompt" });
111
+ parent.appendMessage({ role: "assistant", content: "parent response" });
112
+ const parentSessionFile = parent.getSessionFile();
113
+ const leafId = parent.getLeafId();
114
+
115
+ assert.ok(parentSessionFile);
116
+ assert.ok(leafId);
117
+
118
+ const resolver = createForkContextResolver({
119
+ getSessionFile: () => parentSessionFile,
120
+ getLeafId: () => leafId,
121
+ getSessionDir: () => sessionDir,
122
+ }, "fork");
123
+
124
+ const childSessionFile = resolver.sessionFileForIndex(0);
125
+ assert.ok(childSessionFile);
126
+ assert.notEqual(childSessionFile, parentSessionFile);
127
+ assert.equal(fs.existsSync(childSessionFile), true);
128
+ } finally {
129
+ fs.rmSync(tempDir, { recursive: true, force: true });
130
+ }
131
+ });
132
+
133
+ it("fails clearly for an unflushed user-only parent", () => {
134
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-user-only-"));
135
+ try {
136
+ const sessionDir = path.join(tempDir, "sessions");
137
+ const parent = SessionManager.create(tempDir, sessionDir);
138
+ parent.appendMessage({ role: "user", content: "first turn prompt" });
139
+ const parentSessionFile = parent.getSessionFile();
140
+ const leafId = parent.getLeafId();
141
+
142
+ assert.ok(parentSessionFile);
143
+ assert.ok(leafId);
144
+ assert.equal(fs.existsSync(parentSessionFile), false);
145
+
146
+ const resolver = createForkContextResolver(parent, "fork");
147
+ assert.throws(
148
+ () => resolver.sessionFileForIndex(0),
149
+ /Failed to create forked subagent session: Parent session file does not exist: .*Pi has not persisted enough history to fork yet\./,
150
+ );
151
+ assert.equal(parent.getSessionFile(), parentSessionFile);
152
+ assert.equal(parent.getLeafId(), leafId);
153
+ assert.equal(fs.existsSync(parentSessionFile), false);
154
+ } finally {
155
+ fs.rmSync(tempDir, { recursive: true, force: true });
156
+ }
157
+ });
158
+
159
+ it("creates isolated branched sessions per index (parallel and chain compatible)", () => {
160
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-index-"));
161
+ try {
162
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
163
+ writeMinimalSessionFile(parentSessionFile, "parent");
164
+ let count = 0;
165
+ const resolver = createForkContextResolver({
166
+ getSessionFile: () => parentSessionFile,
167
+ getLeafId: () => "leaf-abc",
168
+ }, "fork", {
169
+ openSession: () => ({
170
+ createBranchedSession: () => {
171
+ count++;
172
+ const childSessionFile = path.join(tempDir, `fork-${count}.jsonl`);
173
+ writeMinimalSessionFile(childSessionFile, `child-${count}`);
174
+ return childSessionFile;
175
+ },
176
+ }),
177
+ });
178
+
179
+ const singleSession = resolver.sessionFileForIndex(0);
180
+ const parallelSessions = [resolver.sessionFileForIndex(1), resolver.sessionFileForIndex(2)];
181
+ const chainSessions = [resolver.sessionFileForIndex(3), resolver.sessionFileForIndex(4)];
182
+
183
+ assert.equal(singleSession, path.join(tempDir, "fork-1.jsonl"));
184
+ assert.deepEqual(parallelSessions, [path.join(tempDir, "fork-2.jsonl"), path.join(tempDir, "fork-3.jsonl")]);
185
+ assert.deepEqual(chainSessions, [path.join(tempDir, "fork-4.jsonl"), path.join(tempDir, "fork-5.jsonl")]);
186
+ assert.equal(count, 5);
187
+ } finally {
188
+ fs.rmSync(tempDir, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ it("memoizes per index to keep behavior deterministic", () => {
193
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-memo-"));
194
+ try {
195
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
196
+ writeMinimalSessionFile(parentSessionFile, "parent");
197
+ let calls = 0;
198
+ const resolver = createForkContextResolver({
199
+ getSessionFile: () => parentSessionFile,
200
+ getLeafId: () => "leaf-abc",
201
+ }, "fork", {
202
+ openSession: () => ({
203
+ createBranchedSession: () => {
204
+ calls++;
205
+ const childSessionFile = path.join(tempDir, `fork-${calls}.jsonl`);
206
+ writeMinimalSessionFile(childSessionFile, `child-${calls}`);
207
+ return childSessionFile;
208
+ },
209
+ }),
210
+ });
211
+
212
+ const first = resolver.sessionFileForIndex(7);
213
+ const second = resolver.sessionFileForIndex(7);
214
+ assert.equal(first, second);
215
+ assert.equal(calls, 1);
216
+ } finally {
217
+ fs.rmSync(tempDir, { recursive: true, force: true });
218
+ }
219
+ });
220
+
221
+ it("persists a forked session when Pi defers writing the branched file", () => {
222
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-deferred-child-"));
223
+ try {
224
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
225
+ const childSessionFile = path.join(tempDir, "nested", "child.jsonl");
226
+ writeMinimalSessionFile(parentSessionFile, "parent");
227
+ const header = { type: "session", version: 1, id: "child", timestamp: "2026-04-16T00:00:00.000Z", cwd: "/tmp", parentSession: parentSessionFile };
228
+ const entries = [{ type: "message", id: "leaf-abc", parentId: null, timestamp: "2026-04-16T00:00:01.000Z", message: { role: "user", content: "first turn" } }];
229
+ const resolver = createForkContextResolver({
230
+ getSessionFile: () => parentSessionFile,
231
+ getLeafId: () => "leaf-abc",
232
+ }, "fork", {
233
+ openSession: () => ({
234
+ createBranchedSession: () => childSessionFile,
235
+ getHeader: () => header,
236
+ getEntries: () => entries,
237
+ }),
238
+ });
239
+
240
+ assert.equal(resolver.sessionFileForIndex(0), childSessionFile);
241
+ assert.equal(fs.existsSync(childSessionFile), true);
242
+ assert.deepEqual(
243
+ fs.readFileSync(childSessionFile, "utf-8").trim().split("\n").map((line) => JSON.parse(line)),
244
+ [header, ...entries],
245
+ );
246
+ } finally {
247
+ fs.rmSync(tempDir, { recursive: true, force: true });
248
+ }
249
+ });
250
+
251
+ it("removes signed Anthropic thinking blocks before forwarding a forked session", () => {
252
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-thinking-"));
253
+ try {
254
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
255
+ const childSessionFile = path.join(tempDir, "child.jsonl");
256
+ writeMinimalSessionFile(parentSessionFile, "parent");
257
+ writeSessionJsonl(childSessionFile, [
258
+ { type: "session", version: 1, id: "child", timestamp: "2026-04-16T00:00:00.000Z", cwd: "/tmp", parentSession: parentSessionFile },
259
+ { type: "message", id: "user-1", parentId: null, timestamp: "2026-04-16T00:00:01.000Z", message: { role: "user", content: "prompt" } },
260
+ { type: "message", id: "assistant-1", parentId: "user-1", timestamp: "2026-04-16T00:00:02.000Z", message: { role: "assistant", provider: "anthropic", api: "anthropic-messages", model: "anthropic/claude-sonnet-4", content: [{ type: "thinking", thinking: "private chain", thinkingSignature: "signed" }, { type: "text", text: "answer" }] } },
261
+ ]);
262
+ const resolver = createForkContextResolver({
263
+ getSessionFile: () => parentSessionFile,
264
+ getLeafId: () => "assistant-1",
265
+ }, "fork", {
266
+ openSession: () => ({
267
+ createBranchedSession: () => childSessionFile,
268
+ }),
269
+ });
270
+
271
+ assert.equal(resolver.sessionFileForIndex(0), childSessionFile);
272
+ assert.equal(resolver.thinkingOverrideForIndex(0), "off");
273
+ const entries = fs.readFileSync(childSessionFile, "utf-8").trim().split("\n").map((line) => JSON.parse(line));
274
+ assert.deepEqual(entries[2].message.content, [{ type: "text", text: "answer" }]);
275
+ assert.equal(entries[3].type, "thinking_level_change");
276
+ assert.equal(entries[3].thinkingLevel, "off");
277
+ assert.equal(entries[3].parentId, "assistant-1");
278
+ } finally {
279
+ fs.rmSync(tempDir, { recursive: true, force: true });
280
+ }
281
+ });
282
+
283
+ it("allows unsigned thinking blocks in forked sessions", () => {
284
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-unsigned-thinking-"));
285
+ try {
286
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
287
+ const childSessionFile = path.join(tempDir, "child.jsonl");
288
+ writeMinimalSessionFile(parentSessionFile, "parent");
289
+ writeSessionJsonl(childSessionFile, [
290
+ { type: "session", version: 1, id: "child", timestamp: "2026-04-16T00:00:00.000Z", cwd: "/tmp", parentSession: parentSessionFile },
291
+ { type: "message", id: "assistant-1", parentId: null, timestamp: "2026-04-16T00:00:02.000Z", message: { role: "assistant", provider: "anthropic", api: "anthropic-messages", model: "anthropic/claude-sonnet-4", content: [{ type: "thinking", thinking: "summary without provider signature" }, { type: "text", text: "answer" }] } },
292
+ ]);
293
+ const resolver = createForkContextResolver({
294
+ getSessionFile: () => parentSessionFile,
295
+ getLeafId: () => "assistant-1",
296
+ }, "fork", {
297
+ openSession: () => ({
298
+ createBranchedSession: () => childSessionFile,
299
+ }),
300
+ });
301
+
302
+ assert.equal(resolver.sessionFileForIndex(0), childSessionFile);
303
+ assert.equal(resolver.thinkingOverrideForIndex(0), undefined);
304
+ } finally {
305
+ fs.rmSync(tempDir, { recursive: true, force: true });
306
+ }
307
+ });
308
+
309
+ it("allows non-Anthropic signed thinking blocks in forked sessions", () => {
310
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-openai-thinking-"));
311
+ try {
312
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
313
+ const childSessionFile = path.join(tempDir, "child.jsonl");
314
+ writeMinimalSessionFile(parentSessionFile, "parent");
315
+ writeSessionJsonl(childSessionFile, [
316
+ { type: "session", version: 1, id: "child", timestamp: "2026-04-16T00:00:00.000Z", cwd: "/tmp", parentSession: parentSessionFile },
317
+ { type: "message", id: "assistant-1", parentId: null, timestamp: "2026-04-16T00:00:02.000Z", message: { role: "assistant", provider: "openai", api: "openai-responses", model: "openai/gpt-5.5", content: [{ type: "thinking", thinking: "reasoning summary", thinkingSignature: "rs_123" }, { type: "text", text: "answer" }] } },
318
+ ]);
319
+ const resolver = createForkContextResolver({
320
+ getSessionFile: () => parentSessionFile,
321
+ getLeafId: () => "assistant-1",
322
+ }, "fork", {
323
+ openSession: () => ({
324
+ createBranchedSession: () => childSessionFile,
325
+ }),
326
+ });
327
+
328
+ assert.equal(resolver.sessionFileForIndex(0), childSessionFile);
329
+ assert.equal(resolver.thinkingOverrideForIndex(0), undefined);
330
+ } finally {
331
+ fs.rmSync(tempDir, { recursive: true, force: true });
332
+ }
333
+ });
334
+
335
+ it("removes redacted thinking blocks before fallback-persisting a forked session", () => {
336
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-redacted-thinking-"));
337
+ try {
338
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
339
+ const childSessionFile = path.join(tempDir, "child.jsonl");
340
+ writeMinimalSessionFile(parentSessionFile, "parent");
341
+ const header = { type: "session", version: 1, id: "child", timestamp: "2026-04-16T00:00:00.000Z", cwd: "/tmp", parentSession: parentSessionFile };
342
+ const entries = [
343
+ { type: "message", id: "user-1", parentId: null, timestamp: "2026-04-16T00:00:01.000Z", message: { role: "user", content: "prompt" } },
344
+ { type: "message", id: "assistant-1", parentId: "user-1", timestamp: "2026-04-16T00:00:02.000Z", message: { role: "assistant", content: [{ type: "redacted_thinking", data: "encrypted" }, { type: "text", text: "answer" }] } },
345
+ ];
346
+ const resolver = createForkContextResolver({
347
+ getSessionFile: () => parentSessionFile,
348
+ getLeafId: () => "assistant-1",
349
+ }, "fork", {
350
+ openSession: () => ({
351
+ createBranchedSession: () => childSessionFile,
352
+ getHeader: () => header,
353
+ getEntries: () => entries,
354
+ }),
355
+ });
356
+
357
+ assert.equal(resolver.sessionFileForIndex(0), childSessionFile);
358
+ assert.equal(resolver.thinkingOverrideForIndex(0), "off");
359
+ const written = fs.readFileSync(childSessionFile, "utf-8").trim().split("\n").map((line) => JSON.parse(line));
360
+ assert.deepEqual(written[2].message.content, [{ type: "text", text: "answer" }]);
361
+ assert.equal(written[3].type, "thinking_level_change");
362
+ assert.equal(written[3].thinkingLevel, "off");
363
+ assert.equal(written[3].parentId, "assistant-1");
364
+ } finally {
365
+ fs.rmSync(tempDir, { recursive: true, force: true });
366
+ }
367
+ });
368
+
369
+ it("fails clearly when branch extraction returns a missing child file without fallback state", () => {
370
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-missing-child-"));
371
+ try {
372
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
373
+ const missingChildSessionFile = path.join(tempDir, "missing-child.jsonl");
374
+ writeMinimalSessionFile(parentSessionFile, "parent");
375
+ const resolver = createForkContextResolver({
376
+ getSessionFile: () => parentSessionFile,
377
+ getLeafId: () => "leaf-abc",
378
+ }, "fork", {
379
+ openSession: () => ({
380
+ createBranchedSession: () => missingChildSessionFile,
381
+ }),
382
+ });
383
+
384
+ assert.throws(
385
+ () => resolver.sessionFileForIndex(0),
386
+ /Failed to create forked subagent session: Session manager returned a forked session file that does not exist and cannot be persisted by fallback: .*missing-child\.jsonl/,
387
+ );
388
+ } finally {
389
+ fs.rmSync(tempDir, { recursive: true, force: true });
390
+ }
391
+ });
392
+
393
+ it("does not silently fallback to fresh when branch extraction fails", () => {
394
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-fork-no-path-"));
395
+ try {
396
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
397
+ writeMinimalSessionFile(parentSessionFile, "parent");
398
+ const resolver = createForkContextResolver({
399
+ getSessionFile: () => parentSessionFile,
400
+ getLeafId: () => "leaf-abc",
401
+ }, "fork", {
402
+ openSession: () => ({
403
+ createBranchedSession: () => undefined,
404
+ }),
405
+ });
406
+
407
+ assert.throws(
408
+ () => resolver.sessionFileForIndex(0),
409
+ /Failed to create forked subagent session: Session manager did not return a forked session file\./,
410
+ );
411
+ } finally {
412
+ fs.rmSync(tempDir, { recursive: true, force: true });
413
+ }
414
+ });
415
+ });
@@ -0,0 +1,126 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { Message } from "@earendil-works/pi-ai";
4
+ import { getFinalOutput } from "../../src/shared/utils.ts";
5
+
6
+ function assistantContent(content: unknown[]): Message {
7
+ return { role: "assistant", content } as unknown as Message;
8
+ }
9
+
10
+ describe("getFinalOutput", () => {
11
+ it("uses the last non-empty text part in the latest assistant message", () => {
12
+ const messages = [assistantContent([
13
+ { type: "text", text: "" },
14
+ { type: "text", text: "Summary" },
15
+ ])];
16
+
17
+ assert.equal(getFinalOutput(messages), "Summary");
18
+ });
19
+
20
+ it("prefers final text over progress text in a multi-part assistant message", () => {
21
+ const messages = [assistantContent([
22
+ { type: "text", text: "Working on the fix..." },
23
+ { type: "thinking", thinking: "Cursor shell: shell $ npm test" },
24
+ { type: "text", text: "Implemented: patch applied." },
25
+ ])];
26
+
27
+ assert.equal(getFinalOutput(messages), "Implemented: patch applied.");
28
+ });
29
+
30
+ it("falls back to an older assistant message when the latest text is whitespace-only", () => {
31
+ const messages = [
32
+ assistantContent([{ type: "text", text: "Earlier" }]),
33
+ assistantContent([{ type: "text", text: " \n\t " }]),
34
+ ];
35
+
36
+ assert.equal(getFinalOutput(messages), "Earlier");
37
+ });
38
+
39
+ it("falls back to an older assistant message when the latest assistant message is tool-only", () => {
40
+ const messages = [
41
+ assistantContent([{ type: "text", text: "Earlier" }]),
42
+ assistantContent([{ type: "toolCall", name: "read", arguments: { path: "README.md" } }]),
43
+ ];
44
+
45
+ assert.equal(getFinalOutput(messages), "Earlier");
46
+ });
47
+
48
+ it("prefers an earlier explicit acceptance report over later summary-only text", () => {
49
+ const report = [
50
+ "Done",
51
+ "```acceptance-report",
52
+ JSON.stringify({
53
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "verified" }],
54
+ changedFiles: ["src/file.ts"],
55
+ }),
56
+ "```",
57
+ ].join("\n");
58
+ const messages = [
59
+ assistantContent([{ type: "text", text: report }]),
60
+ assistantContent([{ type: "text", text: "Done." }]),
61
+ ];
62
+
63
+ assert.equal(getFinalOutput(messages), report);
64
+ });
65
+
66
+ it("prefers an earlier json-fenced acceptance report over later summary-only text", () => {
67
+ const report = [
68
+ "Done",
69
+ "```json",
70
+ JSON.stringify({
71
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "verified" }],
72
+ validationOutput: ["tests passed"],
73
+ }),
74
+ "```",
75
+ ].join("\n");
76
+ const messages = [
77
+ assistantContent([{ type: "text", text: report }]),
78
+ assistantContent([{ type: "text", text: "Done." }]),
79
+ ];
80
+
81
+ assert.equal(getFinalOutput(messages), report);
82
+ });
83
+
84
+ it("does not prefer provider-error acceptance reports", () => {
85
+ const messages = [
86
+ {
87
+ role: "assistant",
88
+ content: [{ type: "text", text: "```acceptance-report\n{}\n```" }],
89
+ stopReason: "error",
90
+ errorMessage: "provider transport failed",
91
+ } as unknown as Message,
92
+ assistantContent([{ type: "text", text: "Done." }]),
93
+ ];
94
+
95
+ assert.equal(getFinalOutput(messages), "Done.");
96
+ });
97
+
98
+ it("returns empty output when all assistant text is empty or whitespace-only", () => {
99
+ const messages = [
100
+ assistantContent([{ type: "text", text: "" }]),
101
+ assistantContent([{ type: "text", text: "\n\t " }]),
102
+ ];
103
+
104
+ assert.equal(getFinalOutput(messages), "");
105
+ });
106
+
107
+ it("does not use provider-error assistant text as fallback output", () => {
108
+ const messages = [
109
+ {
110
+ role: "assistant",
111
+ content: [{ type: "text", text: "temporary provider failure" }],
112
+ stopReason: "error",
113
+ errorMessage: "provider transport failed",
114
+ } as unknown as Message,
115
+ assistantContent([{ type: "text", text: "" }]),
116
+ ];
117
+
118
+ assert.equal(getFinalOutput(messages), "");
119
+ });
120
+
121
+ it("preserves surrounding whitespace on the selected non-empty text", () => {
122
+ const messages = [assistantContent([{ type: "text", text: " \n Summary \n " }])];
123
+
124
+ assert.equal(getFinalOutput(messages), " \n Summary \n ");
125
+ });
126
+ });