@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
@@ -0,0 +1,277 @@
1
+ import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
2
+ import type { AsyncJobState, AsyncJobStep, NestedRunSummary, NestedStepSummary, SubagentRunMode, SubagentState } from "../../shared/types.ts";
3
+
4
+ export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
5
+ export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
6
+ export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "SELESAI_SUBAGENT_ASYNC_JSON:";
7
+
8
+ const DEFAULT_MAX_RUNS = 20;
9
+ const DEFAULT_MAX_CHILDREN_PER_NODE = 8;
10
+ const DEFAULT_MAX_DEPTH = 3;
11
+ const DEFAULT_MAX_STRING_LENGTH = 160;
12
+ const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
13
+
14
+ export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
15
+ export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
16
+
17
+ export interface AsyncStatusSnapshotActivityV1 {
18
+ state?: string;
19
+ currentTool?: string;
20
+ lastActivityAt?: number;
21
+ currentToolStartedAt?: number;
22
+ turnCount?: number;
23
+ toolCount?: number;
24
+ }
25
+
26
+ export interface AsyncStatusSnapshotNodeV1 {
27
+ id: string;
28
+ kind: AsyncStatusSnapshotKind;
29
+ label: string;
30
+ state: AsyncStatusSnapshotState;
31
+ startedAt?: number;
32
+ updatedAt?: number;
33
+ endedAt?: number;
34
+ activity?: AsyncStatusSnapshotActivityV1;
35
+ children?: AsyncStatusSnapshotNodeV1[];
36
+ }
37
+
38
+ export interface AsyncStatusSnapshotCapsV1 {
39
+ maxRuns: number;
40
+ maxChildrenPerNode: number;
41
+ maxDepth: number;
42
+ maxStringLength: number;
43
+ maxSerializedBytes: number;
44
+ }
45
+
46
+ export interface AsyncStatusSnapshotOmittedV1 {
47
+ runs: number;
48
+ children: number;
49
+ byteLimitExceeded: boolean;
50
+ }
51
+
52
+ export interface AsyncStatusSnapshotV1 {
53
+ kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
54
+ version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
55
+ generatedAt: number;
56
+ caps: AsyncStatusSnapshotCapsV1;
57
+ omitted: AsyncStatusSnapshotOmittedV1;
58
+ runs: AsyncStatusSnapshotNodeV1[];
59
+ }
60
+
61
+ export interface AsyncStatusSnapshotOptions {
62
+ generatedAt?: number;
63
+ maxRuns?: number;
64
+ maxChildrenPerNode?: number;
65
+ maxDepth?: number;
66
+ maxStringLength?: number;
67
+ maxSerializedBytes?: number;
68
+ }
69
+
70
+ interface BuildContext {
71
+ caps: AsyncStatusSnapshotCapsV1;
72
+ omitted: AsyncStatusSnapshotOmittedV1;
73
+ }
74
+
75
+ function resolveCaps(options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotCapsV1 {
76
+ return {
77
+ maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
78
+ maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
79
+ maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
80
+ maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
81
+ maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES)),
82
+ };
83
+ }
84
+
85
+ function publicText(value: unknown, fallback: string, maxLength: number): string {
86
+ if (typeof value !== "string") return fallback;
87
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
88
+ return truncateDisplayText(normalized || fallback, maxLength);
89
+ }
90
+
91
+ function publicOptionalText(value: unknown, maxLength: number): string | undefined {
92
+ if (typeof value !== "string") return undefined;
93
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
94
+ return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
95
+ }
96
+
97
+ function publicTime(value: unknown): number | undefined {
98
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
99
+ }
100
+
101
+ function publicCount(value: unknown): number | undefined {
102
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
103
+ }
104
+
105
+ function normalizeState(value: string): AsyncStatusSnapshotState {
106
+ if (value === "completed") return "complete";
107
+ if (value === "pending") return "queued";
108
+ return value as AsyncStatusSnapshotState;
109
+ }
110
+
111
+ function terminalState(state: AsyncStatusSnapshotState): boolean {
112
+ return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
113
+ }
114
+
115
+ function kindForMode(mode: SubagentRunMode | undefined): AsyncStatusSnapshotKind {
116
+ return mode === "workflow" ? "workflow" : "subagent";
117
+ }
118
+
119
+ function labelForAgents(agents: readonly string[] | undefined, fallback: string, maxLength: number): string {
120
+ if (!agents?.length) return publicText(fallback, fallback, maxLength);
121
+ const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
122
+ const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
123
+ return truncateDisplayText(`${visible}${suffix}`, maxLength);
124
+ }
125
+
126
+ function activityFor(source: {
127
+ activityState?: unknown;
128
+ currentTool?: unknown;
129
+ lastActivityAt?: unknown;
130
+ currentToolStartedAt?: unknown;
131
+ turnCount?: unknown;
132
+ toolCount?: unknown;
133
+ }, ctx: BuildContext): AsyncStatusSnapshotActivityV1 | undefined {
134
+ const activity: AsyncStatusSnapshotActivityV1 = {
135
+ ...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
136
+ ...(publicOptionalText(source.currentTool, ctx.caps.maxStringLength) ? { currentTool: publicOptionalText(source.currentTool, ctx.caps.maxStringLength) } : {}),
137
+ ...(publicTime(source.lastActivityAt) !== undefined ? { lastActivityAt: publicTime(source.lastActivityAt) } : {}),
138
+ ...(publicTime(source.currentToolStartedAt) !== undefined ? { currentToolStartedAt: publicTime(source.currentToolStartedAt) } : {}),
139
+ ...(publicCount(source.turnCount) !== undefined ? { turnCount: publicCount(source.turnCount) } : {}),
140
+ ...(publicCount(source.toolCount) !== undefined ? { toolCount: publicCount(source.toolCount) } : {}),
141
+ };
142
+ return Object.keys(activity).length ? activity : undefined;
143
+ }
144
+
145
+ function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: BuildContext): void {
146
+ const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
147
+ children.push(...source.slice(0, remaining));
148
+ ctx.omitted.children += Math.max(0, source.length - remaining);
149
+ }
150
+
151
+ function buildStepNode(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
152
+ const state = normalizeState(step.status);
153
+ const updatedAt = publicTime(step.endedAt) ?? publicTime(step.lastActivityAt) ?? publicTime(step.startedAt);
154
+ const node: AsyncStatusSnapshotNodeV1 = {
155
+ id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
156
+ kind: "step",
157
+ label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
158
+ state,
159
+ ...(publicTime(step.startedAt) !== undefined ? { startedAt: publicTime(step.startedAt) } : {}),
160
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
161
+ ...(terminalState(state) && publicTime(step.endedAt) !== undefined ? { endedAt: publicTime(step.endedAt) } : {}),
162
+ ...(activityFor(step, ctx) ? { activity: activityFor(step, ctx) } : {}),
163
+ };
164
+ if (depth < ctx.caps.maxDepth && step.children?.length) {
165
+ const nested = step.children.map((child, childIndex) => buildNestedNode(child, childIndex, depth + 1, ctx));
166
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
167
+ appendBoundedChildren(bounded, nested, ctx);
168
+ if (bounded.length) node.children = bounded;
169
+ } else if (step.children?.length) {
170
+ ctx.omitted.children += step.children.length;
171
+ }
172
+ return node;
173
+ }
174
+
175
+ function buildNestedNode(child: NestedRunSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
176
+ const state = normalizeState(child.state);
177
+ const updatedAt = publicTime(child.lastUpdate) ?? publicTime(child.endedAt) ?? publicTime(child.lastActivityAt) ?? publicTime(child.startedAt);
178
+ const node: AsyncStatusSnapshotNodeV1 = {
179
+ id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
180
+ kind: kindForMode(child.mode),
181
+ label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
182
+ state,
183
+ ...(publicTime(child.startedAt) !== undefined ? { startedAt: publicTime(child.startedAt) } : {}),
184
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
185
+ ...(terminalState(state) && publicTime(child.endedAt) !== undefined ? { endedAt: publicTime(child.endedAt) } : {}),
186
+ ...(activityFor(child, ctx) ? { activity: activityFor(child, ctx) } : {}),
187
+ };
188
+ if (depth < ctx.caps.maxDepth) {
189
+ const nestedSteps = child.steps?.map((step, stepIndex) => buildStepNode(step, stepIndex, depth + 1, ctx)) ?? [];
190
+ const nestedChildren = child.children?.map((nested, childIndex) => buildNestedNode(nested, childIndex, depth + 1, ctx)) ?? [];
191
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
192
+ appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
193
+ if (bounded.length) node.children = bounded;
194
+ } else {
195
+ ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
196
+ }
197
+ return node;
198
+ }
199
+
200
+ function buildRunNode(job: AsyncJobState, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
201
+ const state = normalizeState(job.status);
202
+ const updatedAt = publicTime(job.updatedAt) ?? publicTime(job.startedAt);
203
+ const node: AsyncStatusSnapshotNodeV1 = {
204
+ id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
205
+ kind: kindForMode(job.mode),
206
+ label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
207
+ state,
208
+ ...(publicTime(job.startedAt) !== undefined ? { startedAt: publicTime(job.startedAt) } : {}),
209
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
210
+ ...(terminalState(state) && updatedAt !== undefined ? { endedAt: updatedAt } : {}),
211
+ ...(activityFor(job, ctx) ? { activity: activityFor(job, ctx) } : {}),
212
+ };
213
+ if (ctx.caps.maxDepth > 0) {
214
+ const stepChildren = job.steps?.map((step, index) => buildStepNode(step, step.index ?? index, 1, ctx)) ?? [];
215
+ const nestedChildren = job.nestedChildren?.map((child, index) => buildNestedNode(child, index, 1, ctx)) ?? [];
216
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
217
+ appendBoundedChildren(bounded, [...stepChildren, ...nestedChildren], ctx);
218
+ if (bounded.length) node.children = bounded;
219
+ } else {
220
+ ctx.omitted.children += (job.steps?.length ?? 0) + (job.nestedChildren?.length ?? 0);
221
+ }
222
+ return node;
223
+ }
224
+
225
+ function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
226
+ return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
227
+ }
228
+
229
+ function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
230
+ while (snapshot.runs.length > 0 && snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) {
231
+ snapshot.runs.pop();
232
+ snapshot.omitted.runs += 1;
233
+ snapshot.omitted.byteLimitExceeded = true;
234
+ }
235
+ if (snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) snapshot.omitted.byteLimitExceeded = true;
236
+ }
237
+
238
+ export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
239
+ const caps = resolveCaps(options);
240
+ const ctx: BuildContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
241
+ const sorted = [...jobs].sort((left, right) => {
242
+ const leftUpdated = left.updatedAt ?? left.startedAt ?? 0;
243
+ const rightUpdated = right.updatedAt ?? right.startedAt ?? 0;
244
+ return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
245
+ });
246
+ ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
247
+ const snapshot: AsyncStatusSnapshotV1 = {
248
+ kind: ASYNC_STATUS_SNAPSHOT_KIND,
249
+ version: ASYNC_STATUS_SNAPSHOT_VERSION,
250
+ generatedAt: options.generatedAt ?? Date.now(),
251
+ caps,
252
+ omitted: ctx.omitted,
253
+ runs: sorted.slice(0, caps.maxRuns).map((job) => buildRunNode(job, ctx)),
254
+ };
255
+ enforceByteLimit(snapshot);
256
+ return snapshot;
257
+ }
258
+
259
+ export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
260
+ if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
261
+ const jobs = new Map<string, AsyncJobState>();
262
+ for (const job of state.asyncJobs.values()) {
263
+ if (job.sessionId === sessionId) jobs.set(job.asyncId, job);
264
+ }
265
+ for (const job of state.fleetJobs?.values() ?? []) {
266
+ if (job.sessionId === sessionId && !jobs.has(job.asyncId)) jobs.set(job.asyncId, job);
267
+ }
268
+ return [...jobs.values()];
269
+ }
270
+
271
+ export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
272
+ return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
273
+ }
274
+
275
+ export function encodeAsyncStatusSnapshotWidget(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): string[] {
276
+ return [`${ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX}${JSON.stringify(buildAsyncStatusSnapshot(jobs, options))}`];
277
+ }
@@ -12,7 +12,7 @@ import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-grou
12
12
  import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
