@tt-a1i/openpi 0.4.0 → 0.6.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 (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -48,20 +48,30 @@ import {
48
48
  truncateToWidth,
49
49
  } from "@earendil-works/pi-tui";
50
50
  import { type Static, Type } from "typebox";
51
- import { formatActivityStatus } from "../shared/activity-status.ts";
51
+ import {
52
+ createStatusWriter,
53
+ formatActivityStatus,
54
+ } from "../shared/activity-status.ts";
55
+ import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
52
56
  import { waitBounded } from "../shared/child-session.ts";
53
57
  import { contextPercent } from "../shared/context-utilization.ts";
58
+ import { completionOwnerFor } from "../shared/completion-inbox.ts";
54
59
  import {
55
60
  registerEditorLayer,
56
61
  removeEditorLayer,
57
62
  } from "../shared/editor-layers.ts";
58
- import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
59
63
  import { loadSetupConfig } from "../shared/setup-config.ts";
60
64
  import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";
61
65
  import {
62
66
  OPENPI_TOOL_SURFACE,
63
67
  patchOwnedTools,
64
68
  } from "../shared/tool-surface.ts";
69
+ import {
70
+ notifyWebCapabilities,
71
+ projectWorkflowCapability,
72
+ registerWebCapability,
73
+ type WebCapabilityScope,
74
+ } from "../shared/web-observer-registry.ts";
65
75
  import {
66
76
  createWorktree,
67
77
  reclaimWorktree,
@@ -78,18 +88,27 @@ import {
78
88
  acceptanceInstruction,
79
89
  acceptanceSchema,
80
90
  applyAcceptance,
81
- evaluateAcceptance,
82
91
  parseAcceptanceContract,
83
92
  } from "./acceptance.ts";
84
93
  import {
85
94
  createWorkflowPersistence,
86
95
  loadJournal,
87
96
  persistWorkflowAgentResult,
97
+ persistWorkflowDeliveryState,
88
98
  persistWorkflowJson,
99
+ persistWorkflowTerminalState,
89
100
  } from "./artifacts.ts";
101
+ import {
102
+ buildExpandedWorkflowCompletion,
103
+ buildWorkflowCompletionDisplay,
104
+ isWorkflowCompletionDisplay,
105
+ workflowCompletionAlerts,
106
+ workflowCompletionResultPreview,
107
+ workflowCompletionSummary,
108
+ } from "./completion-projection.ts";
90
109
  import { RunController } from "./controller.ts";
91
110
  import {
92
- resolveWorkflowLaunchPolicy,
111
+ resolveWorkflowLaunchMode,
93
112
  waitForWorkflowCompletion,
94
113
  } from "./coordinator.ts";
95
114
  import {
@@ -108,8 +127,8 @@ import {
108
127
  } from "./invocation-ledger.ts";
109
128
  import {
110
129
  agentCallKey,
130
+ createJournalAccumulator,
111
131
  createReplayCache,
112
- type JournalEntry,
113
132
  type ReplayCache,
114
133
  } from "./journal.ts";
115
134
  import {
@@ -122,6 +141,7 @@ import {
122
141
  agentContext,
123
142
  aggregateUsage,
124
143
  appendLog,
144
+ compactWorkflowToolDetails,
125
145
  countStates,
126
146
  createUsageReader,
127
147
  emptyUsage,
@@ -146,15 +166,15 @@ import {
146
166
  type WorkflowStripEntry,
147
167
  WorkflowStripState,
148
168
  WorkflowStripWidget,
169
+ workflowStripEntryKey,
149
170
  } from "./navigation.ts";
150
171
  import {
151
172
  normalizeWorkflowOperatorKey,
152
173
  WorkflowOperatorRegistry,
153
174
  } from "./operator.ts";
154
175
  import {
155
- buildBackgroundWorkflowFollowUp,
156
176
  buildBackgroundWorkflowLaunchResult,
157
- buildProjectedWorkflowCompletionBatch,
177
+ buildProjectedWorkflowCompletionBatches,
158
178
  buildProjectedWorkflowResultMessage,
159
179
  buildWorkflowAgentPrompt,
160
180
  buildWorkflowResultMessage,
@@ -169,15 +189,20 @@ import {
169
189
  WORKFLOW_STOP_TOOL_DESCRIPTION,
170
190
  WORKFLOW_TOOL_DESCRIPTION,
171
191
  } from "./prompt.ts";
172
- import {
173
- createWorkflowResultDelivery,
174
- type WorkflowCompletionEnvelope,
175
- } from "./result-delivery.ts";
176
192
  import {
177
193
  beginProcessReplayWorkspaceLease,
178
194
  createReplayIdentity,
179
195
  isReplaySafeAgentCall,
180
196
  } from "./replay-safety.ts";
197
+ import {
198
+ createWorkflowResultDelivery,
199
+ type WorkflowCompletionEnvelope,
200
+ } from "./result-delivery.ts";
201
+ import {
202
+ createWorkflowSettledRunRetention,
203
+ projectWorkflowDetails,
204
+ type WorkflowSettledRunRetentionOptions,
205
+ } from "./retention.ts";
181
206
  import {
182
207
  createWorkflowResources,
183
208
  runAgent,
@@ -186,7 +211,7 @@ import {
186
211
  type WorkflowModel,
187
212
  } from "./runner.ts";
188
213
  import { runWorkflowSandbox } from "./sandbox.ts";
189
- import { safeStringify, writeFileAtomic } from "./serialization.ts";
214
+ import { writeFileAtomic } from "./serialization.ts";
190
215
  import {
191
216
  finalizeWorktreeHandoff,
192
217
  prepareWorktreeHandoff,
@@ -203,7 +228,7 @@ function runHeader(
203
228
  ) {
204
229
  const { done, failed, uncertain } = countStates(details);
205
230
  const settled = done + failed;
206
- const elapsed = formatElapsed(details.startedAt, details.finishedAt);
231
+ const elapsed = formatElapsed(details.startedAt, details.finishedAt, now);
207
232
  // A just-launched run has no agents and a 0s clock; the metrics join in
208
233
  // once there is something real to report.
209
234
  const counts =
@@ -264,7 +289,7 @@ function buildCollapsedRows(
264
289
  ? percent === undefined
265
290
  ? undefined
266
291
  : `${percent}%`
267
- : formatElapsed(agent.startedAt, agent.finishedAt);
292
+ : formatElapsed(agent.startedAt, agent.finishedAt, now);
268
293
  const left = ` ${stateGlyph(agent.state, theme, now)} ${theme.fg(
269
294
  "accent",
270
295
  sanitizeWorkflowDisplayLine(agent.label),
@@ -366,7 +391,7 @@ function buildExpandedWorkflow(
366
391
  sanitizeWorkflowDisplayLine(agent.label),
367
392
  )} ${theme.fg(
368
393
  "dim",
369
- [context, formatElapsed(agent.startedAt, agent.finishedAt)]
394
+ [context, formatElapsed(agent.startedAt, agent.finishedAt, now)]
370
395
  .filter(Boolean)
371
396
  .join(" · "),
372
397
  )}`;
@@ -513,6 +538,8 @@ interface ScriptAgentResult {
513
538
  /** Opaque same-run handle for bounded downstream handoff. */
514
539
  ref?: string;
515
540
  acceptance?: AgentRecord["acceptance"];
541
+ /** Present only for the deprecated model self-attestation compatibility path. */
542
+ acceptanceWarning?: string;
516
543
  error?: string;
517
544
  }
518
545
 
@@ -532,31 +559,35 @@ interface AgentCallOptions {
532
559
  inputs?: unknown;
533
560
  }
534
561
 
535
- const WorkflowParams = Type.Object({
536
- script: Type.String({
537
- description: WORKFLOW_PARAMETER_DESCRIPTIONS.script,
538
- }),
539
- args: Type.Optional(
540
- Type.String({
541
- description: WORKFLOW_PARAMETER_DESCRIPTIONS.args,
542
- }),
543
- ),
544
- background: Type.Optional(
545
- Type.Boolean({
546
- description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
562
+ const WorkflowParams = Type.Object(
563
+ {
564
+ script: Type.String({
565
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.script,
547
566
  }),
548
- ),
549
- wait: Type.Optional(
550
- Type.Boolean({
551
- description: WORKFLOW_PARAMETER_DESCRIPTIONS.wait,
552
- }),
553
- ),
554
- resume_from_run_id: Type.Optional(
555
- Type.String({
556
- description: WORKFLOW_PARAMETER_DESCRIPTIONS.resumeFromRunId,
557
- }),
558
- ),
559
- });
567
+ args: Type.Optional(
568
+ Type.String({
569
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.args,
570
+ }),
571
+ ),
572
+ background: Type.Optional(
573
+ Type.Boolean({
574
+ deprecated: true,
575
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
576
+ }),
577
+ ),
578
+ wait: Type.Optional(
579
+ Type.Boolean({
580
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.wait,
581
+ }),
582
+ ),
583
+ resume_from_run_id: Type.Optional(
584
+ Type.String({
585
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.resumeFromRunId,
586
+ }),
587
+ ),
588
+ },
589
+ { additionalProperties: false },
590
+ );
560
591
 
561
592
  type WorkflowInput = Static<typeof WorkflowParams>;
562
593
 
@@ -594,6 +625,25 @@ function errorText(error: unknown): string {
594
625
  );
595
626
  }
596
627
 
628
+ function isWorkflowRenderDetails(value: unknown): value is WorkflowDetails {
629
+ if (!value || typeof value !== "object") return false;
630
+ const details = value as Partial<WorkflowDetails>;
631
+ return (
632
+ typeof details.runId === "string" &&
633
+ details.runId.length > 0 &&
634
+ typeof details.background === "boolean" &&
635
+ (details.status === "running" ||
636
+ details.status === "completed" ||
637
+ details.status === "failed" ||
638
+ details.status === "aborted" ||
639
+ details.status === "uncertain") &&
640
+ typeof details.startedAt === "number" &&
641
+ Number.isFinite(details.startedAt) &&
642
+ Array.isArray(details.phases) &&
643
+ Array.isArray(details.agents)
644
+ );
645
+ }
646
+
597
647
  function summaryLine(details: WorkflowDetails): string {
598
648
  const { done, failed, uncertain } = countStates(details);
599
649
  const settled = done + failed;
@@ -613,20 +663,16 @@ function writeRunFile(runDir: string, name: string, content: string) {
613
663
  writeFileAtomic(path.join(runDir, name), content);
614
664
  }
615
665
 
616
- function compactToolDetails(details: WorkflowDetails): WorkflowDetails {
617
- return {
618
- ...details,
619
- ...(details.result !== undefined
620
- ? {
621
- result: JSON.parse(
622
- safeStringify(details.result, { maxBytes: 64 * 1024 }),
623
- ),
624
- }
625
- : {}),
626
- agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
627
- };
666
+ function appendArtifactPersistenceFailure(
667
+ details: WorkflowDetails,
668
+ error: unknown,
669
+ ) {
670
+ const persistenceFailure = `Artifact persistence failed: ${errorText(error)}`;
671
+ if (details.status !== "aborted") details.status = "failed";
672
+ details.error = details.error
673
+ ? `${details.error}; ${persistenceFailure}`
674
+ : persistenceFailure;
628
675
  }
629
-
630
676
  export interface ActiveWorkflowRunLifecycle {
631
677
  details: WorkflowDetails;
632
678
  controller: Pick<RunController, "abort" | "settle">;
@@ -634,6 +680,21 @@ export interface ActiveWorkflowRunLifecycle {
634
680
  forceSettle(error: string): void;
635
681
  }
636
682
 
683
+ interface WorkflowLifecycleTestHooks {
684
+ readonly persistWorkflow?: typeof persistWorkflowJson;
685
+ readonly reclaimWorktree?: typeof reclaimWorktree;
686
+ readonly onRunStarted?: (run: ActiveWorkflowRunLifecycle) => void;
687
+ }
688
+
689
+ let workflowLifecycleTestHooks: WorkflowLifecycleTestHooks | undefined;
690
+
691
+ /** Test-only control for deterministic lifecycle race coverage. */
692
+ export function __setWorkflowTestLifecycleHooks(
693
+ hooks: WorkflowLifecycleTestHooks | undefined,
694
+ ) {
695
+ workflowLifecycleTestHooks = hooks;
696
+ }
697
+
637
698
  /** Abort every live child and bound the whole session-shutdown barrier once. */
638
699
  export async function shutdownActiveWorkflowRuns(
639
700
  runs: readonly ActiveWorkflowRunLifecycle[],
@@ -738,19 +799,38 @@ function runDetailText(
738
799
  return `Run ${run.runId} — ${run.status}`;
739
800
  }
740
801
 
741
- export default function workflows(pi: ExtensionAPI) {
802
+ export interface WorkflowExtensionOptions {
803
+ /** Test/configuration seam for the settled session-memory projection. */
804
+ readonly settledRetention?: WorkflowSettledRunRetentionOptions;
805
+ }
806
+
807
+ const WORKFLOW_DELIVERY_DETAILS_MAX_BYTES = 128 * 1024;
808
+
809
+ export default function workflows(
810
+ pi: ExtensionAPI,
811
+ options: WorkflowExtensionOptions = {},
812
+ ) {
742
813
  /** Live background runs, for /workflows and shutdown cleanup. */
743
814
  const activeRuns = new Map<string, ActiveWorkflowRunLifecycle>();
815
+ let unregisterWebCapability: (() => void) | undefined;
816
+ let webCapabilityScope: WebCapabilityScope | undefined;
744
817
  const activeDetails = () =>
745
818
  new Map(
746
819
  [...activeRuns].map(([runId, run]) => [runId, run.details] as const),
747
820
  );
748
- const settledRuns = new Map<string, WorkflowDetails>();
821
+ const settledRuns = createWorkflowSettledRunRetention(
822
+ options.settledRetention,
823
+ );
824
+ /** Disk remains canonical; retained projections cover transient read failures. */
825
+ const dashboardDetails = () => activeDetails();
826
+ const dashboardRetainedDetails = () =>
827
+ new Map<string, WorkflowDetails>(settledRuns.entriesArray());
749
828
  const registerStableToolFamily = () =>
750
829
  patchOwnedTools(pi, "workflows", {
751
830
  enable: OPENPI_TOOL_SURFACE.workflows.entry,
752
831
  });
753
832
  const stripState = new WorkflowStripState();
833
+ const statusWriter = createStatusWriter("workflows");
754
834
  const widgetKey = "workflow-navigation";
755
835
 
756
836
  /**
@@ -763,41 +843,63 @@ export default function workflows(pi: ExtensionAPI) {
763
843
  ): WorkflowCompletionEnvelope => {
764
844
  const deliveryId = details.delivery?.id;
765
845
  if (!deliveryId) throw new Error("Workflow delivery identity is missing");
846
+ const projection = projectWorkflowDetails(
847
+ details,
848
+ WORKFLOW_DELIVERY_DETAILS_MAX_BYTES,
849
+ );
850
+ if (!projection) {
851
+ throw new Error(
852
+ `Workflow ${details.runId} cannot create a bounded completion projection`,
853
+ );
854
+ }
766
855
  return {
767
856
  deliveryId,
768
857
  runId: details.runId,
769
- details,
858
+ details: projection,
770
859
  };
771
860
  };
772
861
  const resultDelivery = createWorkflowResultDelivery({
773
862
  isIdle: () => lastContext?.isIdle() ?? false,
774
- persist: (details) =>
775
- persistWorkflowJson(
863
+ owner: () =>
864
+ lastContext ? completionOwnerFor(lastContext.sessionManager) : undefined,
865
+ persist: (details) => {
866
+ if (!details.delivery)
867
+ throw new Error("Workflow delivery identity is missing");
868
+ persistWorkflowDeliveryState(
776
869
  path.join(getAgentDir(), "workflows", details.runId),
777
- details,
778
- ),
870
+ details.delivery,
871
+ );
872
+ },
779
873
  deliver: async (envelopes, wake) => {
780
- const content = buildProjectedWorkflowCompletionBatch(
781
- envelopes.map((envelope) => ({
782
- deliveryId: envelope.deliveryId,
783
- details: envelope.details,
784
- runDir: path.join(getAgentDir(), "workflows", envelope.runId),
785
- })),
874
+ const hydrated = envelopes.map((envelope) => ({
875
+ ...envelope,
876
+ details:
877
+ readPersistedWorkflowDetails(envelope.runId, {
878
+ hydrateArtifacts: true,
879
+ }) ?? envelope.details,
880
+ }));
881
+ const sourceEntries = hydrated.map((envelope) => ({
882
+ deliveryId: envelope.deliveryId,
883
+ details: envelope.details,
884
+ runDir: path.join(getAgentDir(), "workflows", envelope.runId),
885
+ }));
886
+ const batches = buildProjectedWorkflowCompletionBatches(
887
+ sourceEntries,
786
888
  lastContext?.getContextUsage?.(),
787
889
  );
788
- pi.sendMessage(
789
- {
790
- customType: "workflow-result",
791
- content,
792
- display: true,
793
- ...(envelopes.length === 1
794
- ? { details: compactToolDetails(envelopes[0]!.details) }
795
- : {}),
796
- },
797
- wake
798
- ? { deliverAs: "followUp", triggerTurn: true }
799
- : { deliverAs: "nextTurn" },
800
- );
890
+ for (const batch of batches) {
891
+ pi.sendMessage(
892
+ {
893
+ customType: "workflow-result",
894
+ content: batch.content,
895
+ display: true,
896
+ details: buildWorkflowCompletionDisplay(batch.entries),
897
+ },
898
+ wake
899
+ ? { deliverAs: "followUp", triggerTurn: true }
900
+ : { deliverAs: "nextTurn" },
901
+ );
902
+ }
801
903
  return envelopes.map((envelope) => ({
802
904
  deliveryId: envelope.deliveryId,
803
905
  delivered: true,
@@ -807,6 +909,7 @@ export default function workflows(pi: ExtensionAPI) {
807
909
  let completedRuns = 0;
808
910
  let failedRuns = 0;
809
911
  let widgetVisible = false;
912
+ let widgetEntryKey: string | undefined;
810
913
  let requestWidgetRender: (() => void) | undefined;
811
914
  let navigationLayerRegistered = false;
812
915
  let dashboardOpen = false;
@@ -837,16 +940,25 @@ export default function workflows(pi: ExtensionAPI) {
837
940
  const running = newestEntry(
838
941
  [...activeRuns].map(([runId, run]) => [runId, run.details] as const),
839
942
  );
840
- return running ?? newestEntry(settledRuns);
943
+ return running ?? newestEntry(settledRuns.entriesArray());
841
944
  };
842
945
 
843
946
  const updateWorkflowWidget = () => {
844
947
  const ctx = lastContext;
845
948
  if (!ctx || ctx.mode !== "tui") return;
846
- const visible = Boolean(stripEntry());
847
- if (visible === widgetVisible) return;
949
+ const entry = stripEntry();
950
+ const visible = Boolean(entry);
951
+ const entryKey = workflowStripEntryKey(entry);
952
+ if (visible === widgetVisible) {
953
+ if (visible && entryKey !== widgetEntryKey) {
954
+ widgetEntryKey = entryKey;
955
+ requestWidgetRender?.();
956
+ }
957
+ return;
958
+ }
848
959
  if (!visible) {
849
960
  stripState.focused = false;
961
+ widgetEntryKey = undefined;
850
962
  requestWidgetRender = undefined;
851
963
  ctx.ui.setWidget(widgetKey, undefined);
852
964
  widgetVisible = false;
@@ -861,25 +973,25 @@ export default function workflows(pi: ExtensionAPI) {
861
973
  { placement: "belowEditor" },
862
974
  );
863
975
  widgetVisible = true;
976
+ widgetEntryKey = entryKey;
864
977
  };
865
978
 
866
979
  const updateIndicator = () => {
980
+ if (webCapabilityScope) notifyWebCapabilities(webCapabilityScope);
867
981
  const ctx = lastContext;
868
982
  if (!ctx) return;
869
983
  try {
870
984
  const running = activeRuns.size;
871
- if (running === 0 && completedRuns === 0 && failedRuns === 0) {
872
- ctx.ui.setStatus("workflows", undefined);
873
- } else {
874
- ctx.ui.setStatus(
875
- "workflows",
876
- formatActivityStatus(ctx.ui.theme, "workflows", {
877
- running,
878
- done: completedRuns,
879
- failed: failedRuns,
880
- }),
881
- );
882
- }
985
+ statusWriter.write(
986
+ ctx.ui,
987
+ running === 0 && completedRuns === 0 && failedRuns === 0
988
+ ? undefined
989
+ : formatActivityStatus(ctx.ui.theme, "workflows", {
990
+ running,
991
+ done: completedRuns,
992
+ failed: failedRuns,
993
+ }),
994
+ );
883
995
  updateWorkflowWidget();
884
996
  } catch {
885
997
  // UI may be unavailable.
@@ -893,7 +1005,7 @@ export default function workflows(pi: ExtensionAPI) {
893
1005
  };
894
1006
 
895
1007
  const recordSettledRun = (details: WorkflowDetails) => {
896
- settledRuns.set(details.runId, details);
1008
+ settledRuns.set(details);
897
1009
  if (details.status === "completed") completedRuns += 1;
898
1010
  else failedRuns += 1;
899
1011
  };
@@ -916,10 +1028,11 @@ export default function workflows(pi: ExtensionAPI) {
916
1028
  try {
917
1029
  await showWorkflowDashboard(
918
1030
  ctx,
919
- activeDetails,
1031
+ dashboardDetails,
920
1032
  initialRunId,
921
1033
  startedSince,
922
1034
  stopRun,
1035
+ dashboardRetainedDetails,
923
1036
  );
924
1037
  acknowledgeSettledRuns();
925
1038
  } finally {
@@ -953,6 +1066,20 @@ export default function workflows(pi: ExtensionAPI) {
953
1066
  };
954
1067
 
955
1068
  pi.on("session_start", (_event, ctx) => {
1069
+ unregisterWebCapability?.();
1070
+ const scope = ctx.sessionManager;
1071
+ webCapabilityScope = scope;
1072
+ unregisterWebCapability = registerWebCapability(scope, {
1073
+ kind: "workflows",
1074
+ snapshot: () =>
1075
+ projectWorkflowCapability([
1076
+ ...activeDetails().values(),
1077
+ ...settledRuns
1078
+ .entriesArray()
1079
+ .filter(([runId]) => !activeRuns.has(runId))
1080
+ .map(([, details]) => details),
1081
+ ]),
1082
+ });
956
1083
  registerStableToolFamily();
957
1084
  if (ctx.hasUI) lastContext = ctx;
958
1085
  agentTypes = loadAgentTypes({
@@ -963,7 +1090,7 @@ export default function workflows(pi: ExtensionAPI) {
963
1090
  turnStartedAt = 0;
964
1091
  completedRuns = 0;
965
1092
  failedRuns = 0;
966
- settledRuns.clear();
1093
+ settledRuns.resetSession();
967
1094
  installWorkflowNavigation(ctx);
968
1095
  updateIndicator();
969
1096
 
@@ -1010,17 +1137,25 @@ export default function workflows(pi: ExtensionAPI) {
1010
1137
  navigationLayerRegistered = false;
1011
1138
  }
1012
1139
  await shutdownActiveWorkflowRuns([...activeRuns.values()]);
1140
+ // Give deferred completions one final delivery attempt. Failed sends stay
1141
+ // durably pending; clearing first would discard an envelope whose initial
1142
+ // persistence may have failed.
1143
+ await resultDelivery.parentSettled();
1013
1144
  try {
1014
1145
  lastContext?.ui.setStatus("workflows", undefined);
1015
1146
  lastContext?.ui.setWidget(widgetKey, undefined);
1016
1147
  } catch {
1017
1148
  // UI may already be disposed.
1018
1149
  }
1150
+ statusWriter.reset();
1151
+ unregisterWebCapability?.();
1152
+ unregisterWebCapability = undefined;
1153
+ webCapabilityScope = undefined;
1019
1154
  lastContext = undefined;
1020
1155
  widgetVisible = false;
1156
+ widgetEntryKey = undefined;
1021
1157
  requestWidgetRender = undefined;
1022
1158
  stripState.focused = false;
1023
- resultDelivery.clear();
1024
1159
  });
1025
1160
 
1026
1161
  pi.registerCommand("workflows", {
@@ -1127,11 +1262,11 @@ export default function workflows(pi: ExtensionAPI) {
1127
1262
  const runId = `wf_${randomBytes(6).toString("hex")}`;
1128
1263
  const runDir = path.join(getAgentDir(), "workflows", runId);
1129
1264
  const canDeliverLater = ctx.hasUI && ctx.mode === "tui";
1130
- const launchPolicy = resolveWorkflowLaunchPolicy(
1265
+ const launchMode = resolveWorkflowLaunchMode(
1131
1266
  { wait: params.wait, background: params.background },
1132
1267
  canDeliverLater,
1133
1268
  );
1134
- const background = launchPolicy.detached;
1269
+ const background = launchMode === "detached";
1135
1270
  const now = Date.now();
1136
1271
 
1137
1272
  const details: WorkflowDetails = {
@@ -1146,7 +1281,9 @@ export default function workflows(pi: ExtensionAPI) {
1146
1281
  agents: [],
1147
1282
  delivery: {
1148
1283
  id: `workflow:${runId}:terminal`,
1149
- state: launchPolicy.wait ? "held-for-inline" : "none",
1284
+ ownerSessionId: completionOwnerFor(ctx.sessionManager).sessionId,
1285
+ ownerEpoch: completionOwnerFor(ctx.sessionManager).epoch,
1286
+ state: launchMode === "inline" ? "held-for-inline" : "none",
1150
1287
  attempts: 0,
1151
1288
  updatedAt: now,
1152
1289
  },
@@ -1155,7 +1292,7 @@ export default function workflows(pi: ExtensionAPI) {
1155
1292
  // Resume: replay cached results for calls whose content is unchanged.
1156
1293
  // A missing or unreadable source degrades to a normal full run — resume
1157
1294
  // is an optimization and must not become a new way to fail.
1158
- const journalEntries: JournalEntry[] = [];
1295
+ const journal = createJournalAccumulator();
1159
1296
  let replay: ReplayCache | undefined;
1160
1297
  if (params.resume_from_run_id) {
1161
1298
  const source = resolveRunDir(params.resume_from_run_id);
@@ -1175,7 +1312,10 @@ export default function workflows(pi: ExtensionAPI) {
1175
1312
  writeRunFile(runDir, "args.json", params.args);
1176
1313
  persistWorkflowJson(runDir, details);
1177
1314
  const persistence = createWorkflowPersistence(runDir, details, {
1178
- journal: () => journalEntries,
1315
+ journal: () => journal,
1316
+ ...(workflowLifecycleTestHooks?.persistWorkflow
1317
+ ? { persist: workflowLifecycleTestHooks.persistWorkflow }
1318
+ : {}),
1179
1319
  });
1180
1320
 
1181
1321
  // A caller wait never owns the run. All runs survive an interrupted
@@ -1219,7 +1359,7 @@ export default function workflows(pi: ExtensionAPI) {
1219
1359
  if (background) return;
1220
1360
  onUpdate?.({
1221
1361
  content: [{ type: "text", text: summaryLine(details) }],
1222
- details: compactToolDetails(details),
1362
+ details: compactWorkflowToolDetails(details),
1223
1363
  });
1224
1364
  };
1225
1365
  const emit = (checkpoint = true) => {
@@ -1237,6 +1377,15 @@ export default function workflows(pi: ExtensionAPI) {
1237
1377
  flush(terminal);
1238
1378
  };
1239
1379
 
1380
+ const persistTerminalRecovery = () => {
1381
+ try {
1382
+ persistWorkflowTerminalState(runDir, details);
1383
+ } catch {
1384
+ // The original persistence error remains authoritative; restart
1385
+ // reconciliation handles the remaining uncertainty.
1386
+ }
1387
+ };
1388
+
1240
1389
  const terminalize = (
1241
1390
  status: WorkflowDetails["status"],
1242
1391
  error?: string,
@@ -1289,7 +1438,8 @@ export default function workflows(pi: ExtensionAPI) {
1289
1438
  try {
1290
1439
  persistence.flush();
1291
1440
  } catch (persistenceError) {
1292
- details.error = `${error}; artifact persistence failed: ${errorText(persistenceError)}`;
1441
+ appendArtifactPersistenceFailure(details, persistenceError);
1442
+ persistTerminalRecovery();
1293
1443
  }
1294
1444
  flushNow(true);
1295
1445
  };
@@ -1306,9 +1456,9 @@ export default function workflows(pi: ExtensionAPI) {
1306
1456
 
1307
1457
  // The script's narrator. Unlike phase(), this is append-only progress
1308
1458
  // text, so it never mutates the phase list a run is judged against.
1309
- const logFn = (text: string) => {
1459
+ const logFn = (text: string, kind?: "pipeline-drop") => {
1310
1460
  if (runSettled) return;
1311
- appendLog(details, text, Date.now());
1461
+ appendLog(details, text, Date.now(), kind);
1312
1462
  emit();
1313
1463
  };
1314
1464
 
@@ -1629,35 +1779,90 @@ export default function workflows(pi: ExtensionAPI) {
1629
1779
  });
1630
1780
  const callKey = replayIdentity ? replayKey(replayIdentity) : undefined;
1631
1781
  let replayBoundaryViolated = false;
1782
+ const persistAgentResult = (result: {
1783
+ output: string;
1784
+ structured?: unknown;
1785
+ }) => {
1786
+ try {
1787
+ return {
1788
+ ok: true as const,
1789
+ artifact: persistWorkflowAgentResult(runDir, index, result),
1790
+ };
1791
+ } catch (error) {
1792
+ return { ok: false as const, error: errorText(error) };
1793
+ }
1794
+ };
1632
1795
  // Checked before controller.schedule on purpose: schedule() charges the
1633
1796
  // run's agent-call budget on entry, and a replayed call runs no agent.
1634
1797
  const cached =
1635
1798
  callKey && replayLease.canReplay ? replay?.take(callKey) : undefined;
1636
1799
  if (cached) {
1637
1800
  const finishedAt = Date.now();
1638
- record.invocation = transitionInvocation(record.invocation!, {
1639
- status: "replayed",
1640
- at: finishedAt,
1641
- });
1642
- record.state = "done";
1643
- record.replayed = true;
1644
1801
  record.finishedAt = finishedAt;
1645
1802
  record.preview = sanitizeWorkflowDisplayText(
1646
1803
  cached.output,
1647
1804
  PREVIEW_LENGTH,
1648
1805
  );
1649
- if (acceptanceContract) {
1650
- record.acceptance = evaluateAcceptance(
1651
- acceptanceContract,
1652
- cached.structured,
1806
+ const judged = applyAcceptance({
1807
+ contract: acceptanceContract,
1808
+ structured: cached.structured,
1809
+ agentOk: true,
1810
+ });
1811
+ if (judged.ledger) record.acceptance = judged.ledger;
1812
+ if (!judged.ok) {
1813
+ const error = sanitizeWorkflowDisplayLine(
1814
+ judged.error ?? "Agent failed",
1653
1815
  );
1816
+ record.invocation = transitionInvocation(record.invocation!, {
1817
+ status: "rejected",
1818
+ at: finishedAt,
1819
+ });
1820
+ record.state = "error";
1821
+ record.error = error;
1822
+ emit();
1823
+ replayLease.end();
1824
+ return {
1825
+ ok: false,
1826
+ output: cached.output,
1827
+ ...(cached.structured !== undefined
1828
+ ? { structured: cached.structured }
1829
+ : {}),
1830
+ ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1831
+ error,
1832
+ };
1654
1833
  }
1655
- record.resultArtifact = persistWorkflowAgentResult(runDir, index, {
1834
+ const persisted = persistAgentResult({
1656
1835
  output: cached.output,
1657
1836
  ...(cached.structured !== undefined
1658
1837
  ? { structured: cached.structured }
1659
1838
  : {}),
1660
1839
  });
1840
+ if (!persisted.ok) {
1841
+ record.invocation = transitionInvocation(record.invocation!, {
1842
+ status: "rejected",
1843
+ at: finishedAt,
1844
+ });
1845
+ record.state = "error";
1846
+ record.error = sanitizeWorkflowDisplayLine(persisted.error);
1847
+ emit();
1848
+ replayLease.end();
1849
+ return {
1850
+ ok: false,
1851
+ output: cached.output,
1852
+ ...(cached.structured !== undefined
1853
+ ? { structured: cached.structured }
1854
+ : {}),
1855
+ ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1856
+ error: persisted.error,
1857
+ };
1858
+ }
1859
+ record.invocation = transitionInvocation(record.invocation!, {
1860
+ status: "replayed",
1861
+ at: finishedAt,
1862
+ });
1863
+ record.state = "done";
1864
+ record.replayed = true;
1865
+ record.resultArtifact = persisted.artifact;
1661
1866
  const ref = handoffs.register({
1662
1867
  callId,
1663
1868
  settled: true,
@@ -1672,7 +1877,7 @@ export default function workflows(pi: ExtensionAPI) {
1672
1877
  emit();
1673
1878
  // Re-journal so a chain of resumes keeps working: run C resuming from
1674
1879
  // B still finds what B replayed from A.
1675
- journalEntries.push(cached);
1880
+ journal.append(cached);
1676
1881
  replayLease.end();
1677
1882
  return {
1678
1883
  ok: true,
@@ -1682,6 +1887,9 @@ export default function workflows(pi: ExtensionAPI) {
1682
1887
  : {}),
1683
1888
  ...(ref ? { ref } : {}),
1684
1889
  ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1890
+ ...(judged.acceptanceWarning
1891
+ ? { acceptanceWarning: judged.acceptanceWarning }
1892
+ : {}),
1685
1893
  };
1686
1894
  }
1687
1895
 
@@ -1852,7 +2060,18 @@ export default function workflows(pi: ExtensionAPI) {
1852
2060
  });
1853
2061
  const acceptance = judged.ledger;
1854
2062
  if (acceptance) record.acceptance = acceptance;
1855
- const outcomeOk = judged.ok;
2063
+ let artifactError: string | undefined;
2064
+ if (judged.ok) {
2065
+ const persisted = persistAgentResult({
2066
+ output: outcome.output,
2067
+ ...(outcome.structured !== undefined
2068
+ ? { structured: outcome.structured }
2069
+ : {}),
2070
+ });
2071
+ if (persisted.ok) record.resultArtifact = persisted.artifact;
2072
+ else artifactError = persisted.error;
2073
+ }
2074
+ const outcomeOk = judged.ok && artifactError === undefined;
1856
2075
  record.invocation = transitionInvocation(record.invocation!, {
1857
2076
  status: "settled",
1858
2077
  outcome: outcomeOk ? "success" : "error",
@@ -1860,21 +2079,11 @@ export default function workflows(pi: ExtensionAPI) {
1860
2079
  });
1861
2080
  record.state = outcomeOk ? "done" : "error";
1862
2081
  if (outcomeOk) delete record.error;
1863
- else
1864
- record.error = judged.error
1865
- ? sanitizeWorkflowDisplayLine(judged.error)
2082
+ else {
2083
+ const failureError = artifactError ?? judged.error;
2084
+ record.error = failureError
2085
+ ? sanitizeWorkflowDisplayLine(failureError)
1866
2086
  : undefined;
1867
- if (outcomeOk) {
1868
- record.resultArtifact = persistWorkflowAgentResult(
1869
- runDir,
1870
- index,
1871
- {
1872
- output: outcome.output,
1873
- ...(outcome.structured !== undefined
1874
- ? { structured: outcome.structured }
1875
- : {}),
1876
- },
1877
- );
1878
2087
  }
1879
2088
  const ref = handoffs.register({
1880
2089
  callId,
@@ -1915,7 +2124,7 @@ export default function workflows(pi: ExtensionAPI) {
1915
2124
  !replayBoundaryViolated &&
1916
2125
  replayLease.canJournal()
1917
2126
  ) {
1918
- journalEntries.push({
2127
+ journal.append({
1919
2128
  key: completedKey,
1920
2129
  output: outcome.output,
1921
2130
  ...(outcome.structured !== undefined
@@ -1932,6 +2141,9 @@ export default function workflows(pi: ExtensionAPI) {
1932
2141
  : {}),
1933
2142
  ...(ref ? { ref } : {}),
1934
2143
  ...(acceptance ? { acceptance } : {}),
2144
+ ...(judged.acceptanceWarning
2145
+ ? { acceptanceWarning: judged.acceptanceWarning }
2146
+ : {}),
1935
2147
  ...(record.error !== undefined ? { error: record.error } : {}),
1936
2148
  };
1937
2149
  } finally {
@@ -1959,7 +2171,10 @@ export default function workflows(pi: ExtensionAPI) {
1959
2171
  detached: false,
1960
2172
  };
1961
2173
  } else {
1962
- cleanup = await reclaimWorktree(ctx.cwd, worktree).catch(
2174
+ const reclaimer =
2175
+ workflowLifecycleTestHooks?.reclaimWorktree ??
2176
+ reclaimWorktree;
2177
+ cleanup = await reclaimer(ctx.cwd, worktree).catch(
1963
2178
  (error): WorktreeCleanup => ({
1964
2179
  removed: false,
1965
2180
  branchDeleted: false,
@@ -1981,13 +2196,21 @@ export default function workflows(pi: ExtensionAPI) {
1981
2196
  };
1982
2197
  }
1983
2198
  }
1984
- if (!runSettled) {
1985
- record.worktreeCleanup = cleanup;
1986
- if (cleanup.branchDeleted) delete record.worktreeBranch;
1987
- else record.worktreeBranch = cleanup.branch;
1988
- if (!cleanup.removed) record.worktreePath = worktree.path;
1989
- emit();
1990
- }
2199
+ record.worktreeCleanup = cleanup;
2200
+ if (cleanup.branchDeleted) delete record.worktreeBranch;
2201
+ else record.worktreeBranch = cleanup.branch;
2202
+ if (!cleanup.removed) record.worktreePath = worktree.path;
2203
+ // Forced settlement fixes the execution verdict, but cleanup
2204
+ // provenance discovered afterward still belongs in the run.
2205
+ // No later final flush remains, so failures must be observable.
2206
+ if (runSettled) {
2207
+ try {
2208
+ persistence.flush();
2209
+ } catch (error) {
2210
+ appendArtifactPersistenceFailure(details, error);
2211
+ persistTerminalRecovery();
2212
+ }
2213
+ } else emit();
1991
2214
  }
1992
2215
  }
1993
2216
  }, invocationSignal)
@@ -2041,8 +2264,8 @@ export default function workflows(pi: ExtensionAPI) {
2041
2264
  try {
2042
2265
  persistence.flush();
2043
2266
  } catch (error) {
2044
- details.status = "failed";
2045
- details.error = `Artifact persistence failed: ${errorText(error)}`;
2267
+ appendArtifactPersistenceFailure(details, error);
2268
+ persistTerminalRecovery();
2046
2269
  throw new Error(details.error);
2047
2270
  } finally {
2048
2271
  flushNow(true);
@@ -2059,6 +2282,7 @@ export default function workflows(pi: ExtensionAPI) {
2059
2282
  activeRuns.set(runId, activeRun);
2060
2283
  const completion = runScript();
2061
2284
  activeRun.completion = completion;
2285
+ workflowLifecycleTestHooks?.onRunStarted?.(activeRun);
2062
2286
  if (ctx.hasUI) lastContext = ctx;
2063
2287
  updateIndicator();
2064
2288
 
@@ -2072,8 +2296,8 @@ export default function workflows(pi: ExtensionAPI) {
2072
2296
  try {
2073
2297
  await completion;
2074
2298
  } catch (error) {
2075
- details.status = "failed";
2076
- details.finishedAt = Date.now();
2299
+ if (details.status === "running") details.status = "failed";
2300
+ details.finishedAt ??= Date.now();
2077
2301
  details.error = details.error ?? errorText(error);
2078
2302
  } finally {
2079
2303
  recordTerminalRun();
@@ -2096,7 +2320,7 @@ export default function workflows(pi: ExtensionAPI) {
2096
2320
  }),
2097
2321
  },
2098
2322
  ],
2099
- details: compactToolDetails(details),
2323
+ details: compactWorkflowToolDetails(details),
2100
2324
  };
2101
2325
  }
2102
2326
 
@@ -2125,7 +2349,7 @@ export default function workflows(pi: ExtensionAPI) {
2125
2349
  ),
2126
2350
  },
2127
2351
  ],
2128
- details: compactToolDetails(details),
2352
+ details: compactWorkflowToolDetails(details),
2129
2353
  };
2130
2354
  },
2131
2355
 
@@ -2137,7 +2361,11 @@ export default function workflows(pi: ExtensionAPI) {
2137
2361
  let text =
2138
2362
  theme.fg("toolTitle", theme.bold("workflow ")) +
2139
2363
  theme.fg("accent", (meta as WorkflowMeta).name ?? "(script)");
2140
- if (args.background) text += theme.fg("dim", " (background)");
2364
+ if (args.background !== undefined) {
2365
+ text += theme.fg("dim", ` (deprecated: use wait: ${!args.background})`);
2366
+ } else if (args.wait === true) {
2367
+ text += theme.fg("dim", " (wait)");
2368
+ }
2141
2369
  const description = (meta as WorkflowMeta).description;
2142
2370
  if (description) text += `\n ${theme.fg("dim", description)}`;
2143
2371
  for (const phase of meta.phases.slice(0, 8)) {
@@ -2149,8 +2377,8 @@ export default function workflows(pi: ExtensionAPI) {
2149
2377
  },
2150
2378
 
2151
2379
  renderResult(result, { expanded, isPartial }, theme, context) {
2152
- const details = result.details as WorkflowDetails | undefined;
2153
- if (!details) {
2380
+ const details = result.details;
2381
+ if (!isWorkflowRenderDetails(details)) {
2154
2382
  const first = result.content[0];
2155
2383
  return new Text(
2156
2384
  first?.type === "text" ? first.text : "(no output)",
@@ -2158,15 +2386,18 @@ export default function workflows(pi: ExtensionAPI) {
2158
2386
  0,
2159
2387
  );
2160
2388
  }
2389
+ // A settled Pi tool result is committed transcript history. Keep its
2390
+ // launch snapshot stable; live run state belongs to the strip/dashboard.
2391
+ const settledAt = Date.now();
2161
2392
  const currentDetails = () =>
2162
- activeRuns.get(details.runId)?.details ??
2163
- settledRuns.get(details.runId) ??
2164
- details;
2393
+ isPartial
2394
+ ? (activeRuns.get(details.runId)?.details ??
2395
+ settledRuns.get(details.runId) ??
2396
+ details)
2397
+ : details;
2165
2398
  syncWorkflowSpinner(
2166
2399
  context.state as WorkflowRenderState,
2167
- () =>
2168
- currentDetails().status === "running" &&
2169
- (isPartial || activeRuns.has(details.runId)),
2400
+ () => isPartial && currentDetails().status === "running",
2170
2401
  context.invalidate,
2171
2402
  );
2172
2403
 
@@ -2174,21 +2405,13 @@ export default function workflows(pi: ExtensionAPI) {
2174
2405
  render(width: number) {
2175
2406
  const current = currentDetails();
2176
2407
  const totals = formatUsage(aggregateUsage(current.agents));
2408
+ const now = isPartial ? Date.now() : settledAt;
2177
2409
  if (!expanded) {
2178
- return buildCollapsedRows(
2179
- current,
2180
- theme,
2181
- width,
2182
- Date.now(),
2183
- totals,
2184
- );
2410
+ return buildCollapsedRows(current, theme, width, now, totals);
2185
2411
  }
2186
- return buildExpandedWorkflow(
2187
- current,
2188
- theme,
2189
- Date.now(),
2190
- totals,
2191
- ).render(width);
2412
+ return buildExpandedWorkflow(current, theme, now, totals).render(
2413
+ width,
2414
+ );
2192
2415
  },
2193
2416
  invalidate() {},
2194
2417
  };
@@ -2206,24 +2429,25 @@ export default function workflows(pi: ExtensionAPI) {
2206
2429
 
2207
2430
  const active = activeRuns.get(resolution.runId);
2208
2431
  if (active) return { ok: true, details: active.details } as const;
2209
- const settled = settledRuns.get(resolution.runId);
2210
- if (settled) return { ok: true, details: settled } as const;
2211
-
2212
2432
  const details = readPersistedWorkflowDetails(resolution.runId, {
2213
2433
  hydrateArtifacts: true,
2214
2434
  });
2215
- if (!details) {
2435
+ if (details) {
2436
+ // A run absent from activeRuns cannot still be running this session; a
2437
+ // persisted "running" is a run that was hard-killed or missed the
2438
+ // shutdown settle deadline.
2216
2439
  return {
2217
- ok: false,
2218
- error: `Workflow run ${resolution.runId} could not be read.`,
2440
+ ok: true,
2441
+ details: recoverStaleWorkflowDetails(details),
2219
2442
  } as const;
2220
2443
  }
2221
- // A run absent from activeRuns cannot still be running this session; a
2222
- // persisted "running" is a run that was hard-killed or missed the
2223
- // shutdown settle deadline.
2444
+ // Keep the bounded projection as a diagnostic fallback when an artifact is
2445
+ // temporarily unreadable. An explicit id still resolves to a known run.
2446
+ const settled = settledRuns.get(resolution.runId);
2447
+ if (settled) return { ok: true, details: settled } as const;
2224
2448
  return {
2225
- ok: true,
2226
- details: recoverStaleWorkflowDetails(details),
2449
+ ok: false,
2450
+ error: `Workflow run ${resolution.runId} could not be read.`,
2227
2451
  } as const;
2228
2452
  };
2229
2453
 
@@ -2296,32 +2520,57 @@ export default function workflows(pi: ExtensionAPI) {
2296
2520
  if (!resolution.ok) throw new Error(resolution.error);
2297
2521
  const details = resolution.details;
2298
2522
  const runDir = path.join(getAgentDir(), "workflows", details.runId);
2523
+ const retention = settledRuns.stats;
2299
2524
  return Promise.resolve({
2300
2525
  content: [
2301
2526
  { type: "text", text: buildWorkflowStatusSummary(details, runDir) },
2302
2527
  ],
2303
- details: { runs: [summarize(details)] },
2528
+ details: {
2529
+ runs: [summarize(details)],
2530
+ retention,
2531
+ settledRunsEvicted: retention.settledRunsEvicted,
2532
+ },
2304
2533
  });
2305
2534
  }
2306
2535
  const runs = [
2307
2536
  ...[...activeRuns.values()].map((run) => run.details),
2308
2537
  ...settledRuns.values(),
2309
2538
  ];
2539
+ const retention = settledRuns.stats;
2310
2540
  if (runs.length === 0) {
2311
2541
  return Promise.resolve({
2312
2542
  content: [
2313
- { type: "text", text: "No active or recently finished workflows." },
2543
+ {
2544
+ type: "text",
2545
+ text:
2546
+ retention.evictedRuns > 0
2547
+ ? `No active or retained workflows. ${retention.evictedRuns} settled run(s) omitted from memory in the current session; canonical artifacts remain available on disk.`
2548
+ : "No active or recently finished workflows.",
2549
+ },
2314
2550
  ],
2315
- details: { runs: [] },
2551
+ details: {
2552
+ runs: [],
2553
+ retention,
2554
+ settledRunsEvicted: retention.settledRunsEvicted,
2555
+ },
2316
2556
  });
2317
2557
  }
2318
2558
  const lines = runs.map((d) => {
2319
2559
  const { done, failed, uncertain } = countStates(d);
2320
2560
  return `${d.runId}${d.name ? ` "${d.name}"` : ""} — ${statusWord(d.status)} · ${done + failed}/${d.agents.length} agents${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""}`;
2321
2561
  });
2562
+ if (retention.evictedRuns > 0) {
2563
+ lines.push(
2564
+ `Retention (current session): ${retention.retainedRuns} settled projection(s) retained; ${retention.evictedRuns} evicted/omitted (${retention.evictedBytes} UTF-8 bytes). Canonical artifacts remain available on disk.`,
2565
+ );
2566
+ }
2322
2567
  return Promise.resolve({
2323
2568
  content: [{ type: "text", text: lines.join("\n") }],
2324
- details: { runs: runs.map(summarize) },
2569
+ details: {
2570
+ runs: runs.map(summarize),
2571
+ retention,
2572
+ settledRunsEvicted: retention.settledRunsEvicted,
2573
+ },
2325
2574
  });
2326
2575
  },
2327
2576
  });
@@ -2329,7 +2578,6 @@ export default function workflows(pi: ExtensionAPI) {
2329
2578
  pi.registerMessageRenderer(
2330
2579
  "workflow-result",
2331
2580
  (message, { expanded }, theme) => {
2332
- const details = message.details as WorkflowDetails | undefined;
2333
2581
  const body =
2334
2582
  typeof message.content === "string"
2335
2583
  ? message.content
@@ -2337,18 +2585,73 @@ export default function workflows(pi: ExtensionAPI) {
2337
2585
  ?.map((part) => (part.type === "text" ? part.text : ""))
2338
2586
  .join("") ?? "");
2339
2587
  const safeBody = sanitizeWorkflowDisplayText(body);
2340
- if (!details) return new Text(safeBody, 0, 0);
2341
- const headerParts = runHeader(details, theme, Date.now());
2342
- const header = headerParts.right
2343
- ? `${headerParts.left} ${headerParts.right}`
2344
- : headerParts.left;
2345
- if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
2346
- const preview = safeBody.split("\n").slice(0, 8).join("\n");
2347
- return new Text(
2348
- `${header}\n${preview}\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`,
2349
- 0,
2350
- 0,
2351
- );
2588
+ const display = isWorkflowCompletionDisplay(message.details)
2589
+ ? message.details
2590
+ : undefined;
2591
+ const legacyDetails = isWorkflowRenderDetails(message.details)
2592
+ ? message.details
2593
+ : undefined;
2594
+ if (!display && !legacyDetails) {
2595
+ return new Text(safeBody, 0, 0);
2596
+ }
2597
+ if (legacyDetails) {
2598
+ const headerParts = runHeader(legacyDetails, theme, Date.now());
2599
+ const header = headerParts.right
2600
+ ? `${headerParts.left} ${headerParts.right}`
2601
+ : headerParts.left;
2602
+ if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
2603
+ const preview = safeBody.split("\n").slice(0, 8).join("\n");
2604
+ return new Text(
2605
+ `${header}\n${preview}\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`,
2606
+ 0,
2607
+ 0,
2608
+ );
2609
+ }
2610
+ if (!display) return new Text(safeBody, 0, 0);
2611
+ if (expanded) {
2612
+ return new Text(buildExpandedWorkflowCompletion(display), 0, 0);
2613
+ }
2614
+ return {
2615
+ render(width: number) {
2616
+ const rows: string[] = [];
2617
+ for (const entry of display.entries) {
2618
+ rows.push(
2619
+ truncateToWidth(
2620
+ `${statusGlyph(entry.status, theme, Date.now())} ${workflowCompletionSummary(entry)}`,
2621
+ width,
2622
+ "…",
2623
+ ),
2624
+ );
2625
+ for (const alert of workflowCompletionAlerts(entry)) {
2626
+ rows.push(
2627
+ truncateToWidth(` ${theme.fg("error", alert)}`, width, "…"),
2628
+ );
2629
+ }
2630
+ const result = workflowCompletionResultPreview(entry);
2631
+ if (result) {
2632
+ rows.push(
2633
+ truncateToWidth(
2634
+ ` ${theme.fg("accent", "Result:")} ${result}`,
2635
+ width,
2636
+ "…",
2637
+ ),
2638
+ );
2639
+ }
2640
+ }
2641
+ rows.push(
2642
+ truncateToWidth(
2643
+ theme.fg(
2644
+ "muted",
2645
+ `(${keyHint("app.tools.expand", "to expand")})`,
2646
+ ),
2647
+ width,
2648
+ "…",
2649
+ ),
2650
+ );
2651
+ return rows;
2652
+ },
2653
+ invalidate() {},
2654
+ };
2352
2655
  },
2353
2656
  );
2354
2657
  }