@selesai/code 0.8.6 → 0.8.8

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 (234) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.js +11 -0
  4. package/dist/cli/args.test.d.ts +1 -0
  5. package/dist/cli/args.test.js +24 -0
  6. package/dist/core/agent-session.d.ts +4 -1
  7. package/dist/core/agent-session.js +18 -7
  8. package/dist/core/experimental.d.ts +4 -0
  9. package/dist/core/experimental.js +4 -0
  10. package/dist/core/extensions/types.d.ts +6 -0
  11. package/dist/core/keybindings.d.ts +24 -0
  12. package/dist/core/model-config.d.ts +5 -0
  13. package/dist/core/model-config.js +1 -0
  14. package/dist/core/project-trust.js +2 -2
  15. package/dist/core/provider-composer.js +17 -4
  16. package/dist/core/sdk.d.ts +5 -3
  17. package/dist/core/sdk.js +2 -1
  18. package/dist/core/session-manager.js +2 -2
  19. package/dist/core/settings-manager.d.ts +8 -0
  20. package/dist/core/settings-manager.js +15 -0
  21. package/dist/core/system-prompt.js +3 -9
  22. package/dist/core/system-prompt.test.js +6 -0
  23. package/dist/core/tools/bash.js +2 -0
  24. package/dist/core/tools/edit.js +2 -0
  25. package/dist/core/tools/find.js +1 -1
  26. package/dist/core/tools/grep.js +1 -1
  27. package/dist/core/tools/index.d.ts +1 -0
  28. package/dist/core/tools/index.js +1 -0
  29. package/dist/core/tools/read.js +2 -0
  30. package/dist/core/tools/schema-prune.d.ts +16 -0
  31. package/dist/core/tools/schema-prune.js +39 -0
  32. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  33. package/dist/core/tools/schema-prune.test.js +111 -0
  34. package/dist/core/tools/write.js +2 -0
  35. package/dist/defaults/settings.json +1 -0
  36. package/dist/extensions/model-prompt-injector/config.json +10 -0
  37. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  38. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  39. package/dist/extensions/package.json +1 -0
  40. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  41. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  42. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  43. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  44. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  45. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  46. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  47. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  48. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  49. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  50. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  51. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  52. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  53. package/dist/extensions/pi-subagents/install.mjs +2 -2
  54. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  55. package/dist/extensions/pi-subagents/package.json +1 -1
  56. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  57. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  58. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  59. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  60. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  61. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  62. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  63. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  64. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  65. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  66. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  67. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  68. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  69. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  70. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  71. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  72. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  73. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  74. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  75. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  76. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  77. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  78. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  80. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  82. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  83. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  84. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  85. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  86. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  87. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  88. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  89. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  90. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +187 -41
  91. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  92. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  93. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  94. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  95. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  96. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  97. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  98. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  100. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  101. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  103. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  105. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  106. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  109. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  110. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  112. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  113. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  114. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  116. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  117. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  118. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  119. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  120. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  121. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  122. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  127. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  128. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  129. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  130. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  131. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  133. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  134. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  135. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  136. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  137. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  138. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  139. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  140. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  141. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +395 -98
  142. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  143. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  144. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  145. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  146. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  148. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  149. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  151. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  152. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  154. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  155. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  156. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  157. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  158. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  159. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  160. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  161. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  162. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  163. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  164. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  165. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  166. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  167. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  168. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  169. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  170. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  171. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  172. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  173. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  175. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  176. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  177. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  178. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  179. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  180. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  181. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  182. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  183. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  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-chat-progress.test.ts +94 -1
  193. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  194. package/dist/extensions/question/index.ts +21 -6
  195. package/dist/extensions/question/question-list.ts +7 -1
  196. package/dist/extensions/question/row-layout.ts +3 -0
  197. package/dist/extensions/question/tests/exp.test.ts +70 -0
  198. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  199. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  200. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  201. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  202. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  203. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  204. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  205. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  206. package/dist/extensions/tokenin-onboarding.ts +1 -1
  207. package/dist/index.d.ts +1 -1
  208. package/dist/main.js +4 -0
  209. package/dist/modes/interactive/components/model-selector.js +2 -1
  210. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  211. package/dist/modes/interactive/components/settings-selector.js +10 -0
  212. package/dist/modes/interactive/components/tool-execution.js +10 -1
  213. package/dist/modes/interactive/interactive-mode.d.ts +8 -2
  214. package/dist/modes/interactive/interactive-mode.js +81 -25
  215. package/dist/modes/interactive/model-catalog-refresh.d.ts +6 -0
  216. package/dist/modes/interactive/model-catalog-refresh.js +33 -0
  217. package/dist/modes/interactive/theme/dark.json +2 -0
  218. package/dist/modes/interactive/theme/light.json +2 -0
  219. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  220. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  221. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  222. package/dist/modes/interactive/theme/theme.d.ts +5 -3
  223. package/dist/modes/interactive/theme/theme.js +12 -2
  224. package/dist/skills/pi-subagents/SKILL.md +1 -1
  225. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  226. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  227. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  228. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  229. package/dist/utils/abort.d.ts +4 -0
  230. package/dist/utils/abort.js +47 -0
  231. package/dist/utils/tools-manager.d.ts +5 -1
  232. package/dist/utils/tools-manager.js +12 -21
  233. package/docs/settings.md +6 -0
  234. package/package.json +6 -6
