@agwab/pi-workflow 0.2.1 → 0.3.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 (70) hide show
  1. package/dist/compiler.js +6 -8
  2. package/dist/dynamic-decision.d.ts +0 -1
  3. package/dist/dynamic-decision.js +0 -7
  4. package/dist/dynamic-profiles.d.ts +0 -1
  5. package/dist/dynamic-profiles.js +0 -3
  6. package/dist/engine-run-graph.d.ts +1 -0
  7. package/dist/engine-run-graph.js +142 -2
  8. package/dist/engine.d.ts +5 -0
  9. package/dist/engine.js +112 -27
  10. package/dist/extension.d.ts +2 -1
  11. package/dist/extension.js +27 -6
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +2 -1
  14. package/dist/store.js +55 -11
  15. package/dist/subagent-backend.js +155 -29
  16. package/dist/types.d.ts +6 -0
  17. package/dist/workflow-runtime.js +10 -1
  18. package/dist/workflow-view.js +3 -1
  19. package/dist/workflow-web-source-extension.js +167 -48
  20. package/dist/workflow-web-source.d.ts +2 -1
  21. package/dist/workflow-web-source.js +84 -19
  22. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  23. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  24. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  25. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  26. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  27. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  28. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  29. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  30. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  31. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  32. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  33. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  34. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  35. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  36. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  37. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  38. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  39. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  40. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  41. package/package.json +2 -2
  42. package/src/compiler.ts +14 -9
  43. package/src/dynamic-decision.ts +0 -11
  44. package/src/dynamic-profiles.ts +0 -4
  45. package/src/engine-run-graph.ts +185 -2
  46. package/src/engine.ts +145 -24
  47. package/src/extension.ts +33 -4
  48. package/src/index.ts +3 -1
  49. package/src/store.ts +74 -11
  50. package/src/subagent-backend.ts +201 -28
  51. package/src/types.ts +6 -0
  52. package/src/workflow-runtime.ts +18 -2
  53. package/src/workflow-view.ts +2 -1
  54. package/src/workflow-web-source-extension.ts +621 -228
  55. package/src/workflow-web-source.ts +118 -28
  56. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  57. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  58. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  59. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  60. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  61. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  62. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  63. package/workflows/impact-review/spec.json +3 -3
  64. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  65. package/dist/dynamic-loader.d.ts +0 -25
  66. package/dist/dynamic-loader.js +0 -13
  67. package/src/dynamic-loader.ts +0 -49
  68. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  69. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  70. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -57,7 +57,12 @@ const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
57
57
  const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
58
58
  const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
59
59
  const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
60
+ const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
61
+ const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
62
+ const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
63
+ const PARENT_SUBAGENT_ATTEMPT_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_ATTEMPT_ID";
60
64
  const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
65
+ const STALE_LAUNCH_CLAIM_GRACE_MS = 30_000;
61
66
  const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
62
67
  const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
63
68
  const MODULE_PATH = fileURLToPath(import.meta.url);
@@ -161,8 +166,31 @@ interface SubagentApi {
161
166
  ): Promise<SubagentRunStatusSnapshot | null>;
162
167
  interruptSubagent(options: Record<string, unknown>): Promise<unknown>;
163
168
  reconcileSubagentRun(options: Record<string, unknown>): Promise<unknown>;
169
+ recordSubagentChildEvent?(
170
+ options: Record<string, unknown>,
171
+ ): Promise<unknown>;
172
+ }
173
+
174
+ type ParentSubagentChildEvent =
175
+ | "started"
176
+ | "completed"
177
+ | "failed"
178
+ | "cancelled";
179
+
180
+ interface ParentSubagentRef {
181
+ cwd: string;
182
+ runsDir: string;
183
+ runId: string;
184
+ attemptId?: string;
164
185
  }
165
186
 
187
+ const GENERIC_TASK_STATUS_DETAILS = new Set([
188
+ "completed",
189
+ "failed",
190
+ "interrupted",
191
+ "running",
192
+ ]);
193
+
166
194
  const subagentApiSpecifier = "@agwab/pi-subagent/api";
167
195
  let cachedSubagentApi: Promise<SubagentApi> | undefined;
168
196
  let injectedSubagentApi: SubagentApi | undefined;
