@agwab/pi-workflow 0.2.1 → 0.4.0

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 (119) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
package/dist/engine.js CHANGED
@@ -4,11 +4,12 @@ import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { Worker } from "node:worker_threads";
5
5
  import { compileWorkflow } from "./compiler.js";
6
6
  import { loadWorkflowSpec } from "./schema.js";
7
- import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, workflowRunPath, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
7
+ import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isBlockedTaskResumableForResume, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, workflowRunPath, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
8
8
  import { resolveWorkflowBackend } from "./backend.js";
9
9
  import { ensureManagedWorktree } from "./worktree.js";
10
10
  import { resolveWorkflowHelperRef } from "./workflow-helpers.js";
11
11
  import { buildAvailableToolView } from "./tool-metadata.js";
12
+ import { summarizeWorkflowTelemetry } from "./workflow-artifacts.js";
12
13
  import { workflowBundleFingerprint, workflowBundleSpecPath, } from "./workflow-source-context-runtime.js";
13
14
  import { readSimpleJsonPath, } from "./workflow-runtime.js";
14
15
  import { dynamicRunDir, hashDynamicRequest, readDynamicEvents, } from "./dynamic-events.js";
@@ -18,10 +19,11 @@ import { assertDynamicRuntimeBudgetAvailable, dynamicRuntimeBudgetExceededMessag
18
19
  import { assertDynamicGeneratedMetadataMatches, assertDynamicGenerationBudgetAvailable, buildDynamicGeneratedCompiledTask, dynamicGeneratedInsertIndex, isDynamicCompiledTaskPayload, normalizeDynamicAgentRequest, readDynamicGeneratedTaskResult, } from "./dynamic-generated-task-runtime.js";
19
20
  import { optionalEventString, runDynamicHelperCall, runDynamicNestedWorkflowCall, } from "./dynamic-controller-calls.js";
20
21
  import { normalizeDynamicFanoutPlanRequest, runDynamicDecisionLoopStatusPersistCall, runDynamicDecisionPersistCall, runDynamicFanoutPlanPersistCall, runDynamicResultReadCall, runDynamicStateIndexPersistCall, } from "./dynamic-control-ops.js";
21
- import { assertRunTaskPositionalAlignment, buildForeachGeneratedTasks, dependenciesReady, markDagDependentsSkipped, nextTaskRecordIndex, reconcileDynamicGeneratedRunRecords, recoverStaleRunningDynamicControllers, replaceDependencyList, sourceStageIdsForFrom, stageSourcePolicy, updateDownstreamDependencies, } from "./engine-run-graph.js";
22
+ import { assertRunTaskPositionalAlignment, buildForeachGeneratedTasks, dependenciesReady, foreachStreamingEnabled, foreachStreamingMinChunk, markDagDependentsSkipped, nextTaskRecordIndex, reconcileDynamicGeneratedRunRecords, reconcileForeachGeneratedRunRecords, recoverStaleRunningDynamicControllers, replaceDependencyList, sourceStageIdsForFrom, stageSourcePolicy, updateDownstreamDependencies, } from "./engine-run-graph.js";
22
23
  import { reconcileLoopTaskMaterialization, scheduleLoop, } from "./loop-runtime.js";
23
24
  import { executeSupportTask, normalizeDynamicControllerOutput, prepareArtifactGraphRetryTask, prepareDagTask, readArtifactGraphControl, readArtifactGraphSupportSources, readSupportSources, writeArtifactGraphDynamicResult, } from "./artifact-graph-runtime.js";
24
25
  import { DIRECT_DYNAMIC_RUNTIME_VERSION, ensureDirectDynamicRuntimeBundle, } from "./dynamic-runtime-bundle.js";
26
+ import { hasFatalPartialOutputIssue, readWorkflowPartialOutputLedger, writeWorkflowPartialOutputLedgerFromFile, } from "./workflow-partial-output.js";
25
27
  import { WORKFLOW_RUN_TYPE, } from "./types.js";
26
28
  export { buildRunSourceContext } from "./workflow-source-context-runtime.js";
27
29
  export { evaluateLoopUntilCondition } from "./loop-runtime.js";
@@ -37,6 +39,8 @@ const DYNAMIC_CONTROLLER_ENGINE_CAPABILITIES = Object.freeze({
37
39
  const DYNAMIC_CONTROLLER_ENGINE_INTEGRITY_ERROR_MESSAGE = "incompatible or stale pi-workflow engine: dynamic controller context is missing runDecisionLoop (rebuild dist / reload workflow engine)";
38
40
  const supervisorTimers = new Map();
39
41
  const supervisorRunMtimes = new Map();
42
+ const supervisorErrorCounts = new Map();
43
+ const MAX_SUPERVISOR_CONSECUTIVE_ERRORS = 3;
40
44
  export async function runWorkflowSpec(specPath, cwd, options = {}) {
41
45
  const loaded = await loadWorkflowSpec(specPath, cwd);
42
46
  return runLoadedWorkflowSpec(cwd, loaded.specPath, loaded.spec, options);
@@ -83,7 +87,7 @@ async function runLoadedWorkflowSpec(cwd, specPath, spec, options, provenance) {
83
87
  };
84
88
  const scheduled = (await scheduleRun(cwd, run.runId, compiled, scheduleOptions)) ??
85
89
  (await readRunRecord(cwd, run.runId));
86
- if (scheduled.status === "running")
90
+ if (shouldWatchRun(scheduled))
87
91
  watchRun(cwd, scheduled.runId, scheduleOptions);
88
92
  return scheduled;
89
93
  }
@@ -95,11 +99,19 @@ export async function refreshRun(cwd, runIdOrPrefix) {
95
99
  });
96
100
  return refreshed ?? current;
97
101
  }
102
+ function hasActiveSchedulerWork(run) {
103
+ return (run.status === "running" ||
104
+ run.taskSummary.running > 0 ||
105
+ run.taskSummary.pending > 0);
106
+ }
107
+ function shouldWatchRun(run) {
108
+ return hasActiveSchedulerWork(run);
109
+ }
98
110
  export async function waitForRun(cwd, runIdOrPrefix, timeoutMs, options = {}) {
99
111
  const timeout = clampTimeout(timeoutMs);
100
112
  const deadline = Date.now() + timeout;
101
113
  let run = await refreshRun(cwd, runIdOrPrefix);
102
- while (run.status === "running") {
114
+ while (hasActiveSchedulerWork(run)) {
103
115
  const beforeScheduleRemaining = deadline - Date.now();
104
116
  if (beforeScheduleRemaining <= 0)
105
117
  throw new Error(`Flow run still running after ${timeout}ms: ${run.runId}`);
@@ -107,7 +119,7 @@ export async function waitForRun(cwd, runIdOrPrefix, timeoutMs, options = {}) {
107
119
  run = await refreshRun(cwd, run.runId);
108
120
  const remaining = deadline - Date.now();
109
121
  if (remaining <= 0) {
110
- if (run.status !== "running")
122
+ if (!hasActiveSchedulerWork(run))
111
123
  return run;
112
124
  throw new Error(`Flow run still running after ${timeout}ms: ${run.runId}`);
113
125
  }
@@ -116,6 +128,45 @@ export async function waitForRun(cwd, runIdOrPrefix, timeoutMs, options = {}) {
116
128
  }
117
129
  return run;
118
130
  }
131
+ function assertBlockedRunResumable(run) {
132
+ if (run.status !== "blocked")
133
+ return;
134
+ const blockers = run.tasks.filter((task) => task.status === "blocked" && !isBlockedTaskResumableForResume(task));
135
+ if (blockers.length === 0)
136
+ return;
137
+ const details = blockers
138
+ .slice(0, 3)
139
+ .map((task) => `${task.specId} statusDetail=${task.statusDetail}`)
140
+ .join(", ");
141
+ throw new Error(`Cannot resume blocked run ${run.runId}: non-resumable blocked task(s): ${details}. Resolve the attention/approval blocker before resuming.`);
142
+ }
143
+ export async function stopRun(cwd, runIdOrPrefix) {
144
+ const current = await readRunRecord(cwd, runIdOrPrefix);
145
+ const stopped = await withRunLease(cwd, current.runId, async () => {
146
+ const run = await readRunRecord(cwd, current.runId);
147
+ if (isTerminalWorkflowStatus(run.status)) {
148
+ throw new Error(`stop requires a non-terminal run; ${run.runId} is ${run.status}`);
149
+ }
150
+ await resolveWorkflowBackend(run)
151
+ .cleanupRun(cwd, run)
152
+ .catch(() => undefined);
153
+ const interruptedTaskIds = [];
154
+ for (const task of run.tasks) {
155
+ if (setTaskTerminal(task, "interrupted", "workflow_stopped", {
156
+ exitCode: 130,
157
+ lastMessage: "Workflow stopped by user request",
158
+ })) {
159
+ interruptedTaskIds.push(task.taskId);
160
+ }
161
+ }
162
+ await writeRunRecord(cwd, run);
163
+ unwatchRun(cwd, run.runId);
164
+ return { run, interruptedTaskIds };
165
+ });
166
+ if (!stopped)
167
+ throw new Error(`Could not acquire workflow run lease for ${current.runId}`);
168
+ return stopped;
169
+ }
119
170
  export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
120
171
  const current = await readRunRecord(cwd, runIdOrPrefix);
121
172
  if (current.status !== "failed" &&
@@ -123,6 +174,7 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
123
174
  current.status !== "blocked") {
124
175
  throw new Error(`resume requires a failed, interrupted, or resumable blocked run; ${current.runId} is ${current.status}`);
125
176
  }
177
+ assertBlockedRunResumable(current);
126
178
  const compiledFlow = await readCompiledWorkflow(cwd, current.runId);
127
179
  const hasLoopTasks = compiledFlow?.tasks.some((task) => task.kind === "loop" ||
128
180
  task.loopPlaceholder !== undefined ||
@@ -133,6 +185,10 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
133
185
  const resetTaskIds = [];
134
186
  const updated = await withRunLease(cwd, current.runId, async () => {
135
187
  const run = await readRunRecord(cwd, current.runId);
188
+ assertBlockedRunResumable(run);
189
+ await resolveWorkflowBackend(run)
190
+ .cleanupRun(cwd, run)
191
+ .catch(() => undefined);
136
192
  for (const task of run.tasks) {
137
193
  if (resetTaskForResume(task))
138
194
  resetTaskIds.push(task.taskId);
@@ -147,7 +203,7 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
147
203
  throw new Error(`No failed, interrupted, skipped, or resumable blocked tasks to resume in ${current.runId}`);
148
204
  const scheduled = (await scheduleRun(cwd, current.runId, undefined, options)) ??
149
205
  (await readRunRecord(cwd, current.runId));
150
- if (scheduled.status === "running")
206
+ if (shouldWatchRun(scheduled))
151
207
  watchRun(cwd, scheduled.runId, options);
152
208
  return { run: scheduled, resetTaskIds };
153
209
  }
@@ -155,7 +211,7 @@ export async function resumeSupervisors(cwd, options = {}) {
155
211
  try {
156
212
  const runs = await listRunRecords(cwd);
157
213
  for (const run of runs) {
158
- if (run.status === "running") {
214
+ if (hasActiveSchedulerWork(run)) {
159
215
  await scheduleRun(cwd, run.runId, undefined, options).catch((error) => recordSupervisorError(cwd, run.runId, error));
160
216
  watchRun(cwd, run.runId, options);
161
217
  }
@@ -166,6 +222,15 @@ export async function resumeSupervisors(cwd, options = {}) {
166
222
  await recordSupervisorError(cwd, "index", error);
167
223
  }
168
224
  }
225
+ function unwatchRun(cwd, runId) {
226
+ const key = `${cwd}\0${runId}`;
227
+ const existing = supervisorTimers.get(key);
228
+ if (existing)
229
+ clearInterval(existing);
230
+ supervisorTimers.delete(key);
231
+ supervisorRunMtimes.delete(key);
232
+ supervisorErrorCounts.delete(key);
233
+ }
169
234
  export function watchRun(cwd, runId, options = {}) {
170
235
  const key = `${cwd}\0${runId}`;
171
236
  if (supervisorTimers.has(key))
@@ -179,7 +244,8 @@ export function watchRun(cwd, runId, options = {}) {
179
244
  const currentMtime = afterMtime ?? beforeMtime;
180
245
  if (currentMtime !== undefined)
181
246
  supervisorRunMtimes.set(key, currentMtime);
182
- if (refreshed.status === "running") {
247
+ supervisorErrorCounts.delete(key);
248
+ if (hasActiveSchedulerWork(refreshed)) {
183
249
  const unchanged = previousMtime !== undefined &&
184
250
  currentMtime !== undefined &&
185
251
  currentMtime <= previousMtime;
@@ -187,13 +253,18 @@ export function watchRun(cwd, runId, options = {}) {
187
253
  await scheduleRun(cwd, runId, undefined, options);
188
254
  return;
189
255
  }
190
- const existing = supervisorTimers.get(key);
191
- if (existing)
192
- clearInterval(existing);
193
- supervisorTimers.delete(key);
194
- supervisorRunMtimes.delete(key);
256
+ unwatchRun(cwd, runId);
195
257
  })().catch((error) => {
196
- void recordSupervisorError(cwd, runId, error);
258
+ if (isMissingRunError(error)) {
259
+ unwatchRun(cwd, runId);
260
+ return;
261
+ }
262
+ const failures = (supervisorErrorCounts.get(key) ?? 0) + 1;
263
+ supervisorErrorCounts.set(key, failures);
264
+ void recordSupervisorError(cwd, runId, error).finally(() => {
265
+ if (failures >= MAX_SUPERVISOR_CONSECUTIVE_ERRORS)
266
+ unwatchRun(cwd, runId);
267
+ });
197
268
  });
198
269
  }, POLL_INTERVAL_MS);
199
270
  timer.unref?.();
@@ -204,16 +275,38 @@ async function readRunMtimeMs(cwd, runId) {
204
275
  return (await stat(workflowRunPath(cwd, runId))).mtimeMs;
205
276
  }
206
277
  catch (error) {
207
- if (error.code === "ENOENT")
278
+ if (isEnoentError(error))
208
279
  return undefined;
209
280
  throw error;
210
281
  }
211
282
  }
283
+ function isEnoentError(error) {
284
+ return error?.code === "ENOENT";
285
+ }
286
+ function isMissingRunError(error) {
287
+ return (isEnoentError(error) ||
288
+ (error instanceof Error && /^Flow run not found: /.test(error.message)));
289
+ }
290
+ function assertScheduleLeaseActive(signal) {
291
+ if (!signal?.aborted)
292
+ return;
293
+ const reason = signal.reason;
294
+ if (reason instanceof Error)
295
+ throw reason;
296
+ throw new Error(reason === undefined
297
+ ? "Lost supervisor lease"
298
+ : `Lost supervisor lease: ${String(reason)}`);
299
+ }
212
300
  export async function scheduleRun(cwd, runId, compiled, options = {}) {
213
- return withRunLease(cwd, runId, async () => {
301
+ return withRunLease(cwd, runId, async (leaseSignal) => {
302
+ assertScheduleLeaseActive(leaseSignal);
214
303
  let run = await readRunRecord(cwd, runId);
215
304
  run = await resolveWorkflowBackend(run).refreshRun(cwd, run);
216
- if (run.taskSummary.blocked > 0 || isTerminalWorkflowStatus(run.status))
305
+ if (isTerminalWorkflowStatus(run.status))
306
+ return run;
307
+ if (run.taskSummary.blocked > 0 &&
308
+ run.taskSummary.pending === 0 &&
309
+ run.taskSummary.running === 0)
217
310
  return run;
218
311
  const compiledFlow = compiled ?? (await readCompiledWorkflow(cwd, run.runId));
219
312
  if (!compiledFlow)
@@ -221,7 +314,8 @@ export async function scheduleRun(cwd, runId, compiled, options = {}) {
221
314
  if (compiledFlow.type !== WORKFLOW_RUN_TYPE) {
222
315
  throw new Error(`unsupported compiled workflow type: ${compiledFlow.type}`);
223
316
  }
224
- await scheduleDag(cwd, run, compiledFlow, options);
317
+ await scheduleDag(cwd, run, compiledFlow, options, leaseSignal);
318
+ assertScheduleLeaseActive(leaseSignal);
225
319
  run = await readRunRecord(cwd, run.runId);
226
320
  return run;
227
321
  });
@@ -233,13 +327,13 @@ export async function formatStatus(cwd) {
233
327
  const refreshed = (await readIndex(cwd).catch(() => cached)) ?? cached;
234
328
  if (refreshed.runs.length === 0)
235
329
  return "No workflow runs found.";
236
- return formatIndex(refreshed);
330
+ return formatIndex(cwd, refreshed);
237
331
  }
238
332
  await reconcileActiveRuns(cwd);
239
333
  const rebuilt = await updateIndex(cwd).catch(() => readIndex(cwd));
240
334
  if (!rebuilt || rebuilt.runs.length === 0)
241
335
  return "No workflow runs found.";
242
- return formatIndex(rebuilt);
336
+ return formatIndex(cwd, rebuilt);
243
337
  }
244
338
  export async function formatRunDetails(cwd, runIdOrPrefix) {
245
339
  const run = await refreshRun(cwd, runIdOrPrefix);
@@ -270,10 +364,12 @@ export async function formatLogs(cwd, runIdOrPrefix, taskId = "task-1", lineCoun
270
364
  return `${run.runId}/${task.taskId} output=${task.files.output}\n${tail || "(empty log)"}`;
271
365
  }
272
366
  export function formatRun(run, detail = "summary") {
367
+ const telemetry = summarizeWorkflowTelemetry(run);
273
368
  const lines = [
274
369
  `${run.runId} [${run.status}] type=${run.type} backend=${run.backend.type}/${run.backend.mode}`,
275
370
  `created=${run.createdAt} updated=${run.updatedAt}`,
276
371
  `tasks=${run.taskSummary.completed}/${run.taskSummary.total} completed, running=${run.taskSummary.running}, pending=${run.taskSummary.pending}, blocked=${run.taskSummary.blocked}, failed=${run.taskSummary.failed}, interrupted=${run.taskSummary.interrupted}`,
372
+ `completion=${telemetry.completion.health}, outputRetries=${telemetry.retryCounts.output}, launchRetries=${telemetry.retryCounts.launch}, resumeEvents=${telemetry.resumeCounts.events}, contextLimitFailures=${telemetry.completion.contextLimitFailures}`,
277
373
  ];
278
374
  for (const task of run.tasks) {
279
375
  lines.push(formatTask(task, detail));
@@ -283,13 +379,13 @@ export function formatRun(run, detail = "summary") {
283
379
  async function reconcileActiveRuns(cwd) {
284
380
  const runs = await listRunRecords(cwd);
285
381
  for (const run of runs) {
286
- if (run.status === "running")
382
+ if (hasActiveSchedulerWork(run))
287
383
  await refreshRun(cwd, run.runId).catch((error) => recordSupervisorError(cwd, run.runId, error));
288
384
  }
289
385
  }
290
386
  async function reconcileIndexedActiveRuns(cwd, index) {
291
387
  for (const run of index.runs) {
292
- if (run.status === "running")
388
+ if (hasActiveSchedulerWork(run))
293
389
  await refreshRun(cwd, run.runId).catch((error) => recordSupervisorError(cwd, run.runId, error));
294
390
  }
295
391
  }
@@ -306,11 +402,18 @@ async function recordSupervisorError(cwd, runId, error) {
306
402
  error: error instanceof Error ? error.message : String(error),
307
403
  }).catch(() => undefined);
308
404
  }
309
- async function scheduleDag(cwd, run, compiledFlow, options = {}) {
405
+ async function scheduleDag(cwd, run, compiledFlow, options = {}, leaseSignal) {
406
+ assertScheduleLeaseActive(leaseSignal);
310
407
  if (compiledFlow.type === WORKFLOW_RUN_TYPE) {
311
408
  const loopReconciled = await reconcileLoopTaskMaterialization(cwd, run, compiledFlow);
312
409
  if (loopReconciled)
313
410
  return;
411
+ const foreachReconciled = reconcileForeachGeneratedRunRecords(cwd, run, compiledFlow);
412
+ if (foreachReconciled) {
413
+ await writeJsonAtomic(compiledWorkflowPath(cwd, run.runId), compiledFlow);
414
+ await writeRunRecord(cwd, run);
415
+ return;
416
+ }
314
417
  const dynamicReconciled = reconcileDynamicGeneratedRunRecords(cwd, run, compiledFlow);
315
418
  const staleDynamicRecovered = recoverStaleRunningDynamicControllers(run, compiledFlow);
316
419
  if (dynamicReconciled || staleDynamicRecovered)
@@ -326,6 +429,7 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
326
429
  let running = run.tasks.filter((task) => task.status === "running").length;
327
430
  const bySpecId = new Map(run.tasks.map((task) => [task.specId, task]));
328
431
  for (let index = 0; index < run.tasks.length && running < maxConcurrency; index += 1) {
432
+ assertScheduleLeaseActive(leaseSignal);
329
433
  const task = run.tasks[index];
330
434
  const compiledTask = compiledFlow.tasks[index];
331
435
  if (!task || !compiledTask || task.status !== "pending")
@@ -345,6 +449,8 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
345
449
  const changed = await materializeForeachTask(cwd, run, compiledFlow, index, compiledTask);
346
450
  if (changed)
347
451
  return;
452
+ if (foreachStreamingEnabled(compiledTask))
453
+ continue;
348
454
  }
349
455
  if (compiledTask.stageMaxConcurrency !== undefined) {
350
456
  const runningInStage = run.tasks.filter((candidate) => candidate.stageId === compiledTask.stageId &&
@@ -353,8 +459,10 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
353
459
  Math.max(1, Math.min(MAX_CONCURRENCY, compiledTask.stageMaxConcurrency)))
354
460
  continue;
355
461
  }
462
+ assertScheduleLeaseActive(leaseSignal);
356
463
  const launched = await launchPendingTaskAt(cwd, run, compiledFlow, index, options);
357
- if (launched)
464
+ assertScheduleLeaseActive(leaseSignal);
465
+ if (launched && run.tasks[index]?.status === "running")
358
466
  running += 1;
359
467
  }
360
468
  }
@@ -433,10 +541,12 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
433
541
  return false;
434
542
  const sourceStageIds = sourceStageIdsForFrom(template.foreach.from);
435
543
  const sourceTasks = run.tasks.filter((task) => sourceStageIds.includes(task.stageId ?? ""));
544
+ const streaming = foreachStreamingEnabled(template);
436
545
  const extracted = await extractArtifactGraphForeachItems(cwd, {
437
546
  from: template.foreach.from,
438
547
  sourcePolicy: stageSourcePolicy(compiledFlow, template.stageId),
439
548
  maxItems: template.foreach.maxItems,
549
+ streaming,
440
550
  }, sourceTasks);
441
551
  if (extracted.error) {
442
552
  setTaskTerminal(templateRunTask, "blocked", "foreach_expansion_blocked", {
@@ -455,7 +565,30 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
455
565
  return true;
456
566
  }
457
567
  const placeholderSpecId = template.id;
568
+ if (streaming) {
569
+ return await materializeStreamingForeachTask({
570
+ cwd,
571
+ run,
572
+ compiledFlow,
573
+ index,
574
+ templateRunTask,
575
+ placeholderSpecId,
576
+ sourceTaskSpecIds: sourceTasks.map((task) => task.specId),
577
+ itemMetas: extracted.itemMetas ?? [],
578
+ generatedTasks: generated.tasks,
579
+ waitingForSources: extracted.waitingForSources ?? false,
580
+ minChunk: foreachStreamingMinChunk(template),
581
+ });
582
+ }
458
583
  const generatedSpecIds = generated.tasks.map((task) => task.id);
584
+ const hasDownstreamDependents = compiledFlow.tasks.some((task, taskIndex) => taskIndex !== index && (task.dependsOn ?? []).includes(placeholderSpecId));
585
+ if (generatedSpecIds.length === 0 && !hasDownstreamDependents) {
586
+ setTaskTerminal(templateRunTask, "completed", "foreach_empty", {
587
+ lastMessage: "foreach produced 0 item(s)",
588
+ });
589
+ await writeRunRecord(cwd, run);
590
+ return true;
591
+ }
459
592
  compiledFlow.tasks.splice(index, 1, ...generated.tasks);
460
593
  updateDownstreamDependencies(compiledFlow, placeholderSpecId, generatedSpecIds);
461
594
  const nextIndex = nextTaskRecordIndex(run);
@@ -470,16 +603,183 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
470
603
  await writeRunRecord(cwd, run);
471
604
  return true;
472
605
  }
606
+ async function materializeStreamingForeachTask(input) {
607
+ const sourceTaskSpecIdSet = new Set(input.sourceTaskSpecIds);
608
+ const generatedTasksWithItemDeps = input.generatedTasks.map((task, index) => {
609
+ const itemMeta = input.itemMetas[index];
610
+ if (!itemMeta)
611
+ return task;
612
+ return {
613
+ ...task,
614
+ dependsOn: replaceSourceDependenciesWithItemSource(task.dependsOn ?? [], sourceTaskSpecIdSet, itemMeta, {
615
+ keepPartialSourceDependency: partialGeneratedTaskNeedsCompletedSourceContext(task),
616
+ }),
617
+ foreachGenerated: {
618
+ ...(task.foreachGenerated ?? {
619
+ placeholderSpecId: input.placeholderSpecId,
620
+ }),
621
+ itemHash: itemMeta.itemHash,
622
+ itemSourceSpecId: itemMeta.sourceSpecId,
623
+ itemSourceKind: itemMeta.sourceKind,
624
+ itemRef: itemMeta.itemRef,
625
+ },
626
+ };
627
+ });
628
+ const existingGeneratedTasks = input.compiledFlow.tasks.filter((task) => task.foreachGenerated?.placeholderSpecId === input.placeholderSpecId);
629
+ const existingGeneratedSpecIds = existingGeneratedTasks.map((task) => task.id);
630
+ const existingGeneratedTaskBySpecId = new Map(existingGeneratedTasks.map((task) => [task.id, task]));
631
+ for (const task of generatedTasksWithItemDeps) {
632
+ const existing = existingGeneratedTaskBySpecId.get(task.id);
633
+ const existingHash = existing?.foreachGenerated?.itemHash;
634
+ const nextHash = task.foreachGenerated?.itemHash;
635
+ if (existing && existingHash && nextHash && existingHash !== nextHash) {
636
+ setTaskTerminal(input.templateRunTask, "blocked", "foreach_expansion_blocked", {
637
+ lastMessage: `foreach streaming item ${task.id} changed after materialization`,
638
+ });
639
+ await writeRunRecord(input.cwd, input.run);
640
+ return true;
641
+ }
642
+ }
643
+ const existingGeneratedSpecIdSet = new Set(existingGeneratedSpecIds);
644
+ const finalGeneratedSpecIdSet = new Set(generatedTasksWithItemDeps.map((task) => task.id));
645
+ if (!input.waitingForSources) {
646
+ const withdrawn = existingGeneratedTasks.find((task) => task.foreachGenerated?.itemSourceKind === "partial" &&
647
+ !finalGeneratedSpecIdSet.has(task.id));
648
+ if (withdrawn) {
649
+ setTaskTerminal(input.templateRunTask, "blocked", "foreach_expansion_blocked", {
650
+ lastMessage: `foreach streaming item ${withdrawn.id} was published as partial output but is missing from final control`,
651
+ });
652
+ await writeRunRecord(input.cwd, input.run);
653
+ return true;
654
+ }
655
+ }
656
+ const newGeneratedTasks = generatedTasksWithItemDeps.filter((task) => !existingGeneratedSpecIdSet.has(task.id));
657
+ const allGeneratedSpecIds = [
658
+ ...existingGeneratedSpecIds,
659
+ ...newGeneratedTasks.map((task) => task.id),
660
+ ];
661
+ const shouldHoldForMinChunk = input.waitingForSources &&
662
+ newGeneratedTasks.length > 0 &&
663
+ newGeneratedTasks.length < input.minChunk;
664
+ if (shouldHoldForMinChunk)
665
+ return false;
666
+ let changed = false;
667
+ if (newGeneratedTasks.length > 0) {
668
+ let compiledInsertIndex = input.index + 1;
669
+ while (input.compiledFlow.tasks[compiledInsertIndex]?.foreachGenerated
670
+ ?.placeholderSpecId === input.placeholderSpecId) {
671
+ compiledInsertIndex += 1;
672
+ }
673
+ input.compiledFlow.tasks.splice(compiledInsertIndex, 0, ...newGeneratedTasks);
674
+ let runInsertIndex = input.index + 1;
675
+ while (input.run.tasks[runInsertIndex]?.foreachGenerated?.placeholderSpecId ===
676
+ input.placeholderSpecId) {
677
+ runInsertIndex += 1;
678
+ }
679
+ const nextIndex = nextTaskRecordIndex(input.run);
680
+ const generatedRunTasks = newGeneratedTasks.map((task, offset) => createTaskRunRecord(input.cwd, input.run.runId, task, nextIndex + offset));
681
+ input.run.tasks.splice(runInsertIndex, 0, ...generatedRunTasks);
682
+ changed = true;
683
+ }
684
+ const dependencyTargets = [input.placeholderSpecId, ...allGeneratedSpecIds];
685
+ for (const task of input.compiledFlow.tasks) {
686
+ if (!task.dependsOn)
687
+ continue;
688
+ const replaced = replaceDependencyList(task.dependsOn, input.placeholderSpecId, dependencyTargets);
689
+ if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
690
+ task.dependsOn = replaced;
691
+ changed = true;
692
+ }
693
+ }
694
+ for (const task of input.run.tasks) {
695
+ if (!task.dependsOn)
696
+ continue;
697
+ const replaced = replaceDependencyList(task.dependsOn, input.placeholderSpecId, dependencyTargets);
698
+ if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
699
+ task.dependsOn = replaced;
700
+ changed = true;
701
+ }
702
+ }
703
+ if (!input.waitingForSources) {
704
+ const statusDetail = allGeneratedSpecIds.length === 0
705
+ ? "foreach_empty"
706
+ : "foreach_streaming_complete";
707
+ const lastMessage = allGeneratedSpecIds.length === 0
708
+ ? "foreach produced 0 item(s)"
709
+ : `foreach streaming materialized ${allGeneratedSpecIds.length} item(s)`;
710
+ setTaskTerminal(input.templateRunTask, "completed", statusDetail, {
711
+ lastMessage,
712
+ });
713
+ changed = true;
714
+ }
715
+ else if (newGeneratedTasks.length > 0) {
716
+ input.templateRunTask.statusDetail = "foreach_streaming_waiting";
717
+ input.templateRunTask.lastMessage = `foreach streaming materialized ${allGeneratedSpecIds.length} item(s); waiting for more source tasks`;
718
+ changed = true;
719
+ }
720
+ if (!changed)
721
+ return false;
722
+ await writeJsonAtomic(compiledWorkflowPath(input.cwd, input.run.runId), input.compiledFlow);
723
+ await writeRunRecord(input.cwd, input.run);
724
+ return true;
725
+ }
726
+ function replaceSourceDependenciesWithItemSource(dependsOn, sourceTaskSpecIds, itemMeta, options = {}) {
727
+ const replaced = [];
728
+ let inserted = false;
729
+ const shouldReplaceWithSource = itemMeta.sourceKind !== "partial" ||
730
+ options.keepPartialSourceDependency === true;
731
+ for (const dep of dependsOn) {
732
+ if (!sourceTaskSpecIds.has(dep)) {
733
+ replaced.push(dep);
734
+ continue;
735
+ }
736
+ if (!shouldReplaceWithSource)
737
+ continue;
738
+ if (!inserted) {
739
+ replaced.push(itemMeta.sourceSpecId);
740
+ inserted = true;
741
+ }
742
+ }
743
+ if (!inserted && shouldReplaceWithSource) {
744
+ replaced.push(itemMeta.sourceSpecId);
745
+ }
746
+ return [...new Set(replaced)];
747
+ }
748
+ function partialGeneratedTaskNeedsCompletedSourceContext(task) {
749
+ const artifactGraph = task.artifactGraph;
750
+ if (artifactGraph?.artifactAccess === "none")
751
+ return false;
752
+ return Boolean(artifactGraph?.sourceProjection !== undefined ||
753
+ (artifactGraph?.requiredReads?.length ?? 0) > 0);
754
+ }
473
755
  async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
474
756
  const items = [];
757
+ const itemMetas = [];
475
758
  const path = stage.from?.path;
476
759
  if (typeof path !== "string" || !path.startsWith("$.")) {
477
760
  return {
478
761
  error: "foreach.from.path must be a control JSONPath like $.items",
479
762
  };
480
763
  }
764
+ let waitingForSources = false;
481
765
  for (const task of sourceTasks) {
482
766
  if (task.status !== "completed") {
767
+ if (stage.streaming && !isTerminalTaskStatus(task.status)) {
768
+ const partial = await extractPartialForeachItems(cwd, task, path);
769
+ if (partial.error)
770
+ return { error: partial.error };
771
+ for (const item of partial.items) {
772
+ items.push(item.item);
773
+ itemMetas.push({
774
+ sourceSpecId: task.specId,
775
+ sourceKind: "partial",
776
+ itemHash: item.itemHash,
777
+ itemRef: `${task.specId}:${item.itemRef}`,
778
+ });
779
+ }
780
+ waitingForSources = true;
781
+ continue;
782
+ }
483
783
  if (stage.sourcePolicy !== "partial")
484
784
  return { error: `${task.taskId} did not complete` };
485
785
  continue;
@@ -495,7 +795,15 @@ async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
495
795
  }
496
796
  continue;
497
797
  }
498
- items.push(...value);
798
+ for (const [index, item] of value.entries()) {
799
+ items.push(item);
800
+ itemMetas.push({
801
+ sourceSpecId: task.specId,
802
+ sourceKind: "control",
803
+ itemHash: hashDynamicRequest(item),
804
+ itemRef: `${task.specId}:control:${path}[${index}]`,
805
+ });
806
+ }
499
807
  }
500
808
  catch (error) {
501
809
  if (stage.sourcePolicy !== "partial") {
@@ -510,7 +818,27 @@ async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
510
818
  error: `foreach extracted ${items.length} items, exceeding maxItems=${stage.maxItems}`,
511
819
  };
512
820
  }
513
- return { items };
821
+ return { items, itemMetas, waitingForSources };
822
+ }
823
+ async function extractPartialForeachItems(cwd, task, path) {
824
+ const partialPaths = task.artifactGraph?.output.partial?.paths ?? [];
825
+ if (!partialPaths.includes(path))
826
+ return { items: [] };
827
+ const taskDir = dirname(fromProjectPath(cwd, task.files.result));
828
+ let ledger = await readWorkflowPartialOutputLedger(taskDir).catch(() => undefined);
829
+ if (!ledger) {
830
+ ledger = await writeWorkflowPartialOutputLedgerFromFile({
831
+ taskDir,
832
+ outputFile: fromProjectPath(cwd, task.files.output),
833
+ allowedPaths: partialPaths,
834
+ }).catch(() => undefined);
835
+ }
836
+ if (!ledger)
837
+ return { items: [] };
838
+ const fatal = hasFatalPartialOutputIssue(ledger);
839
+ if (fatal)
840
+ return { items: [], error: fatal.message };
841
+ return { items: ledger.items.filter((item) => item.path === path) };
514
842
  }
515
843
  async function launchPendingTaskAt(cwd, run, compiledFlow, index, options = {}) {
516
844
  const task = run.tasks[index];
@@ -526,11 +854,14 @@ async function launchPendingTaskAt(cwd, run, compiledFlow, index, options = {})
526
854
  await writeRunRecord(cwd, run);
527
855
  return false;
528
856
  }
529
- let launchTask = await prepareDagTask(cwd, run, compiledFlow, index);
530
- if (task.outputRetry) {
531
- launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
532
- }
857
+ let launchTask;
858
+ let prepareComplete = false;
533
859
  try {
860
+ launchTask = await prepareDagTask(cwd, run, compiledFlow, index);
861
+ if (task.outputRetry) {
862
+ launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
863
+ }
864
+ prepareComplete = true;
534
865
  if (launchTask.kind === "support") {
535
866
  return await executeSupportTask(cwd, run, task, launchTask);
536
867
  }
@@ -547,11 +878,13 @@ async function launchPendingTaskAt(cwd, run, compiledFlow, index, options = {})
547
878
  return launch.kind === "launched";
548
879
  }
549
880
  catch (error) {
550
- const statusDetail = launchTask.kind === "support"
551
- ? "support_failed"
552
- : launchTask.safety.requiresWorktree
553
- ? "worktree_failed"
554
- : "launch_failed";
881
+ const statusDetail = !prepareComplete
882
+ ? "prepare_failed"
883
+ : launchTask?.kind === "support"
884
+ ? "support_failed"
885
+ : launchTask?.safety.requiresWorktree
886
+ ? "worktree_failed"
887
+ : "launch_failed";
555
888
  setTaskTerminal(task, "failed", statusDetail, {
556
889
  lastMessage: error instanceof Error ? error.message : String(error),
557
890
  });
@@ -1815,21 +2148,36 @@ function uniqueStrings(values) {
1815
2148
  async function readCompiledWorkflow(cwd, runId) {
1816
2149
  return readJson(compiledWorkflowPath(cwd, runId));
1817
2150
  }
1818
- function formatIndex(index) {
1819
- return index.runs
1820
- .map((run) => {
2151
+ async function formatIndex(cwd, index) {
2152
+ const blocks = await Promise.all(index.runs.map(async (run) => {
1821
2153
  const lines = [
1822
2154
  `${run.runId} [${run.status}] type=${run.type} updated=${run.updatedAt}`,
1823
2155
  `tasks=${run.taskSummary.completed}/${run.taskSummary.total} completed, running=${run.taskSummary.running}, pending=${run.taskSummary.pending}, blocked=${run.taskSummary.blocked}, failed=${run.taskSummary.failed}, skipped=${run.taskSummary.skipped}, interrupted=${run.taskSummary.interrupted}`,
1824
2156
  ];
1825
- for (const task of run.tasks) {
2157
+ for (const task of await indexTasksForStatus(cwd, run)) {
1826
2158
  const message = task.lastMessage ? ` — ${task.lastMessage}` : "";
1827
2159
  const kind = task.kind && task.kind !== "main" ? ` ${task.kind}` : "";
1828
2160
  lines.push(`- ${task.taskId}${kind} ${task.agent} [${task.status}/${task.statusDetail}]${message}`);
1829
2161
  }
1830
2162
  return lines.join("\n");
1831
- })
1832
- .join("\n\n");
2163
+ }));
2164
+ return blocks.join("\n\n");
2165
+ }
2166
+ async function indexTasksForStatus(cwd, run) {
2167
+ if (Array.isArray(run.tasks))
2168
+ return run.tasks;
2169
+ const fullRun = await readRunRecord(cwd, run.runId).catch(() => undefined);
2170
+ return (fullRun?.tasks.map((task) => ({
2171
+ taskId: task.taskId,
2172
+ displayName: task.displayName,
2173
+ agent: task.agent,
2174
+ kind: task.kind,
2175
+ stageId: task.stageId,
2176
+ backendHandle: task.backendHandle,
2177
+ status: task.status,
2178
+ statusDetail: task.statusDetail,
2179
+ lastMessage: task.lastMessage,
2180
+ })) ?? []);
1833
2181
  }
1834
2182
  function formatTask(task, detail) {
1835
2183
  const elapsed = task.elapsedMs !== undefined