@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.
- package/README.md +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +82 -6
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +138 -130
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
775
|
-
|
|
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
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
847
|
-
|
|
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
|
-
|
|
872
|
-
ctx.ui
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
"workflows",
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
1265
|
+
const launchMode = resolveWorkflowLaunchMode(
|
|
1131
1266
|
{ wait: params.wait, background: params.background },
|
|
1132
1267
|
canDeliverLater,
|
|
1133
1268
|
);
|
|
1134
|
-
const background =
|
|
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
|
-
|
|
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
|
|
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: () =>
|
|
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:
|
|
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
|
|
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
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1865
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
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
|
|
2045
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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
|
-
|
|
2188
|
-
|
|
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 (
|
|
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:
|
|
2218
|
-
|
|
2440
|
+
ok: true,
|
|
2441
|
+
details: recoverStaleWorkflowDetails(details),
|
|
2219
2442
|
} as const;
|
|
2220
2443
|
}
|
|
2221
|
-
//
|
|
2222
|
-
//
|
|
2223
|
-
|
|
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:
|
|
2226
|
-
|
|
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: {
|
|
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
|
-
{
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
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
|
}
|