@@ -180,6 +208,85 @@ async function loadSubagentApi(): Promise<SubagentApi> {
180
208
  return cachedSubagentApi;
181
209
  }
182
210
 
211
+ function nonEmptyEnv(
212
+ env: Record<string, string | undefined>,
213
+ key: string,
214
+ ): string | undefined {
215
+ const value = env[key]?.trim();
216
+ return value ? value : undefined;
217
+ }
218
+
219
+ function parentSubagentRefFromEnv(
220
+ env: Record<string, string | undefined> = process.env,
221
+ ): ParentSubagentRef | undefined {
222
+ const cwd = nonEmptyEnv(env, PARENT_SUBAGENT_CWD_ENV);
223
+ const runsDir = nonEmptyEnv(env, PARENT_SUBAGENT_RUNS_DIR_ENV);
224
+ const runId = nonEmptyEnv(env, PARENT_SUBAGENT_RUN_ID_ENV);
225
+ if (!cwd || !runsDir || !runId) return undefined;
226
+ const attemptId = nonEmptyEnv(env, PARENT_SUBAGENT_ATTEMPT_ID_ENV);
227
+ return { cwd, runsDir, runId, ...(attemptId ? { attemptId } : {}) };
228
+ }
229
+
230
+ function terminalChildEventForTaskStatus(
231
+ status: WorkflowTaskRunRecord["status"],
232
+ ): ParentSubagentChildEvent | undefined {
233
+ if (status === "completed") return "completed";
234
+ if (status === "failed") return "failed";
235
+ if (status === "interrupted") return "cancelled";
236
+ return undefined;
237
+ }
238
+
239
+ async function recordParentSubagentChildEvent(options: {
240
+ event: ParentSubagentChildEvent;
241
+ childRunId: string;
242
+ run: WorkflowRunRecord;
243
+ task: WorkflowTaskRunRecord;
244
+ failureKind?: string | null;
245
+ message?: string;
246
+ }): Promise<void> {
247
+ const parent = parentSubagentRefFromEnv();
248
+ if (!parent) return;
249
+ const api = await loadSubagentApi().catch(() => undefined);
250
+ if (!api?.recordSubagentChildEvent) return;
251
+ await api
252
+ .recordSubagentChildEvent({
253
+ ...parent,
254
+ event: options.event,
255
+ childRunId: options.childRunId,
256
+ workflowRunId: options.run.runId,
257
+ childTaskId: options.task.taskId,
258
+ ...(options.failureKind === undefined
259
+ ? {}
260
+ : { failureKind: options.failureKind }),
261
+ ...(options.message === undefined ? {} : { message: options.message }),
262
+ })
263
+ .catch(() => undefined);
264
+ }
265
+
266
+ async function recordTerminalParentSubagentChildEvent(
267
+ run: WorkflowRunRecord,
268
+ task: WorkflowTaskRunRecord,
269
+ snapshot: SubagentRunStatusSnapshot,
270
+ ): Promise<void> {
271
+ const event = terminalChildEventForTaskStatus(task.status);
272
+ if (!event) return;
273
+ const taskFailureKind =
274
+ task.statusDetail && !GENERIC_TASK_STATUS_DETAILS.has(task.statusDetail)
275
+ ? task.statusDetail
276
+ : undefined;
277
+ await recordParentSubagentChildEvent({
278
+ event,
279
+ childRunId: snapshot.runId,
280
+ run,
281
+ task,
282
+ failureKind:
283
+ event === "completed"
284
+ ? undefined
285
+ : (snapshot.failureKind ?? taskFailureKind ?? task.statusDetail),
286
+ message: task.lastMessage,
287
+ });
288
+ }
289
+
183
290
  let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
184
291
  let transientRetryJitterForTests: (() => number) | undefined;
185
292
  const launchWaitQueue: Array<() => void> = [];
@@ -225,8 +332,7 @@ function releaseLaunchSlotAfterDelay(
225
332
  release();
226
333
  return;
227
334
  }
228
- const timer = setTimeout(release, delayMs);
229
- timer.unref?.();
335
+ setTimeout(release, delayMs);
230
336
  }
231
337
 
