@selesai/code 0.8.6 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/settings.json +1 -0
  16. package/dist/extensions/model-prompt-injector/config.json +10 -0
  17. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  18. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  19. package/dist/extensions/package.json +1 -0
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  21. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  22. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  23. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  24. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  25. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  26. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  27. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  28. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  29. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  30. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  31. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  32. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  33. package/dist/extensions/pi-subagents/install.mjs +2 -2
  34. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  35. package/dist/extensions/pi-subagents/package.json +1 -1
  36. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  37. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  38. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  39. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  40. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  41. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  42. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  43. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  45. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  46. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  47. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  48. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  49. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  50. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  51. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  52. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  53. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  54. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  55. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  56. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  57. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  58. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  59. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  60. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  61. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  62. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  64. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  66. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  67. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  70. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
  71. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  72. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  73. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  74. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  75. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  76. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  80. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  81. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  83. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  84. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  85. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  86. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  88. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  89. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  90. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  96. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  97. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  98. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  99. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  100. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  101. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  102. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  104. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  105. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  106. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  107. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  108. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  109. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  110. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  111. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  112. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  113. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  114. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  115. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  116. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  117. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  118. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  119. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  120. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  121. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  122. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  123. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  124. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  125. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  126. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  127. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  128. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  129. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  130. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  131. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  133. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  134. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  135. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  136. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  137. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  138. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  139. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  140. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  141. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  142. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  143. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  144. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  145. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  146. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  147. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  148. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  149. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  150. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  151. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  152. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  153. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  154. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  155. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  156. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  157. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  158. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  159. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  160. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  161. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  162. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  163. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  164. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  165. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  166. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  167. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  168. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  169. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  170. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  171. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  172. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  173. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  174. package/dist/extensions/question/index.ts +21 -6
  175. package/dist/extensions/question/question-list.ts +7 -1
  176. package/dist/extensions/question/row-layout.ts +3 -0
  177. package/dist/extensions/question/tests/exp.test.ts +70 -0
  178. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  179. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  180. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  181. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  182. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  183. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  184. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  185. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  186. package/dist/skills/pi-subagents/SKILL.md +1 -1
  187. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  188. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  189. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  190. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  191. package/docs/settings.md +6 -0
  192. package/package.json +3 -3
@@ -22,12 +22,14 @@ 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;
28
29
  const POLL_INTERVAL_MS = 3000;
29
30
  const HEALTHY_SCAN_INTERVAL_MS = 60_000;
30
31
  const RETRY_DELAY_MS = 100;
32
+ const SLOW_RESULT_SCAN_MS = 500;
31
33
 
32
34
  type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "statSync" | "watch">;
33
35
 
@@ -50,6 +52,8 @@ type ResultWatcherDeps = {
50
52
  parseResult?: (raw: string) => ResultFileData;
51
53
  /** External grouped-result transport. Disable when native completion notifications own delivery. */
52
54
  deliverIntercomResults?: boolean;
55
+ /** Coalesces result-file events. Tests can lower this without changing retry timing. */
56
+ coalesceDelayMs?: number;
53
57
  };
54
58
 