13
13
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
14
14
  import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
15
- import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
15
+ import { ACTIVE_RUN_INDEX_DIR, DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, activeRunMarkerAgeMs, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
16
16
 
17
17
  interface AsyncRunStepSummary {
18
18
  index: number;
@@ -25,6 +25,7 @@ interface AsyncRunStepSummary {
25
25
  structured?: boolean;
26
26
  checkpoint?: ChainCheckpointState;
27
27
  status: AsyncJobStep["status"];
28
+ runner?: AsyncJobStep["runner"];
28
29
  activityState?: ActivityState;
29
30
  lastActivityAt?: number;
30
31
  currentTool?: string;
@@ -120,6 +121,8 @@ export interface AsyncRunSummary {
120
121
  interface AsyncRunListOptions {
121
122
  states?: Array<AsyncRunSummary["state"]>;
122
123
  sessionId?: string;
124
+ /** Accept runs owned by any of these session identities (union). */
125
+ sessionIds?: string[];
123
126
  limit?: number;
124
127
  /** Limits status-file reads after candidates are ordered by status mtime. */
125
128
  entryLimit?: number;
@@ -128,6 +131,7 @@ interface AsyncRunListOptions {
128
131
  now?: () => number;
129
132
  reconcile?: boolean;
130
133
  runId?: string;
134
+ includeNested?: boolean;
131
135
  }
132
136
 
133
137
  function getErrorMessage(error: unknown): string {
@@ -253,6 +257,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
253
257
  ...(step.structured ? { structured: step.structured } : {}),
254
258
  ...(step.checkpoint ? { checkpoint: step.checkpoint } : {}),
255
259
  status: step.status,
260
+ ...(step.runner ? { runner: step.runner } : {}),
256
261
  ...(stepActivityState ? { activityState: stepActivityState } : {}),
257
262
  ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
258
263
  ...(step.currentTool ? { currentTool: step.currentTool } : {}),
@@ -383,6 +388,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
383
388
  && options.states !== undefined
384
389
  && options.states.length > 0
385
390
  && options.states.every(isActiveAsyncState);
391
+ const includeNested = options.includeNested !== false;
386
392
  try {
387
393
  if (options.runId !== undefined) {
388
394
  const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
@@ -443,6 +449,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
443
449
  // entirely when no active runs match.
444
450
  let nestedRouteIndex: Map<string, NestedRoute> | undefined;
445
451
  const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
452
+ if (!includeNested) return undefined;
446
453
  if (usedActiveIndex) return findNestedRouteForRootId(rootRunId);
447
454
  if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
448
455
  return nestedRouteIndex.get(rootRunId);
@@ -459,7 +466,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
459
466
  }
460
467
  if (usedActiveIndex && !isActiveAsyncState(status.state)) {
461
468
  const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
462
- if (processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
469
+ if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) releaseActiveRunIndex(asyncDir);
463
470
  }
464
471
  if (status.displayDismissedAt !== undefined) continue;
465
472
  // Filter before the nested-route lookup: the lookup builds an index over
@@ -468,9 +475,10 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
468
475
  // match.
469
476
  if (allowedStates && !allowedStates.has(status.state)) continue;
470
477
  if (options.sessionId && status.sessionId !== options.sessionId) continue;
478
+ if (options.sessionIds && (status.sessionId === undefined || !options.sessionIds.includes(status.sessionId))) continue;
471
479
  const nestedWarnings: string[] = [];
472
480
  let nestedRoute: NestedRoute | undefined;
473
- if (options.reconcile !== false) {
481
+ if (options.reconcile !== false && includeNested) {
474
482
  try {
475
483
  nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
476
484
  if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
2
+ import { resultFilePath } from "./result-files.ts";
3
3
  import type { AcceptanceLedger, AsyncStatus, CostSummary, ModelAttempt } from "../../shared/types.ts";
4
4
  import { readStatus } from "../../shared/utils.ts";
5
5
 
@@ -195,5 +195,5 @@ export async function waitForImportedAsyncRoot(
195
195
  }
196
196
 
197
197
  export function resolveAsyncRootResultPath(resultsDir: string, runId: string): string {
198
- return path.join(resultsDir, `${runId}.json`);
198
+ return resultFilePath(resultsDir, runId);
199
199
  }
@@ -9,6 +9,8 @@ const ARCHIVE_VERSION = 1;
9
9
  const ARCHIVE_TEXT_LIMIT_BYTES = 64 * 1024;
10
10
  const REPLAY_DIR_NAME = "completion-replay";
11
11
  const ARCHIVE_DIR_NAME = "output-archives";
12
+ const CLEANUP_INTERVAL_MS = 60_000;
13
+ const lastCleanupByResultsDir = new Map<string, number>();
12
14
 
13
15
  export interface CompletionArchiveEntry {
14
16
  agent?: string;
@@ -194,7 +196,7 @@ export function writeCompletionReplay(input: {
194
196
  archivePath,
195
197
  };
196
198
  writePrivateAtomicJson(completionReplayPath(input.resultsDir, input.runId), record);
197
- cleanupCompletionReplay(input.resultsDir, input.now, input.ttlMs);
199
+ cleanupCompletionReplayIfDue(input.resultsDir, input.now, input.ttlMs);
198
200
  return record;
199
201
  }
200
202
 
@@ -233,6 +235,14 @@ export function readCompletionArchive(archivePath: string): CompletionArchive |
233
235
  }
234
236
  }
235
237
 
238
+ export function cleanupCompletionReplayIfDue(resultsDir: string, now: number, maxAgeMs: number, intervalMs = CLEANUP_INTERVAL_MS): boolean {
239
+ const last = lastCleanupByResultsDir.get(resultsDir);
240
+ if (last !== undefined && now - last < intervalMs) return false;
241
+ lastCleanupByResultsDir.set(resultsDir, now);
242
+ cleanupCompletionReplay(resultsDir, now, maxAgeMs);
243
+ return true;
244
+ }
245
+
236
246
  /** Opportunistically remove expired replay and orphan archive files without affecting delivery. */
237
247
  export function cleanupCompletionReplay(resultsDir: string, now: number, maxAgeMs: number): void {
238
248
  const replayDir = path.join(resultsDir, REPLAY_DIR_NAME);
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
+ import { safeTerminalText } from "../../shared/display-text.ts";
4
5
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
5
6
  import { formatActivityLabel } from "../../shared/status-format.ts";
6
7
  import {
@@ -196,7 +197,7 @@ function readSessionTranscriptTail(sessionFile: string, maxLines: number, truste
196
197
  try {
197
198
  const parsed = JSON.parse(line) as unknown;
198
199
  const messageLine = sessionMessageLine(parsed);
199
- if (messageLine) lines.push(messageLine);
200
+ if (messageLine) lines.push(...messageLine.split(/\r?\n/));
200
201
  } catch {
201
202
  malformed++;
202
203
  }
@@ -410,6 +411,18 @@ function appendKnownArtifacts(lines: string[], input: { outputPaths: string[]; s
410
411
  for (const artifact of artifacts) lines.push(` ${artifact}`);
411
412
  }
412
413
 
414
+ /**
415
+ * Sanitizes each assembled line independently.
416
+ *
417
+ * Sanitizing the joined response would let a single binary fragment in child
418
+ * output collapse the whole transcript, including run state, artifact paths, and
419
+ * warnings, into the binary placeholder. Per-line sanitization keeps that damage
420
+ * to the offending line.
421
+ */
422
+ function safeTranscriptLines(lines: string[]): string {
423
+ return lines.map((line) => safeTerminalText(line)).join("\n");
424
+ }
425
+
413
426
  function appendTranscriptBody(lines: string[], sourceLabel: string, sourceLines: string[], truncated: boolean): void {
414
427
  lines.push(`${sourceLabel}${truncated ? " (tail truncated)" : ""}:`);
415
428
  if (sourceLines.length === 0) {
@@ -430,6 +443,8 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
430
443
  : uniqueStrings([runOutputPath]);
431
444
  const sessionFile = selected.index !== undefined ? selected.step?.sessionFile : status.sessionFile;
432
445
  const eventsPath = path.join(asyncDir, "events.jsonl");
446
+ // Synthesized output paths can name files that were never written. Keep the unfiltered paths
447
+ // below so a present-but-unreadable file still reports its read error.
433
448
 
434
449
  const context = contextModeLabel(status.context ?? summarizeContextModes((status.steps ?? []).map((step) => step.context)));
435
450
  const lines = [
@@ -439,7 +454,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
439
454
  stepStateLine(status.mode, selected.index, selected.step),
440
455
  selected.hint,
441
456
  ].filter((line): line is string => Boolean(line));
442
- appendKnownArtifacts(lines, { outputPaths, sessionFile, eventsPath: fs.existsSync(eventsPath) ? eventsPath : undefined, logPath: fs.existsSync(logPath) ? logPath : undefined });
457
+ appendKnownArtifacts(lines, { outputPaths: outputPaths.filter((outputPath) => fs.existsSync(outputPath)), sessionFile, eventsPath: fs.existsSync(eventsPath) ? eventsPath : undefined, logPath: fs.existsSync(logPath) ? logPath : undefined });
443
458
 
444
459
  const warnings: string[] = [];
445
460
  let transcriptLines: string[] = [];
@@ -470,7 +485,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
470
485
  for (const warning of warnings) lines.push(` ${warning}`);
471
486
  }
472
487
  appendTranscriptBody(lines, transcriptSource, transcriptLines, truncated);
473
- return lines.join("\n");
488
+ return safeTranscriptLines(lines);
474
489
  }
475
490
 
476
491
  export function formatNestedRunTranscript(run: NestedRunSummary, options: TranscriptOptions = {}): string {
@@ -488,7 +503,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
488
503
  appendKnownArtifacts(lines, { outputPaths: [], sessionFile: run.sessionFile });
489
504
  if (!run.sessionFile) {
490
505
  appendTranscriptBody(lines, "Transcript tail", [], false);
491
- return lines.join("\n");
506
+ return safeTranscriptLines(lines);
492
507
  }
493
508
  const sessionTail = readSessionTranscriptTail(run.sessionFile, lineLimit, options.sessionRoots ?? []);
494
509
  if (sessionTail.warnings.length) {
@@ -496,7 +511,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
496
511
  for (const warning of sessionTail.warnings) lines.push(` ${warning}`);
497
512
  }
498
513
  appendTranscriptBody(lines, `Session transcript tail from ${run.sessionFile}`, sessionTail.lines, false);
499
- return lines.join("\n");
514
+ return safeTranscriptLines(lines);
500
515
  }
501
516
 
502
517
  export function formatAsyncResultTranscript(data: {
@@ -536,5 +551,5 @@ export function formatAsyncResultTranscript(data: {
536
551
  ].filter((line): line is string => Boolean(line));
537
552
  appendKnownArtifacts(lines, { outputPaths: [], sessionFile, resultPath });
538
553
  appendTranscriptBody(lines, "Result transcript tail", transcriptLines.filter((line) => line.trim()), output.split(/\r?\n/).length > lineLimit);
539
- return lines.join("\n");
554
+ return safeTranscriptLines(lines);
540
555
  }
@@ -239,7 +239,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
239
239
 
240
240
  export default function registerSubagentNotify(
241
241
  pi: ExtensionAPI,
242
- state: Pick<SubagentState, "currentSessionId">,
242
+ state: Pick<SubagentState, "currentSessionId" | "sessionLineage">,
243
243
  options: RegisterSubagentNotifyOptions = {},
244
244
  ): CompletionNotifier {
245
245
  const seen = new Map<string, number>();
@@ -250,6 +250,9 @@ export default function registerSubagentNotify(
250
250
  const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
251
251
  let disposed = false;
252
252
 
253
+ const ownsSession = (sessionId: string) =>
254
+ sessionId === state.currentSessionId || state.sessionLineage?.includes(sessionId) === true;
255
+
253
256
  const settle = (items: PendingCompletion[], accepted: boolean) => {
254
257
  for (const item of items) {
255
258
  pending.delete(item.key);
@@ -274,7 +277,7 @@ export default function registerSubagentNotify(
274
277
  };
275
278
 
276
279
  const deliver = (result: CompletionNotification): Promise<boolean> => {
277
- if (disposed || typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return Promise.resolve(false);
280
+ if (disposed || typeof result.sessionId !== "string" || !ownsSession(result.sessionId)) return Promise.resolve(false);
278
281
  if (result.intercomDelivered === true) return Promise.resolve(true);
279
282
  const key = buildCompletionKey(result, "notify");
280
283
  const seenAt = seen.get(key);