232
338
  async function runWithLaunchSlot<T>(action: () => Promise<T>): Promise<T> {
@@ -282,7 +388,6 @@ export async function cleanupSubagentRun(
282
388
  run: WorkflowRunRecord,
283
389
  ): Promise<void> {
284
390
  for (const task of run.tasks) {
285
- if (isTerminalTaskStatus(task.status)) continue;
286
391
  const handle = getSubagentHandle(task);
287
392
  if (!handle) continue;
288
393
  const api = await loadSubagentApi();
@@ -409,6 +514,13 @@ export async function launchSubagentTask(
409
514
  task.statusDetail = "running";
410
515
  task.lastMessage = "launched via pi-subagent/headless";
411
516
  await writeRunRecord(cwd, run).catch(() => undefined);
517
+ await recordParentSubagentChildEvent({
518
+ event: "started",
519
+ childRunId: launched.runId,
520
+ run,
521
+ task,
522
+ message: task.lastMessage,
523
+ });
412
524
  return { kind: "launched" };
413
525
  }
414
526
 
@@ -440,8 +552,13 @@ export async function refreshRunFromSubagentArtifacts(
440
552
  }
441
553
  }
442
554
  if (!handle) {
555
+ if (isStaleLaunchClaim(task)) {
556
+ resetStaleLaunchClaim(task);
557
+ changed = true;
558
+ continue;
559
+ }
443
560
  if (isTaskTimedOut(task)) {
444
- markTaskTimedOut(task);
561
+ markSubagentTaskTimedOut(task);
445
562
  changed = true;
446
563
  }
447
564
  continue;
@@ -466,16 +583,8 @@ export async function refreshRunFromSubagentArtifacts(
466
583
 
467
584
  if (snapshot === null) {
468
585
  if (isTaskTimedOut(task)) {
469
- await api
470
- .interruptSubagent({
471
- cwd: handle.cwd,
472
- runsDir: handle.runsDir,
473
- runId: handle.runId,
474
- attemptId: handle.attemptId,
475
- reason: "workflow timeout",
476
- })
477
- .catch(() => undefined);
478
- markTaskTimedOut(task);
586
+ await interruptTimedOutSubagent(api, handle);
587
+ markSubagentTaskTimedOut(task);
479
588
  changed = true;
480
589
  }
481
590
  continue;
@@ -492,16 +601,8 @@ export async function refreshRunFromSubagentArtifacts(
492
601
  ? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
493
602
  : "pi-subagent running";
494
603
  if (isTaskTimedOut(task)) {
495
- await api
496
- .interruptSubagent({
497
- cwd: handle.cwd,
498
- runsDir: handle.runsDir,
499
- runId: handle.runId,
500
- attemptId: handle.attemptId,
501
- reason: "workflow timeout",
502
- })
503
- .catch(() => undefined);
504
- markTaskTimedOut(task);
604
+ await interruptTimedOutSubagent(api, handle);
605
+ markSubagentTaskTimedOut(task);
505
606
  changed = true;
506
607
  }
507
608
  continue;
@@ -515,6 +616,48 @@ export async function refreshRunFromSubagentArtifacts(
515
616
  return run;
516
617
  }
517
618
 
619
+ async function interruptTimedOutSubagent(
620
+ api: Awaited<ReturnType<typeof loadSubagentApi>>,
621
+ handle: NonNullable<WorkflowTaskRunRecord["backendHandle"]>,
622
+ ): Promise<void> {
623
+ await api
624
+ .interruptSubagent({
625
+ cwd: handle.cwd,
626
+ runsDir: handle.runsDir,
627
+ runId: handle.runId,
628
+ attemptId: handle.attemptId,
629
+ reason: "workflow timeout",
630
+ })
631
+ .catch(() => undefined);
632
+ }
633
+
634
+ function markSubagentTaskTimedOut(task: WorkflowTaskRunRecord): void {
635
+ markTaskTimedOut(task);
636
+ task.backendHandle = undefined;
637
+ task.backendTaskId = task.taskId;
638
+ task.pid = undefined;
639
+ }
640
+
641
+ function isStaleLaunchClaim(task: WorkflowTaskRunRecord): boolean {
642
+ if (task.statusDetail !== "launching" || !task.startedAt) return false;
643
+ const startedAtMs = Date.parse(task.startedAt);
644
+ return (
645
+ Number.isFinite(startedAtMs) &&
646
+ Date.now() - startedAtMs > STALE_LAUNCH_CLAIM_GRACE_MS
647
+ );
648
+ }
649
+
650
+ function resetStaleLaunchClaim(task: WorkflowTaskRunRecord): void {
651
+ task.status = "pending";
652
+ task.statusDetail = "pending";
653
+ task.startedAt = undefined;
654
+ task.backendHandle = undefined;
655
+ task.backendFiles = undefined;
656
+ task.backendTaskId = task.taskId;
657
+ task.pid = undefined;
658
+ task.lastMessage = "stale pi-subagent launch claim reset";
659
+ }
660
+
518
661
  async function materializeTerminalSubagentResult(
519
662
  cwd: string,
520
663
  run: WorkflowRunRecord,
@@ -593,7 +736,7 @@ async function materializeTerminalSubagentResult(
593
736
  snapshot.metadata?.contextLengthExceeded,
594
737
  );
595
738
  if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
596
- return await materializeTerminalArtifactGraphResult(cwd, run, task, {
739
+ const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
597
740
  outputFile,
598
741
  stderrFile,
599
742
  resultFile,
@@ -602,6 +745,8 @@ async function materializeTerminalSubagentResult(
602
745
  exitCode,
603
746
  subagentResult,
604
747
  });
748
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
749
+ return changed;
605
750
  }
606
751
  if (
607
752
  shouldAttemptArtifactGraphSalvage({
@@ -615,7 +760,7 @@ async function materializeTerminalSubagentResult(
615
760
  snapshot,
616
761
  })
617
762
  ) {
618
- return await materializeTerminalArtifactGraphResult(cwd, run, task, {
763
+ const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
619
764
  outputFile,
620
765
  stderrFile,
621
766
  resultFile,
@@ -629,6 +774,8 @@ async function materializeTerminalSubagentResult(
629
774
  subagentFailureKind: snapshot.failureKind,
630
775
  },
631
776
  });
777
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
778
+ return changed;
632
779
  }
633
780
  const workflowResult = {
634
781
  status: statusInfo.status,
@@ -664,10 +811,12 @@ async function materializeTerminalSubagentResult(
664
811
  ),
665
812
  workflowResult,
666
813
  );
667
- return retryOrFailTransientSubagentFailure(task, {
814
+ const changed = retryOrFailTransientSubagentFailure(task, {
668
815
  reason: statusInfo.failureKind ?? "model",
669
816
  message: errorMessage ?? "pi-subagent run failed before producing output",
670
817
  });
818
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
819
+ return changed;
671
820
  }
672
821
  await writeJson(resultFile, workflowResult);
673
822
 
@@ -682,6 +831,7 @@ async function materializeTerminalSubagentResult(
682
831
  delete task.backendHandle;
683
832
  delete task.backendFiles;
684
833
  }
834
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
685
835
  return changed;
686
836
  }
687
837
 
@@ -1674,6 +1824,7 @@ async function recoverSubagentHandle(
1674
1824
  const runsDir = subagentRunsDir(run, task);
1675
1825
  const absoluteRunsDir = resolve(task.cwd, runsDir);
1676
1826
  const expectedCorrelationId = `${run.runId}:${task.taskId}`;
1827
+ const claimStartedAtMs = timestampMs(task.startedAt);
1677
1828
  const entries = await readdir(absoluteRunsDir, { withFileTypes: true }).catch(
1678
1829
  () => [],
1679
1830
  );
@@ -1688,6 +1839,7 @@ async function recoverSubagentHandle(
1688
1839
  join(absoluteRunsDir, entry.name, "run.json"),
1689
1840
  );
1690
1841
  if (!record || record.correlationId !== expectedCorrelationId) continue;
1842
+ if (isPreClaimSubagentRecord(record, claimStartedAtMs)) continue;
1691
1843
  const attemptId =
1692
1844
  record.activeAttemptId ??
1693
1845
  record.latestAttemptId ??
@@ -1714,6 +1866,20 @@ async function recoverSubagentHandle(
1714
1866
  return candidates[0]?.handle;
1715
1867
  }
1716
1868
 
1869
+ function isPreClaimSubagentRecord(
1870
+ record: SubagentRunRecordLike,
1871
+ claimStartedAtMs: number | undefined,
1872
+ ): boolean {
1873
+ if (claimStartedAtMs === undefined) return false;
1874
+ const recordStartedAtMs =
1875
+ timestampMs(record.startedAt) ??
1876
+ timestampMs(record.attempts?.[0]?.startedAt) ??
1877
+ timestampMs(record.updatedAt);
1878
+ return (
1879
+ recordStartedAtMs !== undefined && recordStartedAtMs < claimStartedAtMs
1880
+ );
1881
+ }
1882
+
1717
1883
  function timestampMs(value: string | undefined): number | undefined {
1718
1884
  if (value === undefined) return undefined;
1719
1885
  const time = Date.parse(value);
@@ -1774,7 +1940,14 @@ function subagentSessionId(
1774
1940
  task: WorkflowTaskRunRecord,
1775
1941
  ): string | undefined {
1776
1942
  if (!task.artifactGraph?.enabled) return undefined;
1777
- return task.outputRetry?.sessionId ?? baseSubagentSessionId(run, task);
1943
+ const baseSessionId = baseSubagentSessionId(run, task);
1944
+ if (task.outputRetry?.sessionId) return task.outputRetry.sessionId;
1945
+ const launchAttempt = task.launchRetry?.attempts ?? 0;
1946
+ if (launchAttempt > 0)
1947
+ return `${baseSessionId}:launch-retry-${launchAttempt}`;
1948
+ const resumeAttempt = task.resumeEvents?.length ?? 0;
1949
+ if (resumeAttempt > 0) return `${baseSessionId}:resume-${resumeAttempt}`;
1950
+ return baseSessionId;
1778
1951
  }
1779
1952
 
1780
1953
  function baseSubagentSessionId(
package/src/types.ts CHANGED
@@ -542,6 +542,9 @@ export interface CompiledTask {
542
542
  branchId?: string;
543
543
  outputProfile?: string;
544
544
  };
545
+ foreachGenerated?: {
546
+ placeholderSpecId: string;
547
+ };
545
548
  loopChild?: CompiledLoopChildTaskRef;
546
549
  loopPlaceholder?: {
547
550
  loopId: string;
@@ -634,6 +637,9 @@ export interface WorkflowTaskRunRecord {
634
637
  branchId?: string;
635
638
  outputProfile?: string;
636
639
  };
640
+ foreachGenerated?: {
641
+ placeholderSpecId: string;
642
+ };
637
643
  launchRetry?: {
638
644
  attempts: number;
639
645
  maxAttempts?: number;
@@ -345,9 +345,25 @@ export function readSimpleJsonPath(value: unknown, path: string): unknown {
345
345
  const parts = path.slice(2).split(".").filter(Boolean);
346
346
  let current = value as any;
347
347
  for (const part of parts) {
348
- if (current === null || typeof current !== "object" || !(part in current))
349
- return undefined;
348
+ if (!canReadJsonPathPart(current, part)) return undefined;
350
349
  current = current[part];
351
350
  }
352
351
  return current;
353
352
  }
353
+
354
+ function canReadJsonPathPart(
355
+ value: unknown,
356
+ part: string,
357
+ ): value is Record<string, unknown> {
358
+ return (
359
+ isSafeJsonPathPart(part) && isRecord(value) && Object.hasOwn(value, part)
360
+ );
361
+ }
362
+
363
+ function isSafeJsonPathPart(part: string): boolean {
364
+ return part !== "__proto__" && part !== "prototype" && part !== "constructor";
365
+ }
366
+
367
+ function isRecord(value: unknown): value is Record<string, unknown> {
368
+ return typeof value === "object" && value !== null;
369
+ }
@@ -1370,10 +1370,11 @@ function statusForSummary(
1370
1370
  ): WorkflowRunStatus | TaskRunStatus {
1371
1371
  if (summary.running > 0) return "running";
1372
1372
  if (summary.blocked > 0) return "blocked";
1373
- if (summary.failed > 0 || summary.interrupted > 0) return "failed";
1373
+ if (summary.failed > 0) return "failed";
1374
1374
  if (summary.pending > 0) return "pending";
1375
1375
  if (summary.total > 0 && summary.completed === summary.total)
1376
1376
  return "completed";
1377
+ if (summary.interrupted > 0) return "interrupted";
1377
1378
  return "interrupted";
1378
1379
  }
1379
1380