@tt-a1i/openpi 0.4.0 → 0.5.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 (109) hide show
  1. package/README.md +102 -40
  2. package/SETUP.md +22 -6
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/background-terminals/index.ts +30 -2
  6. package/extensions/background-terminals/src/domain.ts +2 -0
  7. package/extensions/background-terminals/src/manager.ts +486 -106
  8. package/extensions/background-terminals/src/output.ts +33 -0
  9. package/extensions/background-terminals/src/prompt.ts +13 -5
  10. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  11. package/extensions/clear-context/index.ts +83 -0
  12. package/extensions/context-pivot/index.ts +16 -6
  13. package/extensions/cron/schedule.ts +7 -1
  14. package/extensions/file-mutation-display/render.ts +17 -257
  15. package/extensions/file-search/src/binaries.ts +57 -41
  16. package/extensions/git-read/index.ts +1 -3
  17. package/extensions/model-info/index.ts +21 -33
  18. package/extensions/model-info/session-metrics.ts +96 -0
  19. package/extensions/plan-mode/bash-policy.ts +54 -9
  20. package/extensions/plan-mode/index.ts +7 -2
  21. package/extensions/post-edit/index.ts +16 -6
  22. package/extensions/sessions/git-stats.ts +258 -72
  23. package/extensions/sessions/index.ts +153 -86
  24. package/extensions/sessions/preview-cache.ts +104 -0
  25. package/extensions/sessions/preview-loader.ts +856 -0
  26. package/extensions/sessions/sessions.ts +43 -4
  27. package/extensions/setup/index.ts +123 -127
  28. package/extensions/shared/activity-status.ts +30 -0
  29. package/extensions/shared/agent-session-page.ts +319 -0
  30. package/extensions/shared/agent-tool-renderer.ts +218 -0
  31. package/extensions/shared/agent-transcript.ts +524 -0
  32. package/extensions/shared/capability-intent.ts +1 -1
  33. package/extensions/shared/child-session.ts +437 -21
  34. package/extensions/shared/result-delivery.ts +34 -0
  35. package/extensions/shared/setup-config.ts +73 -33
  36. package/extensions/shared/setup-episode-state.ts +1 -1
  37. package/extensions/shared/terminal-text.ts +110 -23
  38. package/extensions/shared/text-projection.ts +72 -15
  39. package/extensions/shared/tool-activity.ts +382 -0
  40. package/extensions/shared/tool-surface.ts +29 -2
  41. package/extensions/shared/transcript-viewport.ts +46 -0
  42. package/extensions/shared/web-observer-registry.ts +390 -0
  43. package/extensions/shared/worktree.ts +11 -0
  44. package/extensions/subagents/index.ts +270 -59
  45. package/extensions/subagents/navigation.ts +34 -5
  46. package/extensions/subagents/src/backend.ts +12 -1
  47. package/extensions/subagents/src/backends/pi.ts +375 -66
  48. package/extensions/subagents/src/domain.ts +5 -0
  49. package/extensions/subagents/src/manager.ts +34 -2
  50. package/extensions/subagents/src/prompt.ts +32 -4
  51. package/extensions/subagents/src/result-artifact.ts +4 -0
  52. package/extensions/subagents/src/result-delivery.ts +7 -1
  53. package/extensions/subagents/src/runtime.ts +15 -1
  54. package/extensions/subagents/src/ui/takeover.ts +73 -257
  55. package/extensions/subagents/src/ui/transcript.ts +38 -535
  56. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  57. package/extensions/suggestions/src/ui.ts +10 -4
  58. package/extensions/tasks/index.ts +0 -3
  59. package/extensions/ui-customization/footer.ts +0 -40
  60. package/extensions/ui-customization/index.ts +0 -4
  61. package/extensions/user-input-fold/index.ts +1 -1
  62. package/extensions/web/index.ts +234 -0
  63. package/extensions/workflows/artifacts.ts +137 -47
  64. package/extensions/workflows/completion-projection.ts +457 -0
  65. package/extensions/workflows/coordinator.ts +8 -10
  66. package/extensions/workflows/dashboard.ts +167 -228
  67. package/extensions/workflows/handoff.ts +70 -16
  68. package/extensions/workflows/index.ts +488 -198
  69. package/extensions/workflows/journal.ts +148 -13
  70. package/extensions/workflows/model.ts +74 -4
  71. package/extensions/workflows/navigation.ts +32 -8
  72. package/extensions/workflows/progress-projection.ts +306 -0
  73. package/extensions/workflows/prompt.ts +66 -6
  74. package/extensions/workflows/replay-safety.ts +42 -21
  75. package/extensions/workflows/result-delivery.ts +128 -64
  76. package/extensions/workflows/retention.ts +593 -0
  77. package/extensions/workflows/runner.ts +388 -279
  78. package/extensions/workflows/sandbox-child.cjs +25 -3
  79. package/extensions/workflows/sandbox.ts +62 -8
  80. package/extensions/workflows/serialization.ts +325 -17
  81. package/extensions/workflows/tool-renderer.ts +22 -0
  82. package/extensions/workflows/transcript.ts +149 -0
  83. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  84. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  85. package/package.json +28 -8
  86. package/skills/subagents/REFERENCE.md +189 -0
  87. package/skills/subagents/SKILL.md +1 -1
  88. package/skills/workflows/REFERENCE.md +4 -2
  89. package/web/adapter/pi-adapter.ts +661 -0
  90. package/web/host/browser-launcher.ts +20 -0
  91. package/web/host/static-assets.ts +4 -0
  92. package/web/host/terminal-status.ts +38 -0
  93. package/web/host/web-host.ts +789 -0
  94. package/web/http-dispatcher.ts +125 -0
  95. package/web/protocol/types.ts +462 -0
  96. package/web/runtime/pi-runtime.ts +991 -0
  97. package/web/runtime/types.ts +71 -0
  98. package/web/runtime/web-host-lease.ts +497 -0
  99. package/web/trace.ts +18 -0
  100. package/web/ui/app.js +1398 -0
  101. package/web/ui/index.html +139 -0
  102. package/web/ui/styles.css +598 -0
  103. package/web/vite.config.mjs +34 -0
  104. package/extensions/execution-convergence/active-evidence.ts +0 -129
  105. package/extensions/execution-convergence/index.ts +0 -442
  106. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  107. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  108. package/extensions/setup/intercom.ts +0 -603
  109. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -48,20 +48,29 @@ 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";
54
58
  import {
55
59
  registerEditorLayer,
56
60
  removeEditorLayer,
57
61
  } from "../shared/editor-layers.ts";
58
- import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
59
62
  import { loadSetupConfig } from "../shared/setup-config.ts";
60
63
  import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";