@@ -22,6 +22,7 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
22
22
  import { resolveWatchPath } from "../../shared/utils.ts";
23
23
  import { recordWaitCompletion } from "./wait-completions.ts";
24
24
  import { MISSION_BINDING_FILE, syncMissionFromAsyncCompletion } from "../../missions/lifecycle.ts";
25
+ import { missionObserverResultCandidateFiles, promotePendingResultFile, removeMissionObserverIndex, removeResultIndex, resultCandidateFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForMissionObserverRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writeResultIndexForData } from "./result-files.ts";
25
26
  import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
26
27
 
27
28
  const WATCHER_RESTART_DELAY_MS = 3000;
@@ -50,6 +51,8 @@ type ResultWatcherDeps = {
50
51
  parseResult?: (raw: string) => ResultFileData;
51
52
  /** External grouped-result transport. Disable when native completion notifications own delivery. */
52
53
  deliverIntercomResults?: boolean;
54
+ /** Coalesces result-file events. Tests can lower this without changing retry timing. */
55
+ coalesceDelayMs?: number;
53
56
  };
54
57
 
55
58
  type ResultFileChild = {
@@ -79,6 +82,7 @@ type ResultFileData = CompletionNotification & {
79
82
  asyncDir?: string;
80
83
  intercomTarget?: string;
81
84
  parallelHandoff?: ParallelHandoffReference;
85
+ notificationDeliveredAt?: unknown;
82
86
  };
83
87
 
84
88
  type ResultFileIdentity = {
@@ -134,6 +138,16 @@ function shouldPoll(error: unknown): boolean {
134
138
  return code === "EMFILE" || code === "ENOSPC";
135
139
  }
136
140
 
141
+ function hasDeliveredNotification(data: ResultFileData): boolean {
142
+ return typeof data.notificationDeliveredAt === "number" && Number.isFinite(data.notificationDeliveredAt);
143
+ }
144
+
145
+ function markDeliveredNotification(resultPath: string, data: ResultFileData, runId: string, now: number): ResultFileData {
146
+ const marked = { ...data, runId, notificationDeliveredAt: now };
147
+ writeAsyncResultFile(resultPath, marked);
148
+ return marked;
149
+ }
150
+
137
151
  /**
138
152
  * Watches persisted async results for the session currently owned by this
139
153
  * runtime. `stopResultWatcher()` revokes ownership before closing resources,
@@ -194,16 +208,55 @@ export function createResultWatcher(
194
208
  state.resultFileCoalescer.schedule(file, delayMs);
195
209
  };
196
210
 
197
- const inspectResult = (file: string): ResultFileIdentity | undefined => {
198
- const resultPath = path.join(resultsDir, file);
211
+ const publicResultPath = (file: string): string => path.join(resultsDir, file);
212
+ const publicResultFileExists = (file: string): boolean => {
213
+ try {
214
+ return fsApi.statSync(publicResultPath(file)).isFile();
215
+ } catch (error) {
216
+ if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
217
+ return false;
218
+ }
219
+ };
220
+ const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
221
+ if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
222
+ const runId = file.replace(/\.json$/i, "");
223
+ const sessionResult = state.currentSessionId ? resultPayloadPathForSessionRun(resultsDir, state.currentSessionId, runId) : undefined;
224
+ if (sessionResult) return sessionResult;
225
+ const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
226
+ if (observerResult) return observerResult;
227
+ if (observed?.has(runId)) {
228
+ const indexedResult = resultPayloadPathForIndexedRun(resultsDir, runId);
229
+ if (indexedResult) return indexedResult;
230
+ }
231
+ return publicResultFileExists(file) ? publicResultPath(file) : undefined;
232
+ };
233
+ const resultSignature = (file: string, observed?: ReadonlySet<string>): string | undefined => {
234
+ const resultPath = resultPayloadPath(file, observed);
235
+ if (!resultPath) {
236
+ identityCache.delete(file);
237
+ return undefined;
238
+ }
199
239
  try {
200
240
  const stat = fsApi.statSync(resultPath);
201
- const signature = `${stat.size}:${stat.mtimeMs}`;
241
+ if (!stat.isFile()) return undefined;
242
+ return `${resultPath}:${stat.size}:${stat.mtimeMs}`;
243
+ } catch (error) {
244
+ identityCache.delete(file);
245
+ if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
246
+ return undefined;
247
+ }
248
+ };
249
+ const inspectResult = (file: string, knownSignature?: string, observed?: ReadonlySet<string>): { identity: ResultFileIdentity; signature: string } | undefined => {
250
+ const resultPath = resultPayloadPath(file, observed);
251
+ if (!resultPath) return undefined;
252
+ try {
253
+ const signature = knownSignature ?? resultSignature(file, observed);
254
+ if (!signature) return undefined;
202
255
  const cached = identityCache.get(file);
203
- if (cached?.signature === signature) return cached.identity;
256
+ if (cached?.signature === signature) return { identity: cached.identity, signature };
204
257
  const identity = resultFileIdentity(fsApi.readFileSync(resultPath, "utf-8"), file);
205
258
  identityCache.set(file, { signature, identity });
206
- return identity;
259
+ return { identity, signature };
207
260
  } catch (error) {
208
261
  identityCache.delete(file);
209
262
  if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
@@ -220,9 +273,10 @@ export function createResultWatcher(
220
273
  }
221
274
  };
222
275
 
223
- const shouldProcessResult = (file: string, observed?: ReadonlySet<string>): boolean => {
224
- const identity = inspectResult(file);
225
- if (!identity) return false;
276
+ const shouldProcessResult = (file: string, observed?: ReadonlySet<string>, knownSignature?: string): boolean => {
277
+ const inspected = inspectResult(file, knownSignature, observed);
278
+ if (!inspected) return false;
279
+ const { identity } = inspected;
226
280
  // Missing identity stays on the normal parser path so malformed or legacy
227
281
  // files keep their existing diagnostics and compatibility behavior.
228
282
  if (!identity.sessionId) return true;
@@ -233,32 +287,80 @@ export function createResultWatcher(
233
287
  return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
234
288
  };
235
289
 
290
+ const removeDeliveredResult = (file: string, sessionId: string, runId: string, toolCallId: string | undefined): boolean => {
291
+ try {
292
+ if (publicResultFileExists(file)) fsApi.unlinkSync(publicResultPath(file));
293
+ identityCache.delete(file);
294
+ removeResultIndex(resultsDir, sessionId, runId, toolCallId);
295
+ return true;
296
+ } catch (error) {
297
+ if (!isNotFound(error)) {
298
+ console.error(`Failed to remove delivered subagent result '${publicResultPath(file)}'; will retry:`, error);
299
+ return false;
300
+ }
301
+ return true;
302
+ }
303
+ };
236
304
  const handleResult = async (file: string, triggerTurn: boolean) => {
237
- const resultPath = path.join(resultsDir, file);
238
- if (processing.has(file) || !fsApi.existsSync(resultPath)) return;
239
- if (!shouldProcessResult(file)) return;
305
+ if (processing.has(file)) return;
306
+ let observed: ReadonlySet<string> | undefined;
307
+ if (!shouldProcessResult(file)) {
308
+ const runId = file === path.basename(file) && file.endsWith(".json") ? file.replace(/\.json$/i, "") : undefined;
309
+ observed = observedRunIds();
310
+ if (!runId || !observed.has(runId) || !shouldProcessResult(file, observed)) return;
311
+ }
240
312
  processing.add(file);
313
+ let resultPath = publicResultPath(file);
241
314
  try {
242
- const data = parseResult(fsApi.readFileSync(resultPath, "utf-8"));
315
+ const payloadPath = resultPayloadPath(file, observed);
316
+ if (!payloadPath) return;
317
+ resultPath = payloadPath;
318
+ let raw = fsApi.readFileSync(resultPath, "utf-8");
319
+ let identity = resultFileIdentity(raw, file);
320
+ if (identity.sessionId && identity.runId) {
321
+ const pendingState = promotePendingResultFile(resultsDir, identity.sessionId, identity.runId, file);
322
+ if (pendingState === "promoted") {
323
+ identityCache.delete(file);
324
+ resultPath = publicResultPath(file);
325
+ raw = fsApi.readFileSync(resultPath, "utf-8");
326
+ identity = resultFileIdentity(raw, file);
327
+ } else if (pendingState === "pending") {
328
+ const pendingPath = resultPayloadPathForSessionRun(resultsDir, identity.sessionId, identity.runId);
329
+ if (!pendingPath) {
330
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
331
+ return;
332
+ }
333
+ resultPath = pendingPath;
334
+ raw = fsApi.readFileSync(resultPath, "utf-8");
335
+ identity = resultFileIdentity(raw, file);
336
+ }
337
+ }
338
+ let data = parseResult(raw);
243
339
  if (typeof data.sessionId !== "string" || !data.sessionId) return;
340
+ const sessionId = data.sessionId;
244
341
  const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
342
+ const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
343
+ let observerSucceeded = true;
245
344
  try {
246
345
  syncMissionFromAsyncCompletion({ ...data, runId });
247
346
  } catch (error) {
347
+ observerSucceeded = false;
248
348
  console.error(`Mission completion sync failed for '${resultPath}':`, error);
249
349
  }
250
350
  try {
251
351
  deps.observeCompletion?.({ ...data, runId });
252
352
  } catch (error) {
353
+ observerSucceeded = false;
253
354
  console.error(`Completion observer failed for '${resultPath}':`, error);
254
355
  }
356
+ if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
255
357
  const epoch = deliveryEpoch;
256
- if (!ownsSession(data.sessionId, epoch)) return;
358
+ if (!ownsSession(sessionId, epoch)) return;
257
359
  // Recorded before dedupe and before the unlink below so subagent_wait can
258
360
  // use the in-memory record or its bounded durable replay after cleanup.
259
361
  recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
260
362
  resultsDir,
261
- sessionId: data.sessionId,
363
+ sessionId,
262
364
  });
263
365
  const hasExplicitNestedChildren = data.nestedChildren !== undefined;
264
366
  let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
@@ -273,20 +375,17 @@ export function createResultWatcher(
273
375
  }
274
376
 
275
377
  const completionKey = buildCompletionKey(data, `result:${file}`);
378
+ const alreadyDelivered = hasDeliveredNotification(data);
276
379
  const lastSeenAt = state.completionSeen.get(completionKey);
277
380
  if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
278
381
  state.completionSeen.delete(completionKey);
279
382
  } else if (lastSeenAt !== undefined) {
280
- if (!ownsSession(data.sessionId, epoch) || !fsApi.existsSync(resultPath)) return;
281
- try {
282
- fsApi.unlinkSync(resultPath);
283
- identityCache.delete(file);
284
- } catch (error) {
285
- if (!isNotFound(error)) {
286
- console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
287
- scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
288
- }
383
+ if (!observerSucceeded) {
384
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
385
+ return;
289
386
  }
387
+ if (!ownsSession(sessionId, epoch)) return;
388
+ if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
290
389
  return;
291
390
  }
292
391
 
@@ -334,6 +433,17 @@ export function createResultWatcher(
334
433
  };
335
434
  }), nestedChildren);
336
435
 
436
+ if (alreadyDelivered) {
437
+ markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
438
+ if (!observerSucceeded) {
439
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
440
+ return;
441
+ }
442
+ if (!ownsSession(sessionId, epoch)) return;
443
+ if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
444
+ return;
445
+ }
446
+
337
447
  const intercomTarget = data.intercomTarget?.trim();
338
448
  let intercomDelivered = false;
339
449
  if (deliverIntercomResults && intercomTarget && triggerTurn) {
@@ -350,7 +460,7 @@ export function createResultWatcher(
350
460
  asyncDir: data.asyncDir,
351
461
  ...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
352
462
  }));
353
- if (!ownsSession(data.sessionId, epoch)) return;
463
+ if (!ownsSession(sessionId, epoch)) return;
354
464
  if (!intercomDelivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
355
465
  }
356
466
 
@@ -374,11 +484,19 @@ export function createResultWatcher(
374
484
  })) : [],
375
485
  } : {}),
376
486
  });
377
- if (!ownsSession(data.sessionId, epoch)) return;
487
+ if (!ownsSession(sessionId, epoch)) return;
378
488
  if (!accepted) {
379
489
  scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
380
490
  return;
381
491
  }
492
+ try {
493
+ data = markDeliveredNotification(publicResultPath(file), data, runId, Date.now());
494
+ identityCache.delete(file);
495
+ } catch (error) {
496
+ console.error(`Failed to mark subagent result notification delivered for '${resultPath}'; will retry:`, error);
497
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
498
+ return;
499
+ }
382
500
  markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
383
501
  try {
384
502
  pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
@@ -403,16 +521,12 @@ export function createResultWatcher(
403
521
  } catch (error) {
404
522
  console.error(`Completion observer failed for '${resultPath}':`, error);
405
523
  }
406
- if (!ownsSession(data.sessionId, epoch) || !fsApi.existsSync(resultPath)) return;
407
- try {
408
- fsApi.unlinkSync(resultPath);
409
- identityCache.delete(file);
410
- } catch (error) {
411
- if (!isNotFound(error)) {
412
- console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
413
- scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
414
- }
524
+ if (!observerSucceeded) {
525
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
526
+ return;
415
527
  }
528
+ if (!ownsSession(sessionId, epoch)) return;
529
+ if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
416
530
  } catch (error) {
417
531
  if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
418
532
  } finally {
@@ -424,18 +538,45 @@ export function createResultWatcher(
424
538
  const triggerTurn = pendingTriggerTurn.get(file) !== false;
425
539
  pendingTriggerTurn.delete(file);
426
540
  void handleResult(file, triggerTurn);
427
- }, 50);
541
+ }, deps.coalesceDelayMs ?? 50);
428
542
 
543
+ const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
544
+ const files = new Set<string>();
545
+ if (state.currentSessionId) {
546
+ for (const file of resultCandidateFilesForSession(resultsDir, state.currentSessionId)) files.add(file);
547
+ }
548
+ for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
549
+ for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
550
+ for (const runId of observed) files.add(`${runId}.json`);
551
+ return [...files];
552
+ };
429
553
  const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
430
554
  try {
431
555
  const triggerTurn = options.triggerTurn !== false;
432
- fsApi.readdirSync(resultsDir)
433
- .filter((f) => f.endsWith(".json"))
434
- .forEach((file) => {
435
- if (shouldProcessResult(file)) scheduleResult(file, triggerTurn);
436
- });
556
+ const observed = observedRunIds();
557
+ for (const file of indexedResultCandidates(observed)) {
558
+ const signature = resultSignature(file, observed);
559
+ if (!signature) continue;
560
+ if (!shouldProcessResult(file, observed, signature)) continue;
561
+ scheduleResult(file, triggerTurn);
562
+ }
563
+ // Selesai fork: during the lineage adoption window, also scan flat
564
+ // result files written directly under the results root by a previous
565
+ // session's runtime, so completions that raced the session switch are
566
+ // adopted instead of left behind. Bounded to the window; outside it,
567
+ // flat files are stale leftovers and only the indexed inbox is scanned.
568
+ if (state.adoptedResultWindowStart !== undefined) {
569
+ const indexed = indexedResultCandidates(observed);
570
+ for (const file of fsApi.readdirSync(resultsDir)) {
571
+ if (!file.endsWith(".json") || indexed.includes(file)) continue;
572
+ const signature = resultSignature(file, observed);
573
+ if (!signature) continue;
574
+ if (!shouldProcessResult(file, observed, signature)) continue;
575
+ scheduleResult(file, triggerTurn);
576
+ }
577
+ }
437
578
  } catch (error) {
438
- if (!isNotFound(error)) console.error(`Failed to scan subagent result directory '${resultsDir}':`, error);
579
+ if (!isNotFound(error)) console.error(`Failed to scan subagent result index in '${resultsDir}':`, error);
439
580
  }
440
581
  };
441
582
 
@@ -492,6 +633,11 @@ export function createResultWatcher(
492
633
  const fileName = file.toString();
493
634
  if (fileName.endsWith(".json")) {
494
635
  identityCache.delete(fileName);
636
+ try {
637
+ writeResultIndexForData(path.join(resultsDir, fileName), JSON.parse(fsApi.readFileSync(path.join(resultsDir, fileName), "utf-8")) as Record<string, unknown>);
638
+ } catch {
639
+ // The writer may still be renaming the file; handleResult will retry from the normal result path.
640
+ }
495
641
  scheduleResult(fileName, true);
496
642
  }
497
643
  });
@@ -1,6 +1,19 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { AsyncRunSummary } from "./async-status.ts";
3
3
 
4
+ const INTERCOM_DETACH_ERROR = "detached for intercom coordination";
5
+
6
+ function isIntercomDetached(run: AsyncRunSummary): boolean {
7
+ return run.steps.some((step) => step.execution?.status === "detached"
8
+ || step.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true)
9
+ || run.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true;
10
+ }
11
+
12
+ function formatIntercomDetachGuidance(run: AsyncRunSummary): string | undefined {
13
+ if (!isIntercomDetached(run)) return undefined;
14
+ return `Run "${run.id}" detached for intercom coordination. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.id}" }). Use subagent({ action: "status", id: "${run.id}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
15
+ }
16
+
4
17
  export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefined {
5
18
  const step = run.steps.find((candidate) => candidate.status === "failed" && candidate.sessionFile && fs.existsSync(candidate.sessionFile));
6
19
  if (!step) {
@@ -15,19 +28,26 @@ export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefin
15
28
 
16
29
  export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string | undefined {
17
30
  if (run.state !== "failed") return undefined;
31
+ const detachGuidance = formatIntercomDetachGuidance(run);
32
+ if (detachGuidance) return detachGuidance;
18
33
  const command = formatAsyncReviveCommand(run);
19
34
  if (!command) return undefined;
20
35
  return `Resume-first: failed run "${run.id}" has a persisted child session. Revive the original run with ${command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
21
36
  }
22
37
 
23
38
  export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
24
- const resumable = runs
25
- .filter((run) => run.state === "failed")
39
+ const failedRuns = runs.filter((run) => run.state === "failed");
40
+ const detachGuidance = failedRuns
41
+ .map(formatIntercomDetachGuidance)
42
+ .filter((guidance): guidance is string => Boolean(guidance));
43
+ const resumable = failedRuns
44
+ .filter((run) => !isIntercomDetached(run))
26
45
  .map((run) => ({ run, command: formatAsyncReviveCommand(run) }))
27
46
  .filter((entry): entry is { run: AsyncRunSummary; command: string } => Boolean(entry.command));
28
- if (resumable.length === 0) return "";
29
- const guidance = resumable.length === 1
30
- ? `failed run "${resumable[0]!.run.id}" has a persisted child session. Revive the original run with ${resumable[0]!.command}`
31
- : `${resumable.length} failed runs have persisted child sessions. Inspect status and revive each original run before retrying`;
32
- return ` Resume-first: ${guidance} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
47
+ const resumeGuidance = resumable.length === 0
48
+ ? ""
49
+ : resumable.length === 1
50
+ ? ` Resume-first: failed run "${resumable[0]!.run.id}" has a persisted child session. Revive the original run with ${resumable[0]!.command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`
51
+ : ` Resume-first: ${resumable.length} failed runs have persisted child sessions. Inspect status and revive each original run before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
52
+ return `${detachGuidance.length > 0 ? ` ${detachGuidance.join(" ")}` : ""}${resumeGuidance}`;
33
53
  }
@@ -1,30 +1,68 @@
1
1
  import * as fs from "node:fs";
2
- import { listAsyncRuns } from "./async-status.ts";
2
+ import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
3
+ import { readAsyncRecoveryDescriptor } from "./async-resume.ts";
3
4
  import type { TokenUsage } from "../../shared/types.ts";
4
5
 
5
6
  const MAX_RETAINED_CHILDREN = 10;
6
7
  const MAX_TASK_SUMMARY_LENGTH = 120;
7
8
 
9
+ type RetainedChildState = "complete" | "failed" | "paused" | "stopped";
10
+ type RetainedChildResumability =
11
+ | { state: "resumable"; sessionPath: string }
12
+ | { state: "not-resumable"; reason: string };
13
+
8
14
  export interface RetainedChild {
9
15
  runId: string;
10
16
  parentRunId?: string;
17
+ workflowKey?: string;
18
+ state: RetainedChildState;
11
19
  agent: string;
12
20
  taskSummary: string;
13
21
  completedAt: number;
14
- sessionPath: string;
22
+ resumability: RetainedChildResumability;
23
+ sessionPath?: string;
15
24
  tokenTotals?: TokenUsage;
16
25
  }
17
26
 
18
- function retainedSessionFile(sessionFile: string | undefined): sessionFile is string {
19
- if (!sessionFile?.endsWith(".jsonl")) return false;
27
+ function isRetainedChildState(state: AsyncRunSummary["state"]): state is RetainedChildState {
28
+ return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
29
+ }
30
+
31
+ function isTerminalStepStatus(status: AsyncRunSummary["steps"][number]["status"]): boolean {
32
+ return status === "complete" || status === "completed" || status === "failed" || status === "paused" || status === "stopped";
33
+ }
34
+
35
+ function retainedSessionFile(sessionFile: string | undefined): RetainedChildResumability {
36
+ if (!sessionFile) return { state: "not-resumable", reason: "no persisted session file" };
37
+ if (!sessionFile.endsWith(".jsonl")) return { state: "not-resumable", reason: "persisted session file is not a .jsonl file" };
20
38
  try {
21
39
  const stat = fs.lstatSync(sessionFile);
22
- return stat.isFile() && !stat.isSymbolicLink();
23
- } catch {
24
- return false;
40
+ if (!stat.isFile() || stat.isSymbolicLink()) return { state: "not-resumable", reason: "persisted session file is not a regular file" };
41
+ return { state: "resumable", sessionPath: sessionFile };
42
+ } catch (error) {
43
+ if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
44
+ return { state: "not-resumable", reason: `persisted session file is missing: ${sessionFile}` };
45
+ }
46
+ return { state: "not-resumable", reason: `persisted session file could not be inspected: ${error instanceof Error ? error.message : String(error)}` };
25
47
  }
26
48
  }
27
49
 
50
+ function childResumability(run: AsyncRunSummary, step: AsyncRunSummary["steps"][number]): RetainedChildResumability {
51
+ if (run.state === "stopped" || step.status === "stopped") return { state: "not-resumable", reason: "stopped run" };
52
+ if (step.runner?.type === "external-cli") return { state: "not-resumable", reason: "external CLI runner" };
53
+ const session = retainedSessionFile(step.sessionFile ?? run.sessionFile);
54
+ if (session.state === "not-resumable") return session;
55
+ try {
56
+ const recoveryDescriptor = readAsyncRecoveryDescriptor(run.asyncDir);
57
+ if (!recoveryDescriptor) return { state: "not-resumable", reason: "missing recovery descriptor" };
58
+ if (recoveryDescriptor.sourceRunId !== run.id) return { state: "not-resumable", reason: `recovery descriptor belongs to run ${recoveryDescriptor.sourceRunId}` };
59
+ if (recoveryDescriptor.agent !== step.agent) return { state: "not-resumable", reason: `recovery descriptor belongs to agent ${recoveryDescriptor.agent}` };
60
+ } catch (error) {
61
+ return { state: "not-resumable", reason: `invalid recovery descriptor: ${error instanceof Error ? error.message : String(error)}` };
62
+ }
63
+ return session;
64
+ }
65
+
28
66
  function boundedTaskSummary(value: string | undefined): string {
29
67
  const normalized = value?.replace(/\s+/g, " ").trim() ?? "";
30
68
  return normalized.length > MAX_TASK_SUMMARY_LENGTH
@@ -32,37 +70,56 @@ function boundedTaskSummary(value: string | undefined): string {
32
70
  : normalized;
33
71
  }
34
72
 
35
- export function listRetainedChildren(asyncDirRoot: string, sessionIds: string[]): RetainedChild[] {
36
- return listAsyncRuns(asyncDirRoot, { sessionIds, states: ["complete"], reconcile: false })
73
+ export function listRetainedChildren(asyncDirRoot: string, sessionIds: string | string[]): RetainedChild[] {
74
+ const children = listAsyncRuns(asyncDirRoot, { sessionIds: Array.isArray(sessionIds) ? sessionIds : [sessionIds], states: ["complete", "failed", "paused", "stopped"], reconcile: false })
37
75
  .flatMap((run) => {
38
76
  if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
77
+ if (!isRetainedChildState(run.state)) return [];
39
78
  const step = run.steps[0]!;
40
- if ((step.status !== "complete" && step.status !== "completed") || !retainedSessionFile(step.sessionFile ?? run.sessionFile)) return [];
41
- const completedAt = run.endedAt;
79
+ if (!isTerminalStepStatus(step.status)) return [];
80
+ const completedAt = run.endedAt ?? run.lastUpdate;
42
81
  if (completedAt === undefined) return [];
82
+ const resumability = childResumability(run, step);
43
83
  return [{
44
84
  runId: run.id,
45
85
  ...(run.parentWorkflowRunId ? { parentRunId: run.parentWorkflowRunId } : {}),
86
+ ...(run.workflowKey ? { workflowKey: run.workflowKey } : {}),
87
+ state: run.state,
46
88
  agent: step.agent,
47
89
  taskSummary: boundedTaskSummary(step.description),
48
90
  completedAt,
49
- sessionPath: step.sessionFile ?? run.sessionFile!,
91
+ resumability,
92
+ ...(resumability.state === "resumable" ? { sessionPath: resumability.sessionPath } : {}),
50
93
  ...(step.tokens ?? run.totalTokens ? { tokenTotals: step.tokens ?? run.totalTokens } : {}),
51
94
  }];
52
95
  })
53
- .sort((left, right) => right.completedAt - left.completedAt)
54
- .slice(0, MAX_RETAINED_CHILDREN);
96
+ .sort((left, right) => right.completedAt - left.completedAt);
97
+ return children;
55
98
  }
56
99
 
57
100
  export function formatRetainedChildren(children: RetainedChild[]): string {
58
- if (children.length === 0) return "No completed retained children in the active parent session.";
101
+ if (children.length === 0) return "No retained workflow children in the active parent session. If a retained-writer challenge is required, launch a same-role fallback challenge and label it as fallback.";
102
+ const retained = children.slice(0, MAX_RETAINED_CHILDREN);
103
+ if (!retained.some((child) => child.resumability.state === "resumable")) {
104
+ const resumable = children.slice(MAX_RETAINED_CHILDREN).find((child) => child.resumability.state === "resumable");
105
+ if (resumable && retained.length === MAX_RETAINED_CHILDREN) retained[MAX_RETAINED_CHILDREN - 1] = resumable;
106
+ }
107
+ const hasResumableChild = retained.some((child) => child.resumability.state === "resumable");
59
108
  return [
60
- `Completed retained children (newest first, last ${MAX_RETAINED_CHILDREN}):`,
61
- ...children.flatMap((child) => [
62
- `- ${child.runId} | ${child.agent} | ${new Date(child.completedAt).toISOString()}`,
109
+ `Retained workflow children (up to ${MAX_RETAINED_CHILDREN}; newest first, with a resumable child retained when available):`,
110
+ ...retained.flatMap((child) => [
111
+ `- ${child.runId} | ${child.agent} | ${child.state} | ${new Date(child.completedAt).toISOString()}`,
112
+ ...(child.parentRunId ? [` workflow: ${child.parentRunId}${child.workflowKey ? ` (${child.workflowKey})` : ""}`] : []),
63
113
  ` task: ${child.taskSummary || "(no task summary)"}`,
64
- ` session: ${child.sessionPath}`,
114
+ child.resumability.state === "resumable"
115
+ ? " resumability: resumable"
116
+ : ` resumability: not resumable (${child.resumability.reason})`,
117
+ ...(child.resumability.state === "resumable" ? [
118
+ ` session: ${child.resumability.sessionPath}`,
119
+ ` resume: subagent({ action: "resume", id: "${child.runId}", message: "..." })`,
120
+ ] : []),
65
121
  ...(child.tokenTotals ? [` tokens: input ${child.tokenTotals.input}, output ${child.tokenTotals.output}, total ${child.tokenTotals.total}`] : []),
66
122
  ]),
123
+ ...(hasResumableChild ? [] : ["No resumable retained child is listed. Launch a same-role fallback challenge and label it as fallback."]),
67
124
  ].join("\n");
68
125
  }