55
59
  type ResultFileChild = {
@@ -79,6 +83,7 @@ type ResultFileData = CompletionNotification & {
79
83
  asyncDir?: string;
80
84
  intercomTarget?: string;
81
85
  parallelHandoff?: ParallelHandoffReference;
86
+ notificationDeliveredAt?: unknown;
82
87
  };
83
88
 
84
89
  type ResultFileIdentity = {
@@ -87,6 +92,12 @@ type ResultFileIdentity = {
87
92
  asyncDir?: string;
88
93
  };
89
94
 
95
+ interface ResultScanStats {
96
+ files: number;
97
+ scheduled: number;
98
+ startedAt: number;
99
+ }
100
+
90
101
  function jsonStringProperty(raw: string, property: string): string | undefined {
91
102
  const matches = raw.matchAll(new RegExp(`"${property}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, "g"));
92
103
  let encoded: string | undefined;
@@ -134,6 +145,16 @@ function shouldPoll(error: unknown): boolean {
134
145
  return code === "EMFILE" || code === "ENOSPC";
135
146
  }
136
147
 
148
+ function hasDeliveredNotification(data: ResultFileData): boolean {
149
+ return typeof data.notificationDeliveredAt === "number" && Number.isFinite(data.notificationDeliveredAt);
150
+ }
151
+
152
+ function markDeliveredNotification(resultPath: string, data: ResultFileData, runId: string, now: number): ResultFileData {
153
+ const marked = { ...data, runId, notificationDeliveredAt: now };
154
+ writeAsyncResultFile(resultPath, marked);
155
+ return marked;
156
+ }
157
+
137
158
  /**
138
159
  * Watches persisted async results for the session currently owned by this
139
160
  * runtime. `stopResultWatcher()` revokes ownership before closing resources,
@@ -194,16 +215,55 @@ export function createResultWatcher(
194
215
  state.resultFileCoalescer.schedule(file, delayMs);
195
216
  };
196
217
 
197
- const inspectResult = (file: string): ResultFileIdentity | undefined => {
198
- const resultPath = path.join(resultsDir, file);
218
+ const publicResultPath = (file: string): string => path.join(resultsDir, file);
219
+ const publicResultFileExists = (file: string): boolean => {
220
+ try {
221
+ return fsApi.statSync(publicResultPath(file)).isFile();
222
+ } catch (error) {
223
+ if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
224
+ return false;
225
+ }
226
+ };
227
+ const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
228
+ if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
229
+ const runId = file.replace(/\.json$/i, "");
230
+ const sessionResult = state.currentSessionId ? resultPayloadPathForSessionRun(resultsDir, state.currentSessionId, runId) : undefined;
231
+ if (sessionResult) return sessionResult;
232
+ const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
233
+ if (observerResult) return observerResult;
234
+ if (observed?.has(runId)) {
235
+ const indexedResult = resultPayloadPathForIndexedRun(resultsDir, runId);
236
+ if (indexedResult) return indexedResult;
237
+ }
238
+ return publicResultFileExists(file) ? publicResultPath(file) : undefined;
239
+ };
240
+ const resultSignature = (file: string, observed?: ReadonlySet<string>): string | undefined => {
241
+ const resultPath = resultPayloadPath(file, observed);
242
+ if (!resultPath) {
243
+ identityCache.delete(file);
244
+ return undefined;
245
+ }
199
246
  try {
200
247
  const stat = fsApi.statSync(resultPath);
201
- const signature = `${stat.size}:${stat.mtimeMs}`;
248
+ if (!stat.isFile()) return undefined;
249
+ return `${resultPath}:${stat.size}:${stat.mtimeMs}`;
250
+ } catch (error) {
251
+ identityCache.delete(file);
252
+ if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
253
+ return undefined;
254
+ }
255
+ };
256
+ const inspectResult = (file: string, knownSignature?: string, observed?: ReadonlySet<string>): { identity: ResultFileIdentity; signature: string } | undefined => {
257
+ const resultPath = resultPayloadPath(file, observed);
258
+ if (!resultPath) return undefined;
259
+ try {
260
+ const signature = knownSignature ?? resultSignature(file, observed);
261
+ if (!signature) return undefined;
202
262
  const cached = identityCache.get(file);
203
- if (cached?.signature === signature) return cached.identity;
263
+ if (cached?.signature === signature) return { identity: cached.identity, signature };
204
264
  const identity = resultFileIdentity(fsApi.readFileSync(resultPath, "utf-8"), file);
205
265
  identityCache.set(file, { signature, identity });
206
- return identity;
266
+ return { identity, signature };
207
267
  } catch (error) {
208
268
  identityCache.delete(file);
209
269
  if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
@@ -220,9 +280,10 @@ export function createResultWatcher(
220
280
  }
221
281
  };
222
282
 
223
- const shouldProcessResult = (file: string, observed?: ReadonlySet<string>): boolean => {
224
- const identity = inspectResult(file);
225
- if (!identity) return false;
283
+ const shouldProcessResult = (file: string, observed?: ReadonlySet<string>, knownSignature?: string): boolean => {
284
+ const inspected = inspectResult(file, knownSignature, observed);
285
+ if (!inspected) return false;
286
+ const { identity } = inspected;
226
287
  // Missing identity stays on the normal parser path so malformed or legacy
227
288
  // files keep their existing diagnostics and compatibility behavior.
228
289
  if (!identity.sessionId) return true;
@@ -233,32 +294,80 @@ export function createResultWatcher(
233
294
  return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
234
295
  };
235
296
 
297
+ const removeDeliveredResult = (file: string, sessionId: string, runId: string, toolCallId: string | undefined): boolean => {
298
+ try {
299
+ if (publicResultFileExists(file)) fsApi.unlinkSync(publicResultPath(file));
300
+ identityCache.delete(file);
301
+ removeResultIndex(resultsDir, sessionId, runId, toolCallId);
302
+ return true;
303
+ } catch (error) {
304
+ if (!isNotFound(error)) {
305
+ console.error(`Failed to remove delivered subagent result '${publicResultPath(file)}'; will retry:`, error);
306
+ return false;
307
+ }
308
+ return true;
309
+ }
310
+ };
236
311
  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;
312
+ if (processing.has(file)) return;
313
+ let observed: ReadonlySet<string> | undefined;
314
+ if (!shouldProcessResult(file)) {
315
+ const runId = file === path.basename(file) && file.endsWith(".json") ? file.replace(/\.json$/i, "") : undefined;
316
+ observed = observedRunIds();
317
+ if (!runId || !observed.has(runId) || !shouldProcessResult(file, observed)) return;
318
+ }
240
319
  processing.add(file);
320
+ let resultPath = publicResultPath(file);
241
321
  try {
242
- const data = parseResult(fsApi.readFileSync(resultPath, "utf-8"));
322
+ const payloadPath = resultPayloadPath(file, observed);
323
+ if (!payloadPath) return;
324
+ resultPath = payloadPath;
325
+ let raw = fsApi.readFileSync(resultPath, "utf-8");
326
+ let identity = resultFileIdentity(raw, file);
327
+ if (identity.sessionId && identity.runId) {
328
+ const pendingState = promotePendingResultFile(resultsDir, identity.sessionId, identity.runId, file);
329
+ if (pendingState === "promoted") {
330
+ identityCache.delete(file);
331
+ resultPath = publicResultPath(file);
332
+ raw = fsApi.readFileSync(resultPath, "utf-8");
333
+ identity = resultFileIdentity(raw, file);
334
+ } else if (pendingState === "pending") {
335
+ const pendingPath = resultPayloadPathForSessionRun(resultsDir, identity.sessionId, identity.runId);
336
+ if (!pendingPath) {
337
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
338
+ return;
339
+ }
340
+ resultPath = pendingPath;
341
+ raw = fsApi.readFileSync(resultPath, "utf-8");
342
+ identity = resultFileIdentity(raw, file);
343
+ }
344
+ }
345
+ let data = parseResult(raw);
243
346
  if (typeof data.sessionId !== "string" || !data.sessionId) return;
347
+ const sessionId = data.sessionId;
244
348
  const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
349
+ const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
350
+ let observerSucceeded = true;
245
351
  try {
246
352
  syncMissionFromAsyncCompletion({ ...data, runId });
247
353
  } catch (error) {
354
+ observerSucceeded = false;
248
355
  console.error(`Mission completion sync failed for '${resultPath}':`, error);
249
356
  }
250
357
  try {
251
358
  deps.observeCompletion?.({ ...data, runId });
252
359
  } catch (error) {
360
+ observerSucceeded = false;
253
361
  console.error(`Completion observer failed for '${resultPath}':`, error);
254
362
  }
363
+ if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
255
364
  const epoch = deliveryEpoch;
256
- if (!ownsSession(data.sessionId, epoch)) return;
365
+ if (!ownsSession(sessionId, epoch)) return;
257
366
  // Recorded before dedupe and before the unlink below so subagent_wait can
258
367
  // use the in-memory record or its bounded durable replay after cleanup.
259
368
  recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
260
369
  resultsDir,
261
- sessionId: data.sessionId,
370
+ sessionId,
262
371
  });
263
372
  const hasExplicitNestedChildren = data.nestedChildren !== undefined;
264
373
  let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
@@ -273,20 +382,17 @@ export function createResultWatcher(
273
382
  }
274
383
 
275
384
  const completionKey = buildCompletionKey(data, `result:${file}`);
385
+ const alreadyDelivered = hasDeliveredNotification(data);
276
386
  const lastSeenAt = state.completionSeen.get(completionKey);
277
387
  if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
278
388
  state.completionSeen.delete(completionKey);
279
389
  } 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
- }
390
+ if (!observerSucceeded) {
391
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
392
+ return;
289
393
  }
394
+ if (!ownsSession(sessionId, epoch)) return;
395
+ if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
290
396
  return;
291
397
  }
292
398
 
@@ -334,6 +440,17 @@ export function createResultWatcher(
334
440
  };
335
441
  }), nestedChildren);
336
442
 
443
+ if (alreadyDelivered) {
444
+ markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
445
+ if (!observerSucceeded) {
446
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
447
+ return;
448
+ }
449
+ if (!ownsSession(sessionId, epoch)) return;
450
+ if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
451
+ return;
452
+ }
453
+
337
454
  const intercomTarget = data.intercomTarget?.trim();
338
455
  let intercomDelivered = false;
339
456
  if (deliverIntercomResults && intercomTarget && triggerTurn) {
@@ -350,7 +467,7 @@ export function createResultWatcher(
350
467
  asyncDir: data.asyncDir,
351
468
  ...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
352
469
  }));
353
- if (!ownsSession(data.sessionId, epoch)) return;
470
+ if (!ownsSession(sessionId, epoch)) return;
354
471
  if (!intercomDelivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
355
472
  }
356
473
 
@@ -374,11 +491,19 @@ export function createResultWatcher(
374
491
  })) : [],
375
492
  } : {}),
376
493
  });
377
- if (!ownsSession(data.sessionId, epoch)) return;
494
+ if (!ownsSession(sessionId, epoch)) return;
378
495
  if (!accepted) {
379
496
  scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
380
497
  return;
381
498
  }
499
+ try {
500
+ data = markDeliveredNotification(publicResultPath(file), data, runId, Date.now());
501
+ identityCache.delete(file);
502
+ } catch (error) {
503
+ console.error(`Failed to mark subagent result notification delivered for '${resultPath}'; will retry:`, error);
504
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
505
+ return;
506
+ }
382
507
  markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
383
508
  try {
384
509
  pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
@@ -403,16 +528,12 @@ export function createResultWatcher(
403
528
  } catch (error) {
404
529
  console.error(`Completion observer failed for '${resultPath}':`, error);
405
530
  }
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
- }
531
+ if (!observerSucceeded) {
532
+ scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
533
+ return;
415
534
  }
535
+ if (!ownsSession(sessionId, epoch)) return;
536
+ if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
416
537
  } catch (error) {
417
538
  if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
418
539
  } finally {
@@ -424,18 +545,56 @@ export function createResultWatcher(
424
545
  const triggerTurn = pendingTriggerTurn.get(file) !== false;
425
546
  pendingTriggerTurn.delete(file);
426
547
  void handleResult(file, triggerTurn);
427
- }, 50);
548
+ }, deps.coalesceDelayMs ?? 50);
428
549
 
550
+ const logScanStats = (stats: ResultScanStats) => {
551
+ const elapsed = Date.now() - stats.startedAt;
552
+ if (elapsed < SLOW_RESULT_SCAN_MS) return;
553
+ console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
554
+ };
555
+ const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
556
+ const files = new Set<string>();
557
+ if (state.currentSessionId) {
558
+ for (const file of resultCandidateFilesForSession(resultsDir, state.currentSessionId)) files.add(file);
559
+ }
560
+ for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
561
+ for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
562
+ for (const runId of observed) files.add(`${runId}.json`);
563
+ return [...files];
564
+ };
429
565
  const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
430
566
  try {
431
567
  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
- });
568
+ const stats: ResultScanStats = { files: 0, scheduled: 0, startedAt: Date.now() };
569
+ const observed = observedRunIds();
570
+ for (const file of indexedResultCandidates(observed)) {
571
+ stats.files += 1;
572
+ const signature = resultSignature(file, observed);
573
+ if (!signature) continue;
574
+ if (!shouldProcessResult(file, observed, signature)) continue;
575
+ stats.scheduled += 1;
576
+ scheduleResult(file, triggerTurn);
577
+ }
578
+ // Selesai fork: during the lineage adoption window, also scan flat
579
+ // result files written directly under the results root by a previous
580
+ // session's runtime, so completions that raced the session switch are
581
+ // adopted instead of left behind. Bounded to the window; outside it,
582
+ // flat files are stale leftovers and only the indexed inbox is scanned.
583
+ if (state.adoptedResultWindowStart !== undefined) {
584
+ const indexed = indexedResultCandidates(observed);
585
+ for (const file of fsApi.readdirSync(resultsDir)) {
586
+ if (!file.endsWith(".json") || indexed.includes(file)) continue;
587
+ stats.files += 1;
588
+ const signature = resultSignature(file, observed);
589
+ if (!signature) continue;
590
+ if (!shouldProcessResult(file, observed, signature)) continue;
591
+ stats.scheduled += 1;
592
+ scheduleResult(file, triggerTurn);
593
+ }
594
+ }
595
+ logScanStats(stats);
437
596
  } catch (error) {
438
- if (!isNotFound(error)) console.error(`Failed to scan subagent result directory '${resultsDir}':`, error);
597
+ if (!isNotFound(error)) console.error(`Failed to scan subagent result index in '${resultsDir}':`, error);
439
598
  }
440
599
  };
441
600
 
@@ -492,6 +651,11 @@ export function createResultWatcher(
492
651
  const fileName = file.toString();
493
652
  if (fileName.endsWith(".json")) {
494
653
  identityCache.delete(fileName);
654
+ try {
655
+ writeResultIndexForData(path.join(resultsDir, fileName), JSON.parse(fsApi.readFileSync(path.join(resultsDir, fileName), "utf-8")) as Record<string, unknown>);
656
+ } catch {
657
+ // The writer may still be renaming the file; handleResult will retry from the normal result path.
658
+ }
495
659
  scheduleResult(fileName, true);
496
660
  }
497
661
  });
@@ -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
  }