61
64
  import {
62
65
  OPENPI_TOOL_SURFACE,
63
66
  patchOwnedTools,
64
67
  } from "../shared/tool-surface.ts";
68
+ import {
69
+ notifyWebCapabilities,
70
+ projectWorkflowCapability,
71
+ registerWebCapability,
72
+ type WebCapabilityScope,
73
+ } from "../shared/web-observer-registry.ts";
65
74
  import {
66
75
  createWorktree,
67
76
  reclaimWorktree,
@@ -78,18 +87,27 @@ import {
78
87
  acceptanceInstruction,
79
88
  acceptanceSchema,
80
89
  applyAcceptance,
81
- evaluateAcceptance,
82
90
  parseAcceptanceContract,
83
91
  } from "./acceptance.ts";
84
92
  import {
85
93
  createWorkflowPersistence,
86
94
  loadJournal,
87
95
  persistWorkflowAgentResult,
96
+ persistWorkflowDeliveryState,
88
97
  persistWorkflowJson,
98
+ persistWorkflowTerminalState,
89
99
  } from "./artifacts.ts";
100
+ import {
101
+ buildExpandedWorkflowCompletion,
102
+ buildWorkflowCompletionDisplay,
103
+ isWorkflowCompletionDisplay,
104
+ workflowCompletionAlerts,
105
+ workflowCompletionResultPreview,
106
+ workflowCompletionSummary,
107
+ } from "./completion-projection.ts";
90
108
  import { RunController } from "./controller.ts";
91
109
  import {
92
- resolveWorkflowLaunchPolicy,
110
+ resolveWorkflowLaunchMode,
93
111
  waitForWorkflowCompletion,
94
112
  } from "./coordinator.ts";
95
113
  import {
@@ -108,8 +126,8 @@ import {
108
126
  } from "./invocation-ledger.ts";
109
127
  import {
110
128
  agentCallKey,
129
+ createJournalAccumulator,
111
130
  createReplayCache,
112
- type JournalEntry,
113
131
  type ReplayCache,
114
132
  } from "./journal.ts";
115
133
  import {
@@ -122,6 +140,7 @@ import {
122
140
  agentContext,
123
141
  aggregateUsage,
124
142
  appendLog,
143
+ compactWorkflowToolDetails,
125
144
  countStates,
126
145
  createUsageReader,
127
146
  emptyUsage,
@@ -146,15 +165,15 @@ import {
146
165
  type WorkflowStripEntry,
147
166
  WorkflowStripState,
148
167
  WorkflowStripWidget,
168
+ workflowStripEntryKey,
149
169
  } from "./navigation.ts";
150
170
  import {
151
171
  normalizeWorkflowOperatorKey,
152
172
  WorkflowOperatorRegistry,
153
173
  } from "./operator.ts";
154
174
  import {
155
- buildBackgroundWorkflowFollowUp,
156
175
  buildBackgroundWorkflowLaunchResult,
157
- buildProjectedWorkflowCompletionBatch,
176
+ buildProjectedWorkflowCompletionBatches,
158
177
  buildProjectedWorkflowResultMessage,
159
178
  buildWorkflowAgentPrompt,
160
179
  buildWorkflowResultMessage,
@@ -169,15 +188,20 @@ import {
169
188
  WORKFLOW_STOP_TOOL_DESCRIPTION,
170
189
  WORKFLOW_TOOL_DESCRIPTION,
171
190
  } from "./prompt.ts";
172
- import {
173
- createWorkflowResultDelivery,
174
- type WorkflowCompletionEnvelope,
175
- } from "./result-delivery.ts";
176
191
  import {
177
192
  beginProcessReplayWorkspaceLease,
178
193
  createReplayIdentity,
179
194
  isReplaySafeAgentCall,
180
195
  } from "./replay-safety.ts";
196
+ import {
197
+ createWorkflowResultDelivery,
198
+ type WorkflowCompletionEnvelope,
199
+ } from "./result-delivery.ts";
200
+ import {
201
+ createWorkflowSettledRunRetention,
202
+ projectWorkflowDetails,
203
+ type WorkflowSettledRunRetentionOptions,
204
+ } from "./retention.ts";
181
205
  import {
182
206
  createWorkflowResources,
183
207
  runAgent,
@@ -186,7 +210,7 @@ import {
186
210
  type WorkflowModel,
187
211
  } from "./runner.ts";
188
212
  import { runWorkflowSandbox } from "./sandbox.ts";
189
- import { safeStringify, writeFileAtomic } from "./serialization.ts";
213
+ import { writeFileAtomic } from "./serialization.ts";
190
214
  import {
191
215
  finalizeWorktreeHandoff,
192
216
  prepareWorktreeHandoff,
@@ -203,7 +227,7 @@ function runHeader(
203
227
  ) {
204
228
  const { done, failed, uncertain } = countStates(details);
205
229
  const settled = done + failed;
206
- const elapsed = formatElapsed(details.startedAt, details.finishedAt);
230
+ const elapsed = formatElapsed(details.startedAt, details.finishedAt, now);
207
231
  // A just-launched run has no agents and a 0s clock; the metrics join in
208
232
  // once there is something real to report.
209
233
  const counts =
@@ -264,7 +288,7 @@ function buildCollapsedRows(
264
288
  ? percent === undefined
265
289
  ? undefined
266
290
  : `${percent}%`
267
- : formatElapsed(agent.startedAt, agent.finishedAt);
291
+ : formatElapsed(agent.startedAt, agent.finishedAt, now);
268
292
  const left = ` ${stateGlyph(agent.state, theme, now)} ${theme.fg(
269
293
  "accent",
270
294
  sanitizeWorkflowDisplayLine(agent.label),
@@ -366,7 +390,7 @@ function buildExpandedWorkflow(
366
390
  sanitizeWorkflowDisplayLine(agent.label),
367
391
  )} ${theme.fg(
368
392
  "dim",
369
- [context, formatElapsed(agent.startedAt, agent.finishedAt)]
393
+ [context, formatElapsed(agent.startedAt, agent.finishedAt, now)]
370
394
  .filter(Boolean)
371
395
  .join(" · "),
372
396
  )}`;
@@ -532,31 +556,35 @@ interface AgentCallOptions {
532
556
  inputs?: unknown;
533
557
  }
534
558
 
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,
559
+ const WorkflowParams = Type.Object(
560
+ {
561
+ script: Type.String({
562
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.script,
547
563
  }),
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
- });
564
+ args: Type.Optional(
565
+ Type.String({
566
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.args,
567
+ }),
568
+ ),
569
+ background: Type.Optional(
570
+ Type.Boolean({
571
+ deprecated: true,
572
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
573
+ }),
574
+ ),
575
+ wait: Type.Optional(
576
+ Type.Boolean({
577
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.wait,
578
+ }),
579
+ ),
580
+ resume_from_run_id: Type.Optional(
581
+ Type.String({
582
+ description: WORKFLOW_PARAMETER_DESCRIPTIONS.resumeFromRunId,
583
+ }),
584
+ ),
585
+ },
586
+ { additionalProperties: false },
587
+ );
560
588
 
561
589
  type WorkflowInput = Static<typeof WorkflowParams>;
562
590
 
@@ -594,6 +622,25 @@ function errorText(error: unknown): string {
594
622
  );
595
623
  }
596
624
 
625
+ function isWorkflowRenderDetails(value: unknown): value is WorkflowDetails {
626
+ if (!value || typeof value !== "object") return false;
627
+ const details = value as Partial<WorkflowDetails>;
628
+ return (
629
+ typeof details.runId === "string" &&
630
+ details.runId.length > 0 &&
631
+ typeof details.background === "boolean" &&
632
+ (details.status === "running" ||
633
+ details.status === "completed" ||
634
+ details.status === "failed" ||
635
+ details.status === "aborted" ||
636
+ details.status === "uncertain") &&
637
+ typeof details.startedAt === "number" &&
638
+ Number.isFinite(details.startedAt) &&
639
+ Array.isArray(details.phases) &&
640
+ Array.isArray(details.agents)
641
+ );
642
+ }
643
+
597
644
  function summaryLine(details: WorkflowDetails): string {
598
645
  const { done, failed, uncertain } = countStates(details);
599
646
  const settled = done + failed;
@@ -613,20 +660,16 @@ function writeRunFile(runDir: string, name: string, content: string) {
613
660
  writeFileAtomic(path.join(runDir, name), content);
614
661
  }
615
662
 
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
- };
663
+ function appendArtifactPersistenceFailure(
664
+ details: WorkflowDetails,
665
+ error: unknown,
666
+ ) {
667
+ const persistenceFailure = `Artifact persistence failed: ${errorText(error)}`;
668
+ if (details.status !== "aborted") details.status = "failed";
669
+ details.error = details.error
670
+ ? `${details.error}; ${persistenceFailure}`
671
+ : persistenceFailure;
628
672
  }
629
-
630
673
  export interface ActiveWorkflowRunLifecycle {
631
674
  details: WorkflowDetails;
632
675
  controller: Pick<RunController, "abort" | "settle">;
@@ -634,6 +677,21 @@ export interface ActiveWorkflowRunLifecycle {
634
677
  forceSettle(error: string): void;
635
678
  }
636
679
 
680
+ interface WorkflowLifecycleTestHooks {
681
+ readonly persistWorkflow?: typeof persistWorkflowJson;
682
+ readonly reclaimWorktree?: typeof reclaimWorktree;
683
+ readonly onRunStarted?: (run: ActiveWorkflowRunLifecycle) => void;
684
+ }
685
+
686
+ let workflowLifecycleTestHooks: WorkflowLifecycleTestHooks | undefined;
687
+
688
+ /** Test-only control for deterministic lifecycle race coverage. */
689
+ export function __setWorkflowTestLifecycleHooks(
690
+ hooks: WorkflowLifecycleTestHooks | undefined,
691
+ ) {
692
+ workflowLifecycleTestHooks = hooks;
693
+ }
694
+
637
695
  /** Abort every live child and bound the whole session-shutdown barrier once. */
638
696
  export async function shutdownActiveWorkflowRuns(
639
697
  runs: readonly ActiveWorkflowRunLifecycle[],
@@ -738,19 +796,38 @@ function runDetailText(
738
796
  return `Run ${run.runId} — ${run.status}`;
739
797
  }
740
798
 
741
- export default function workflows(pi: ExtensionAPI) {
799
+ export interface WorkflowExtensionOptions {
800
+ /** Test/configuration seam for the settled session-memory projection. */
801
+ readonly settledRetention?: WorkflowSettledRunRetentionOptions;
802
+ }
803
+
804
+ const WORKFLOW_DELIVERY_DETAILS_MAX_BYTES = 128 * 1024;
805
+
806
+ export default function workflows(
807
+ pi: ExtensionAPI,
808
+ options: WorkflowExtensionOptions = {},
809
+ ) {
742
810
  /** Live background runs, for /workflows and shutdown cleanup. */
743
811
  const activeRuns = new Map<string, ActiveWorkflowRunLifecycle>();
812
+ let unregisterWebCapability: (() => void) | undefined;
813
+ let webCapabilityScope: WebCapabilityScope | undefined;
744
814
  const activeDetails = () =>
745
815
  new Map(
746
816
  [...activeRuns].map(([runId, run]) => [runId, run.details] as const),
747
817
  );
748
- const settledRuns = new Map<string, WorkflowDetails>();
818
+ const settledRuns = createWorkflowSettledRunRetention(
819
+ options.settledRetention,
820
+ );
821
+ /** Disk remains canonical; retained projections cover transient read failures. */
822
+ const dashboardDetails = () => activeDetails();
823
+ const dashboardRetainedDetails = () =>
824
+ new Map<string, WorkflowDetails>(settledRuns.entriesArray());
749
825
  const registerStableToolFamily = () =>
750
826
  patchOwnedTools(pi, "workflows", {
751
827
  enable: OPENPI_TOOL_SURFACE.workflows.entry,
752
828
  });
753
829
  const stripState = new WorkflowStripState();
830
+ const statusWriter = createStatusWriter("workflows");
754
831
  const widgetKey = "workflow-navigation";
755
832
 
756
833
  /**
@@ -763,41 +840,61 @@ export default function workflows(pi: ExtensionAPI) {
763
840
  ): WorkflowCompletionEnvelope => {
764
841
  const deliveryId = details.delivery?.id;
765
842
  if (!deliveryId) throw new Error("Workflow delivery identity is missing");
843
+ const projection = projectWorkflowDetails(
844
+ details,
845
+ WORKFLOW_DELIVERY_DETAILS_MAX_BYTES,
846
+ );
847
+ if (!projection) {
848
+ throw new Error(
849
+ `Workflow ${details.runId} cannot create a bounded completion projection`,
850
+ );
851
+ }
766
852
  return {
767
853
  deliveryId,
768
854
  runId: details.runId,
769
- details,
855
+ details: projection,
770
856
  };
771
857
  };
772
858
  const resultDelivery = createWorkflowResultDelivery({
773
859
  isIdle: () => lastContext?.isIdle() ?? false,
774
- persist: (details) =>
775
- persistWorkflowJson(
860
+ persist: (details) => {
861
+ if (!details.delivery)
862
+ throw new Error("Workflow delivery identity is missing");
863
+ persistWorkflowDeliveryState(
776
864
  path.join(getAgentDir(), "workflows", details.runId),
777
- details,
778
- ),
865
+ details.delivery,
866
+ );
867
+ },
779
868
  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
- })),
869
+ const hydrated = envelopes.map((envelope) => ({
870
+ ...envelope,
871
+ details:
872
+ readPersistedWorkflowDetails(envelope.runId, {
873
+ hydrateArtifacts: true,
874
+ }) ?? envelope.details,
875
+ }));
876
+ const sourceEntries = hydrated.map((envelope) => ({
877
+ deliveryId: envelope.deliveryId,
878
+ details: envelope.details,
879
+ runDir: path.join(getAgentDir(), "workflows", envelope.runId),
880
+ }));
881
+ const batches = buildProjectedWorkflowCompletionBatches(
882
+ sourceEntries,
786
883
  lastContext?.getContextUsage?.(),
787
884
  );
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
- );
885
+ for (const batch of batches) {
886
+ pi.sendMessage(
887
+ {
888
+ customType: "workflow-result",
889
+ content: batch.content,
890
+ display: true,
891
+ details: buildWorkflowCompletionDisplay(batch.entries),
892
+ },
893
+ wake
894
+ ? { deliverAs: "followUp", triggerTurn: true }
895
+ : { deliverAs: "nextTurn" },
896
+ );
897
+ }
801
898
  return envelopes.map((envelope) => ({
802
899
  deliveryId: envelope.deliveryId,
803
900
  delivered: true,
@@ -807,6 +904,7 @@ export default function workflows(pi: ExtensionAPI) {
807
904
  let completedRuns = 0;
808
905
  let failedRuns = 0;
809
906
  let widgetVisible = false;
907
+ let widgetEntryKey: string | undefined;
810
908
  let requestWidgetRender: (() => void) | undefined;
811
909
  let navigationLayerRegistered = false;
812
910
  let dashboardOpen = false;
@@ -837,16 +935,25 @@ export default function workflows(pi: ExtensionAPI) {
837
935
  const running = newestEntry(
838
936
  [...activeRuns].map(([runId, run]) => [runId, run.details] as const),
839
937
  );
840
- return running ?? newestEntry(settledRuns);
938
+ return running ?? newestEntry(settledRuns.entriesArray());
841
939
  };
842
940
 
843
941
  const updateWorkflowWidget = () => {
844
942
  const ctx = lastContext;
845
943
  if (!ctx || ctx.mode !== "tui") return;
846
- const visible = Boolean(stripEntry());
847
- if (visible === widgetVisible) return;
944
+ const entry = stripEntry();
945
+ const visible = Boolean(entry);
946
+ const entryKey = workflowStripEntryKey(entry);
947
+ if (visible === widgetVisible) {
948
+ if (visible && entryKey !== widgetEntryKey) {
949
+ widgetEntryKey = entryKey;
950
+ requestWidgetRender?.();
951
+ }
952
+ return;
953
+ }
848
954
  if (!visible) {
849
955
  stripState.focused = false;
956
+ widgetEntryKey = undefined;
850
957
  requestWidgetRender = undefined;
851
958
  ctx.ui.setWidget(widgetKey, undefined);
852
959
  widgetVisible = false;
@@ -861,25 +968,25 @@ export default function workflows(pi: ExtensionAPI) {
861
968
  { placement: "belowEditor" },
862
969
  );
863
970
  widgetVisible = true;
971
+ widgetEntryKey = entryKey;
864
972
  };
865
973
 
866
974
  const updateIndicator = () => {
975
+ if (webCapabilityScope) notifyWebCapabilities(webCapabilityScope);
867
976
  const ctx = lastContext;
868
977
  if (!ctx) return;
869
978
  try {
870
979
  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
- }
980
+ statusWriter.write(
981
+ ctx.ui,
982
+ running === 0 && completedRuns === 0 && failedRuns === 0
983
+ ? undefined
984
+ : formatActivityStatus(ctx.ui.theme, "workflows", {
985
+ running,
986
+ done: completedRuns,
987
+ failed: failedRuns,
988
+ }),
989
+ );
883
990
  updateWorkflowWidget();
884
991
  } catch {
885
992
  // UI may be unavailable.
@@ -893,7 +1000,7 @@ export default function workflows(pi: ExtensionAPI) {
893
1000
  };
894
1001
 
895
1002
  const recordSettledRun = (details: WorkflowDetails) => {
896
- settledRuns.set(details.runId, details);
1003
+ settledRuns.set(details);
897
1004
  if (details.status === "completed") completedRuns += 1;
898
1005
  else failedRuns += 1;
899
1006
  };
@@ -916,10 +1023,11 @@ export default function workflows(pi: ExtensionAPI) {
916
1023
  try {
917
1024
  await showWorkflowDashboard(
918
1025
  ctx,
919
- activeDetails,
1026
+ dashboardDetails,
920
1027
  initialRunId,
921
1028
  startedSince,
922
1029
  stopRun,
1030
+ dashboardRetainedDetails,
923
1031
  );
924
1032
  acknowledgeSettledRuns();
925
1033
  } finally {
@@ -953,6 +1061,20 @@ export default function workflows(pi: ExtensionAPI) {
953
1061
  };
954
1062
 
955
1063
  pi.on("session_start", (_event, ctx) => {
1064
+ unregisterWebCapability?.();
1065
+ const scope = ctx.sessionManager;
1066
+ webCapabilityScope = scope;
1067
+ unregisterWebCapability = registerWebCapability(scope, {
1068
+ kind: "workflows",
1069
+ snapshot: () =>
1070
+ projectWorkflowCapability([
1071
+ ...activeDetails().values(),
1072
+ ...settledRuns
1073
+ .entriesArray()
1074
+ .filter(([runId]) => !activeRuns.has(runId))
1075
+ .map(([, details]) => details),
1076
+ ]),
1077
+ });
956
1078
  registerStableToolFamily();
957
1079
  if (ctx.hasUI) lastContext = ctx;
958
1080
  agentTypes = loadAgentTypes({
@@ -963,7 +1085,7 @@ export default function workflows(pi: ExtensionAPI) {
963
1085
  turnStartedAt = 0;
964
1086
  completedRuns = 0;
965
1087
  failedRuns = 0;
966
- settledRuns.clear();
1088
+ settledRuns.resetSession();
967
1089
  installWorkflowNavigation(ctx);
968
1090
  updateIndicator();
969
1091
 
@@ -1010,17 +1132,25 @@ export default function workflows(pi: ExtensionAPI) {
1010
1132
  navigationLayerRegistered = false;
1011
1133
  }
1012
1134
  await shutdownActiveWorkflowRuns([...activeRuns.values()]);
1135
+ // Give deferred completions one final delivery attempt. Failed sends stay
1136
+ // durably pending; clearing first would discard an envelope whose initial
1137
+ // persistence may have failed.
1138
+ await resultDelivery.parentSettled();
1013
1139
  try {
1014
1140
  lastContext?.ui.setStatus("workflows", undefined);
1015
1141
  lastContext?.ui.setWidget(widgetKey, undefined);
1016
1142
  } catch {
1017
1143
  // UI may already be disposed.
1018
1144
  }
1145
+ statusWriter.reset();
1146
+ unregisterWebCapability?.();
1147
+ unregisterWebCapability = undefined;
1148
+ webCapabilityScope = undefined;
1019
1149
  lastContext = undefined;
1020
1150
  widgetVisible = false;
1151
+ widgetEntryKey = undefined;
1021
1152
  requestWidgetRender = undefined;
1022
1153
  stripState.focused = false;
1023
- resultDelivery.clear();
1024
1154
  });
1025
1155
 
1026
1156
  pi.registerCommand("workflows", {
@@ -1127,11 +1257,11 @@ export default function workflows(pi: ExtensionAPI) {
1127
1257
  const runId = `wf_${randomBytes(6).toString("hex")}`;
1128
1258
  const runDir = path.join(getAgentDir(), "workflows", runId);
1129
1259
  const canDeliverLater = ctx.hasUI && ctx.mode === "tui";
1130
- const launchPolicy = resolveWorkflowLaunchPolicy(
1260
+ const launchMode = resolveWorkflowLaunchMode(
1131
1261
  { wait: params.wait, background: params.background },
1132
1262
  canDeliverLater,
1133
1263
  );
1134
- const background = launchPolicy.detached;
1264
+ const background = launchMode === "detached";
1135
1265
  const now = Date.now();
1136
1266
 
1137
1267
  const details: WorkflowDetails = {
@@ -1146,7 +1276,7 @@ export default function workflows(pi: ExtensionAPI) {
1146
1276
  agents: [],
1147
1277
  delivery: {
1148
1278
  id: `workflow:${runId}:terminal`,
1149
- state: launchPolicy.wait ? "held-for-inline" : "none",
1279
+ state: launchMode === "inline" ? "held-for-inline" : "none",
1150
1280
  attempts: 0,
1151
1281
  updatedAt: now,
1152
1282
  },
@@ -1155,7 +1285,7 @@ export default function workflows(pi: ExtensionAPI) {
1155
1285
  // Resume: replay cached results for calls whose content is unchanged.
1156
1286
  // A missing or unreadable source degrades to a normal full run — resume
1157
1287
  // is an optimization and must not become a new way to fail.
1158
- const journalEntries: JournalEntry[] = [];
1288
+ const journal = createJournalAccumulator();
1159
1289
  let replay: ReplayCache | undefined;
1160
1290
  if (params.resume_from_run_id) {
1161
1291
  const source = resolveRunDir(params.resume_from_run_id);
@@ -1175,7 +1305,10 @@ export default function workflows(pi: ExtensionAPI) {
1175
1305
  writeRunFile(runDir, "args.json", params.args);
1176
1306
  persistWorkflowJson(runDir, details);
1177
1307
  const persistence = createWorkflowPersistence(runDir, details, {
1178
- journal: () => journalEntries,
1308
+ journal: () => journal,
1309
+ ...(workflowLifecycleTestHooks?.persistWorkflow
1310
+ ? { persist: workflowLifecycleTestHooks.persistWorkflow }
1311
+ : {}),
1179
1312
  });
1180
1313
 
1181
1314
  // A caller wait never owns the run. All runs survive an interrupted
@@ -1219,7 +1352,7 @@ export default function workflows(pi: ExtensionAPI) {
1219
1352
  if (background) return;
1220
1353
  onUpdate?.({
1221
1354
  content: [{ type: "text", text: summaryLine(details) }],
1222
- details: compactToolDetails(details),
1355
+ details: compactWorkflowToolDetails(details),
1223
1356
  });
1224
1357
  };
1225
1358
  const emit = (checkpoint = true) => {
@@ -1237,6 +1370,15 @@ export default function workflows(pi: ExtensionAPI) {
1237
1370
  flush(terminal);
1238
1371
  };
1239
1372
 
1373
+ const persistTerminalRecovery = () => {
1374
+ try {
1375
+ persistWorkflowTerminalState(runDir, details);
1376
+ } catch {
1377
+ // The original persistence error remains authoritative; restart
1378
+ // reconciliation handles the remaining uncertainty.
1379
+ }
1380
+ };
1381
+
1240
1382
  const terminalize = (
1241
1383
  status: WorkflowDetails["status"],
1242
1384
  error?: string,
@@ -1289,7 +1431,8 @@ export default function workflows(pi: ExtensionAPI) {
1289
1431
  try {
1290
1432
  persistence.flush();
1291
1433
  } catch (persistenceError) {
1292
- details.error = `${error}; artifact persistence failed: ${errorText(persistenceError)}`;
1434
+ appendArtifactPersistenceFailure(details, persistenceError);
1435
+ persistTerminalRecovery();
1293
1436
  }
1294
1437
  flushNow(true);
1295
1438
  };
@@ -1306,9 +1449,9 @@ export default function workflows(pi: ExtensionAPI) {
1306
1449
 
1307
1450
  // The script's narrator. Unlike phase(), this is append-only progress
1308
1451
  // text, so it never mutates the phase list a run is judged against.
1309
- const logFn = (text: string) => {
1452
+ const logFn = (text: string, kind?: "pipeline-drop") => {
1310
1453
  if (runSettled) return;
1311
- appendLog(details, text, Date.now());
1454
+ appendLog(details, text, Date.now(), kind);
1312
1455
  emit();
1313
1456
  };
1314
1457
 
@@ -1629,35 +1772,90 @@ export default function workflows(pi: ExtensionAPI) {
1629
1772
  });
1630
1773
  const callKey = replayIdentity ? replayKey(replayIdentity) : undefined;
1631
1774
  let replayBoundaryViolated = false;
1775
+ const persistAgentResult = (result: {
1776
+ output: string;
1777
+ structured?: unknown;
1778
+ }) => {
1779
+ try {
1780
+ return {
1781
+ ok: true as const,
1782
+ artifact: persistWorkflowAgentResult(runDir, index, result),
1783
+ };
1784
+ } catch (error) {
1785
+ return { ok: false as const, error: errorText(error) };
1786
+ }
1787
+ };
1632
1788
  // Checked before controller.schedule on purpose: schedule() charges the
1633
1789
  // run's agent-call budget on entry, and a replayed call runs no agent.
1634
1790
  const cached =
1635
1791
  callKey && replayLease.canReplay ? replay?.take(callKey) : undefined;
1636
1792
  if (cached) {
1637
1793
  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
1794
  record.finishedAt = finishedAt;
1645
1795
  record.preview = sanitizeWorkflowDisplayText(
1646
1796
  cached.output,
1647
1797
  PREVIEW_LENGTH,
1648
1798
  );
1649
- if (acceptanceContract) {
1650
- record.acceptance = evaluateAcceptance(
1651
- acceptanceContract,
1652
- cached.structured,
1799
+ const judged = applyAcceptance({
1800
+ contract: acceptanceContract,
1801
+ structured: cached.structured,
1802
+ agentOk: true,
1803
+ });
1804
+ if (judged.ledger) record.acceptance = judged.ledger;
1805
+ if (!judged.ok) {
1806
+ const error = sanitizeWorkflowDisplayLine(
1807
+ judged.error ?? "Agent failed",
1653
1808
  );
1809
+ record.invocation = transitionInvocation(record.invocation!, {
1810
+ status: "rejected",
1811
+ at: finishedAt,
1812
+ });
1813
+ record.state = "error";
1814
+ record.error = error;
1815
+ emit();
1816
+ replayLease.end();
1817
+ return {
1818
+ ok: false,
1819
+ output: cached.output,
1820
+ ...(cached.structured !== undefined
1821
+ ? { structured: cached.structured }
1822
+ : {}),
1823
+ ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1824
+ error,
1825
+ };
1654
1826
  }
1655
- record.resultArtifact = persistWorkflowAgentResult(runDir, index, {
1827
+ const persisted = persistAgentResult({
1656
1828
  output: cached.output,
1657
1829
  ...(cached.structured !== undefined
1658
1830
  ? { structured: cached.structured }
1659
1831
  : {}),
1660
1832
  });
1833
+ if (!persisted.ok) {
1834
+ record.invocation = transitionInvocation(record.invocation!, {
1835
+ status: "rejected",
1836
+ at: finishedAt,
1837
+ });
1838
+ record.state = "error";
1839
+ record.error = sanitizeWorkflowDisplayLine(persisted.error);
1840
+ emit();
1841
+ replayLease.end();
1842
+ return {
1843
+ ok: false,
1844
+ output: cached.output,
1845
+ ...(cached.structured !== undefined
1846
+ ? { structured: cached.structured }
1847
+ : {}),
1848
+ ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1849
+ error: persisted.error,
1850
+ };
1851
+ }
1852
+ record.invocation = transitionInvocation(record.invocation!, {
1853
+ status: "replayed",
1854
+ at: finishedAt,
1855
+ });
1856
+ record.state = "done";
1857
+ record.replayed = true;
1858
+ record.resultArtifact = persisted.artifact;
1661
1859
  const ref = handoffs.register({
1662
1860
  callId,
1663
1861
  settled: true,
@@ -1672,7 +1870,7 @@ export default function workflows(pi: ExtensionAPI) {
1672
1870
  emit();
1673
1871
  // Re-journal so a chain of resumes keeps working: run C resuming from
1674
1872
  // B still finds what B replayed from A.
1675
- journalEntries.push(cached);
1873
+ journal.append(cached);
1676
1874
  replayLease.end();
1677
1875
  return {
1678
1876
  ok: true,
@@ -1852,7 +2050,18 @@ export default function workflows(pi: ExtensionAPI) {
1852
2050
  });
1853
2051
  const acceptance = judged.ledger;
1854
2052
  if (acceptance) record.acceptance = acceptance;
1855
- const outcomeOk = judged.ok;
2053
+ let artifactError: string | undefined;
2054
+ if (judged.ok) {
2055
+ const persisted = persistAgentResult({
2056
+ output: outcome.output,
2057
+ ...(outcome.structured !== undefined
2058
+ ? { structured: outcome.structured }
2059
+ : {}),
2060
+ });
2061
+ if (persisted.ok) record.resultArtifact = persisted.artifact;
2062
+ else artifactError = persisted.error;
2063
+ }
2064
+ const outcomeOk = judged.ok && artifactError === undefined;
1856
2065
  record.invocation = transitionInvocation(record.invocation!, {
1857
2066
  status: "settled",
1858
2067
  outcome: outcomeOk ? "success" : "error",
@@ -1860,21 +2069,11 @@ export default function workflows(pi: ExtensionAPI) {
1860
2069
  });
1861
2070
  record.state = outcomeOk ? "done" : "error";
1862
2071
  if (outcomeOk) delete record.error;
1863
- else
1864
- record.error = judged.error
1865
- ? sanitizeWorkflowDisplayLine(judged.error)
2072
+ else {
2073
+ const failureError = artifactError ?? judged.error;
2074
+ record.error = failureError
2075
+ ? sanitizeWorkflowDisplayLine(failureError)
1866
2076
  : 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
2077
  }
1879
2078
  const ref = handoffs.register({
1880
2079
  callId,
@@ -1915,7 +2114,7 @@ export default function workflows(pi: ExtensionAPI) {
1915
2114
  !replayBoundaryViolated &&
1916
2115
  replayLease.canJournal()
1917
2116
  ) {
1918
- journalEntries.push({
2117
+ journal.append({
1919
2118
  key: completedKey,
1920
2119
  output: outcome.output,
1921
2120
  ...(outcome.structured !== undefined
@@ -1959,7 +2158,10 @@ export default function workflows(pi: ExtensionAPI) {
1959
2158
  detached: false,
1960
2159
  };
1961
2160
  } else {
1962
- cleanup = await reclaimWorktree(ctx.cwd, worktree).catch(
2161
+ const reclaimer =
2162
+ workflowLifecycleTestHooks?.reclaimWorktree ??
2163
+ reclaimWorktree;
2164
+ cleanup = await reclaimer(ctx.cwd, worktree).catch(
1963
2165
  (error): WorktreeCleanup => ({
1964
2166
  removed: false,
1965
2167
  branchDeleted: false,
@@ -1981,13 +2183,21 @@ export default function workflows(pi: ExtensionAPI) {
1981
2183
  };
1982
2184
  }
1983
2185
  }
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
- }
2186
+ record.worktreeCleanup = cleanup;
2187
+ if (cleanup.branchDeleted) delete record.worktreeBranch;
2188
+ else record.worktreeBranch = cleanup.branch;
2189
+ if (!cleanup.removed) record.worktreePath = worktree.path;
2190
+ // Forced settlement fixes the execution verdict, but cleanup
2191
+ // provenance discovered afterward still belongs in the run.
2192
+ // No later final flush remains, so failures must be observable.
2193
+ if (runSettled) {
2194
+ try {
2195
+ persistence.flush();
2196
+ } catch (error) {
2197
+ appendArtifactPersistenceFailure(details, error);
2198
+ persistTerminalRecovery();
2199
+ }
2200
+ } else emit();
1991
2201
  }
1992
2202
  }
1993
2203
  }, invocationSignal)
@@ -2041,8 +2251,8 @@ export default function workflows(pi: ExtensionAPI) {
2041
2251
  try {
2042
2252
  persistence.flush();
2043
2253
  } catch (error) {
2044
- details.status = "failed";
2045
- details.error = `Artifact persistence failed: ${errorText(error)}`;
2254
+ appendArtifactPersistenceFailure(details, error);
2255
+ persistTerminalRecovery();
2046
2256
  throw new Error(details.error);
2047
2257
  } finally {
2048
2258
  flushNow(true);
@@ -2059,6 +2269,7 @@ export default function workflows(pi: ExtensionAPI) {
2059
2269
  activeRuns.set(runId, activeRun);
2060
2270
  const completion = runScript();
2061
2271
  activeRun.completion = completion;
2272
+ workflowLifecycleTestHooks?.onRunStarted?.(activeRun);
2062
2273
  if (ctx.hasUI) lastContext = ctx;
2063
2274
  updateIndicator();
2064
2275
 
@@ -2072,8 +2283,8 @@ export default function workflows(pi: ExtensionAPI) {
2072
2283
  try {
2073
2284
  await completion;
2074
2285
  } catch (error) {
2075
- details.status = "failed";
2076
- details.finishedAt = Date.now();
2286
+ if (details.status === "running") details.status = "failed";
2287
+ details.finishedAt ??= Date.now();
2077
2288
  details.error = details.error ?? errorText(error);
2078
2289
  } finally {
2079
2290
  recordTerminalRun();
@@ -2096,7 +2307,7 @@ export default function workflows(pi: ExtensionAPI) {
2096
2307
  }),
2097
2308
  },
2098
2309
  ],
2099
- details: compactToolDetails(details),
2310
+ details: compactWorkflowToolDetails(details),
2100
2311
  };
2101
2312
  }
2102
2313
 
@@ -2125,7 +2336,7 @@ export default function workflows(pi: ExtensionAPI) {
2125
2336
  ),
2126
2337
  },
2127
2338
  ],
2128
- details: compactToolDetails(details),
2339
+ details: compactWorkflowToolDetails(details),
2129
2340
  };
2130
2341
  },
2131
2342
 
@@ -2137,7 +2348,11 @@ export default function workflows(pi: ExtensionAPI) {
2137
2348
  let text =
2138
2349
  theme.fg("toolTitle", theme.bold("workflow ")) +
2139
2350
  theme.fg("accent", (meta as WorkflowMeta).name ?? "(script)");
2140
- if (args.background) text += theme.fg("dim", " (background)");
2351
+ if (args.background !== undefined) {
2352
+ text += theme.fg("dim", ` (deprecated: use wait: ${!args.background})`);
2353
+ } else if (args.wait === true) {
2354
+ text += theme.fg("dim", " (wait)");
2355
+ }
2141
2356
  const description = (meta as WorkflowMeta).description;
2142
2357
  if (description) text += `\n ${theme.fg("dim", description)}`;
2143
2358
  for (const phase of meta.phases.slice(0, 8)) {
@@ -2149,8 +2364,8 @@ export default function workflows(pi: ExtensionAPI) {
2149
2364
  },
2150
2365
 
2151
2366
  renderResult(result, { expanded, isPartial }, theme, context) {
2152
- const details = result.details as WorkflowDetails | undefined;
2153
- if (!details) {
2367
+ const details = result.details;
2368
+ if (!isWorkflowRenderDetails(details)) {
2154
2369
  const first = result.content[0];
2155
2370
  return new Text(
2156
2371
  first?.type === "text" ? first.text : "(no output)",
@@ -2158,15 +2373,18 @@ export default function workflows(pi: ExtensionAPI) {
2158
2373
  0,
2159
2374
  );
2160
2375
  }
2376
+ // A settled Pi tool result is committed transcript history. Keep its
2377
+ // launch snapshot stable; live run state belongs to the strip/dashboard.
2378
+ const settledAt = Date.now();
2161
2379
  const currentDetails = () =>
2162
- activeRuns.get(details.runId)?.details ??
2163
- settledRuns.get(details.runId) ??
2164
- details;
2380
+ isPartial
2381
+ ? (activeRuns.get(details.runId)?.details ??
2382
+ settledRuns.get(details.runId) ??
2383
+ details)
2384
+ : details;
2165
2385
  syncWorkflowSpinner(
2166
2386
  context.state as WorkflowRenderState,
2167
- () =>
2168
- currentDetails().status === "running" &&
2169
- (isPartial || activeRuns.has(details.runId)),
2387
+ () => isPartial && currentDetails().status === "running",
2170
2388
  context.invalidate,
2171
2389
  );
2172
2390
 
@@ -2174,21 +2392,13 @@ export default function workflows(pi: ExtensionAPI) {
2174
2392
  render(width: number) {
2175
2393
  const current = currentDetails();
2176
2394
  const totals = formatUsage(aggregateUsage(current.agents));
2395
+ const now = isPartial ? Date.now() : settledAt;
2177
2396
  if (!expanded) {
2178
- return buildCollapsedRows(
2179
- current,
2180
- theme,
2181
- width,
2182
- Date.now(),
2183
- totals,
2184
- );
2397
+ return buildCollapsedRows(current, theme, width, now, totals);
2185
2398
  }
2186
- return buildExpandedWorkflow(
2187
- current,
2188
- theme,
2189
- Date.now(),
2190
- totals,
2191
- ).render(width);
2399
+ return buildExpandedWorkflow(current, theme, now, totals).render(
2400
+ width,
2401
+ );
2192
2402
  },
2193
2403
  invalidate() {},
2194
2404
  };
@@ -2206,24 +2416,25 @@ export default function workflows(pi: ExtensionAPI) {
2206
2416
 
2207
2417
  const active = activeRuns.get(resolution.runId);
2208
2418
  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
2419
  const details = readPersistedWorkflowDetails(resolution.runId, {
2213
2420
  hydrateArtifacts: true,
2214
2421
  });
2215
- if (!details) {
2422
+ if (details) {
2423
+ // A run absent from activeRuns cannot still be running this session; a
2424
+ // persisted "running" is a run that was hard-killed or missed the
2425
+ // shutdown settle deadline.
2216
2426
  return {
2217
- ok: false,
2218
- error: `Workflow run ${resolution.runId} could not be read.`,
2427
+ ok: true,
2428
+ details: recoverStaleWorkflowDetails(details),
2219
2429
  } as const;
2220
2430
  }
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.
2431
+ // Keep the bounded projection as a diagnostic fallback when an artifact is
2432
+ // temporarily unreadable. An explicit id still resolves to a known run.
2433
+ const settled = settledRuns.get(resolution.runId);
2434
+ if (settled) return { ok: true, details: settled } as const;
2224
2435
  return {
2225
- ok: true,
2226
- details: recoverStaleWorkflowDetails(details),
2436
+ ok: false,
2437
+ error: `Workflow run ${resolution.runId} could not be read.`,
2227
2438
  } as const;
2228
2439
  };
2229
2440
 
@@ -2296,32 +2507,57 @@ export default function workflows(pi: ExtensionAPI) {
2296
2507
  if (!resolution.ok) throw new Error(resolution.error);
2297
2508
  const details = resolution.details;
2298
2509
  const runDir = path.join(getAgentDir(), "workflows", details.runId);
2510
+ const retention = settledRuns.stats;
2299
2511
  return Promise.resolve({
2300
2512
  content: [
2301
2513
  { type: "text", text: buildWorkflowStatusSummary(details, runDir) },
2302
2514
  ],
2303
- details: { runs: [summarize(details)] },
2515
+ details: {
2516
+ runs: [summarize(details)],
2517
+ retention,
2518
+ settledRunsEvicted: retention.settledRunsEvicted,
2519
+ },
2304
2520
  });
2305
2521
  }
2306
2522
  const runs = [
2307
2523
  ...[...activeRuns.values()].map((run) => run.details),
2308
2524
  ...settledRuns.values(),
2309
2525
  ];
2526
+ const retention = settledRuns.stats;
2310
2527
  if (runs.length === 0) {
2311
2528
  return Promise.resolve({
2312
2529
  content: [
2313
- { type: "text", text: "No active or recently finished workflows." },
2530
+ {
2531
+ type: "text",
2532
+ text:
2533
+ retention.evictedRuns > 0
2534
+ ? `No active or retained workflows. ${retention.evictedRuns} settled run(s) omitted from memory in the current session; canonical artifacts remain available on disk.`
2535
+ : "No active or recently finished workflows.",
2536
+ },
2314
2537
  ],
2315
- details: { runs: [] },
2538
+ details: {
2539
+ runs: [],
2540
+ retention,
2541
+ settledRunsEvicted: retention.settledRunsEvicted,
2542
+ },
2316
2543
  });
2317
2544
  }
2318
2545
  const lines = runs.map((d) => {
2319
2546
  const { done, failed, uncertain } = countStates(d);
2320
2547
  return `${d.runId}${d.name ? ` "${d.name}"` : ""} — ${statusWord(d.status)} · ${done + failed}/${d.agents.length} agents${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""}`;
2321
2548
  });
2549
+ if (retention.evictedRuns > 0) {
2550
+ lines.push(
2551
+ `Retention (current session): ${retention.retainedRuns} settled projection(s) retained; ${retention.evictedRuns} evicted/omitted (${retention.evictedBytes} UTF-8 bytes). Canonical artifacts remain available on disk.`,
2552
+ );
2553
+ }
2322
2554
  return Promise.resolve({
2323
2555
  content: [{ type: "text", text: lines.join("\n") }],
2324
- details: { runs: runs.map(summarize) },
2556
+ details: {
2557
+ runs: runs.map(summarize),
2558
+ retention,
2559
+ settledRunsEvicted: retention.settledRunsEvicted,
2560
+ },
2325
2561
  });
2326
2562
  },
2327
2563
  });
@@ -2329,7 +2565,6 @@ export default function workflows(pi: ExtensionAPI) {
2329
2565
  pi.registerMessageRenderer(
2330
2566
  "workflow-result",
2331
2567
  (message, { expanded }, theme) => {
2332
- const details = message.details as WorkflowDetails | undefined;
2333
2568
  const body =
2334
2569
  typeof message.content === "string"
2335
2570
  ? message.content
@@ -2337,18 +2572,73 @@ export default function workflows(pi: ExtensionAPI) {
2337
2572
  ?.map((part) => (part.type === "text" ? part.text : ""))
2338
2573
  .join("") ?? "");
2339
2574
  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
- );
2575
+ const display = isWorkflowCompletionDisplay(message.details)
2576
+ ? message.details
2577
+ : undefined;
2578
+ const legacyDetails = isWorkflowRenderDetails(message.details)
2579
+ ? message.details
2580
+ : undefined;
2581
+ if (!display && !legacyDetails) {
2582
+ return new Text(safeBody, 0, 0);
2583
+ }
2584
+ if (legacyDetails) {
2585
+ const headerParts = runHeader(legacyDetails, theme, Date.now());
2586
+ const header = headerParts.right
2587
+ ? `${headerParts.left} ${headerParts.right}`
2588
+ : headerParts.left;
2589
+ if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
2590
+ const preview = safeBody.split("\n").slice(0, 8).join("\n");
2591
+ return new Text(
2592
+ `${header}\n${preview}\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`,
2593
+ 0,
2594
+ 0,
2595
+ );
2596
+ }
2597
+ if (!display) return new Text(safeBody, 0, 0);
2598
+ if (expanded) {
2599
+ return new Text(buildExpandedWorkflowCompletion(display), 0, 0);
2600
+ }
2601
+ return {
2602
+ render(width: number) {
2603
+ const rows: string[] = [];
2604
+ for (const entry of display.entries) {
2605
+ rows.push(
2606
+ truncateToWidth(
2607
+ `${statusGlyph(entry.status, theme, Date.now())} ${workflowCompletionSummary(entry)}`,
2608
+ width,
2609
+ "…",
2610
+ ),
2611
+ );
2612
+ for (const alert of workflowCompletionAlerts(entry)) {
2613
+ rows.push(
2614
+ truncateToWidth(` ${theme.fg("error", alert)}`, width, "…"),
2615
+ );
2616
+ }
2617
+ const result = workflowCompletionResultPreview(entry);
2618
+ if (result) {
2619
+ rows.push(
2620
+ truncateToWidth(
2621
+ ` ${theme.fg("accent", "Result:")} ${result}`,
2622
+ width,
2623
+ "…",
2624
+ ),
2625
+ );
2626
+ }
2627
+ }
2628
+ rows.push(
2629
+ truncateToWidth(
2630
+ theme.fg(
2631
+ "muted",
2632
+ `(${keyHint("app.tools.expand", "to expand")})`,
2633
+ ),
2634
+ width,
2635
+ "…",
2636
+ ),
2637
+ );
2638
+ return rows;
2639
+ },
2640
+ invalidate() {},
2641
+ };
2352
2642
  },
2353
2643
  );
2354
2644
  }