@osolmaz/pi-workflows 0.13.4 → 0.15.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 +146 -160
- package/dist/builtins/autoimplement.workflow.js +3 -1
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/change-verification.workflow.js +7 -2
- package/dist/builtins/change-verification.workflow.js.map +1 -1
- package/dist/builtins/metadata.d.ts +22 -0
- package/dist/builtins/metadata.js +10 -0
- package/dist/builtins/metadata.js.map +1 -0
- package/dist/builtins/monitor.workflow.js +4 -1
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.js +4 -1
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.js +3 -1
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -2
- package/dist/controllers/index.js +0 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +85 -31
- package/dist/controllers/sqlite.js +541 -126
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +1 -0
- package/dist/extension/controller-command.d.ts +22 -0
- package/dist/extension/controller-command.js +32 -0
- package/dist/extension/controller-command.js.map +1 -0
- package/dist/extension/index.d.ts +2 -9
- package/dist/extension/index.js +876 -2928
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/message-card.d.ts +13 -0
- package/dist/extension/message-card.js +46 -0
- package/dist/extension/message-card.js.map +1 -0
- package/dist/extension/step-message.d.ts +5 -6
- package/dist/extension/step-message.js +41 -52
- package/dist/extension/step-message.js.map +1 -1
- package/dist/host/child-worker-supervisor.d.ts +50 -0
- package/dist/host/child-worker-supervisor.js +159 -0
- package/dist/host/child-worker-supervisor.js.map +1 -0
- package/dist/host/client.d.ts +48 -0
- package/dist/host/client.js +216 -0
- package/dist/host/client.js.map +1 -0
- package/dist/host/controller-worker-entry.d.ts +2 -0
- package/dist/host/controller-worker-entry.js +244 -0
- package/dist/host/controller-worker-entry.js.map +1 -0
- package/dist/host/controller-worker-protocol.d.ts +36 -0
- package/dist/host/controller-worker-protocol.js +49 -0
- package/dist/host/controller-worker-protocol.js.map +1 -0
- package/dist/host/controller-worker-supervisor.d.ts +21 -0
- package/dist/host/controller-worker-supervisor.js +54 -0
- package/dist/host/controller-worker-supervisor.js.map +1 -0
- package/dist/host/host-entry.d.ts +2 -0
- package/dist/host/host-entry.js +23 -0
- package/dist/host/host-entry.js.map +1 -0
- package/dist/host/processes.d.ts +17 -12
- package/dist/host/processes.js +154 -50
- package/dist/host/processes.js.map +1 -1
- package/dist/host/protocol.d.ts +38 -0
- package/dist/host/protocol.js +156 -0
- package/dist/host/protocol.js.map +1 -0
- package/dist/host/resolver-entry.d.ts +49 -0
- package/dist/host/resolver-entry.js +149 -0
- package/dist/host/resolver-entry.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +14 -3
- package/dist/host/rpc-executor.js +63 -30
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +95 -29
- package/dist/host/runner.js +2311 -348
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +174 -0
- package/dist/host/state.js +689 -0
- package/dist/host/state.js.map +1 -0
- package/dist/host/worker-entry.d.ts +10 -0
- package/dist/host/worker-entry.js +446 -0
- package/dist/host/worker-entry.js.map +1 -0
- package/dist/host/worker-protocol.d.ts +31 -0
- package/dist/host/worker-protocol.js +122 -0
- package/dist/host/worker-protocol.js.map +1 -0
- package/dist/host/worker-store.d.ts +80 -0
- package/dist/host/worker-store.js +91 -0
- package/dist/host/worker-store.js.map +1 -0
- package/dist/host/worker-supervisor.d.ts +22 -0
- package/dist/host/worker-supervisor.js +54 -0
- package/dist/host/worker-supervisor.js.map +1 -0
- package/dist/render/canvas.d.ts +2 -1
- package/dist/render/canvas.js +40 -15
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +7 -3
- package/dist/render/graph-render.js +125 -74
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.js +3 -1
- package/dist/state/database.js.map +1 -1
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.js +1 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/prune.js +36 -10
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +171 -3
- package/dist/state/schema.js.map +1 -1
- package/dist/state/viewer.d.ts +46 -0
- package/dist/state/viewer.js +249 -0
- package/dist/state/viewer.js.map +1 -0
- package/dist/viewer/cli.d.ts +1 -1
- package/dist/viewer/cli.js +47 -18
- package/dist/viewer/cli.js.map +1 -1
- package/dist/workflows/composition.js +25 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +3 -1
- package/dist/workflows/definition.js +25 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +1 -0
- package/dist/workflows/engine.js +157 -42
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -1
- package/dist/workflows/errors.js +4 -7
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -0
- package/dist/workflows/human-decision.js +31 -0
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -6
- package/dist/workflows/schema.js.map +1 -1
- package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
- package/dist/{viewer → workflows}/session-reducer.js +4 -0
- package/dist/workflows/session-reducer.js.map +1 -0
- package/dist/workflows/store.d.ts +87 -1
- package/dist/workflows/store.js +892 -93
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +0 -22
- package/dist/workflows/tool-input.js +1 -44
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +36 -4
- package/docs/2026-08-25-workflow-follow-ups.md +8 -6
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
- package/docs/CONTROLLERS.md +11 -11
- package/docs/DEFERRED_TURNS.md +61 -27
- package/docs/HUMAN_DECISIONS.md +12 -4
- package/docs/SQLITE_STATE.md +42 -8
- package/docs/WORKFLOW_HOST.md +452 -0
- package/docs/development.md +46 -30
- package/docs/live-replay-protocol.md +129 -100
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
- package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
- package/docs/plans/tui-viewer-implementation-plan.md +5 -0
- package/docs/tui-viewer.md +18 -3
- package/docs/workflows.md +164 -156
- package/examples/workflows/command-batch.workflow.ts +2 -0
- package/examples/workflows/shell.workflow.ts +2 -1
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/skills/autodoc/SKILL.md +1 -1
- package/skills/autoimplement/SKILL.md +1 -1
- package/skills/autoplan/SKILL.md +1 -1
- package/skills/pi-workflows/SKILL.md +2 -0
- package/src/builtins/autoimplement.workflow.ts +3 -0
- package/src/builtins/change-verification.workflow.ts +7 -2
- package/src/builtins/metadata.ts +9 -0
- package/src/builtins/monitor.workflow.ts +4 -0
- package/src/builtins/sanity-check.workflow.ts +4 -0
- package/src/builtins/workspace-preparation.workflow.ts +3 -1
- package/src/controllers/index.ts +3 -5
- package/src/controllers/sqlite.ts +929 -225
- package/src/controllers/store.ts +1 -0
- package/src/extension/controller-command.ts +45 -0
- package/src/extension/index.ts +948 -3506
- package/src/extension/message-card.ts +61 -0
- package/src/extension/step-message.ts +58 -63
- package/src/host/child-worker-supervisor.ts +183 -0
- package/src/host/client.ts +293 -0
- package/src/host/controller-worker-entry.ts +311 -0
- package/src/host/controller-worker-protocol.ts +104 -0
- package/src/host/controller-worker-supervisor.ts +79 -0
- package/src/host/host-entry.ts +23 -0
- package/src/host/processes.ts +171 -54
- package/src/host/protocol.ts +196 -0
- package/src/host/resolver-entry.ts +241 -0
- package/src/host/rpc-executor.ts +76 -34
- package/src/host/runner.ts +2813 -422
- package/src/host/state.ts +1160 -0
- package/src/host/worker-entry.ts +533 -0
- package/src/host/worker-protocol.ts +165 -0
- package/src/host/worker-store.ts +229 -0
- package/src/host/worker-supervisor.ts +74 -0
- package/src/render/canvas.ts +44 -10
- package/src/render/graph-render.ts +145 -90
- package/src/state/database.ts +2 -1
- package/src/state/index.ts +14 -0
- package/src/state/prune.ts +35 -9
- package/src/state/schema.ts +171 -3
- package/src/state/viewer.ts +356 -0
- package/src/viewer/cli.ts +49 -17
- package/src/workflows/composition.ts +36 -2
- package/src/workflows/definition.ts +32 -0
- package/src/workflows/engine.ts +157 -54
- package/src/workflows/errors.ts +11 -2
- package/src/workflows/human-decision.ts +49 -0
- package/src/workflows/index.ts +2 -0
- package/src/workflows/schema.ts +19 -6
- package/src/{viewer → workflows}/session-reducer.ts +11 -1
- package/src/workflows/store.ts +1316 -108
- package/src/workflows/tool-input.ts +2 -60
- package/src/workflows/types.ts +32 -4
- package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
- package/dist/controllers/workflow-engine-scheduler.js +0 -175
- package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
- package/dist/extension/controller-host.d.ts +0 -48
- package/dist/extension/controller-host.js +0 -110
- package/dist/extension/controller-host.js.map +0 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
- package/dist/extension/deferred-turn-coordinator.js +0 -143
- package/dist/extension/deferred-turn-coordinator.js.map +0 -1
- package/dist/extension/deferred-turn.d.ts +0 -44
- package/dist/extension/deferred-turn.js +0 -110
- package/dist/extension/deferred-turn.js.map +0 -1
- package/dist/extension/executor.d.ts +0 -86
- package/dist/extension/executor.js +0 -311
- package/dist/extension/executor.js.map +0 -1
- package/dist/extension/follow-up-coordinator.d.ts +0 -27
- package/dist/extension/follow-up-coordinator.js +0 -131
- package/dist/extension/follow-up-coordinator.js.map +0 -1
- package/dist/extension/recorder.d.ts +0 -84
- package/dist/extension/recorder.js +0 -528
- package/dist/extension/recorder.js.map +0 -1
- package/dist/extension/session-events.d.ts +0 -133
- package/dist/extension/session-events.js +0 -61
- package/dist/extension/session-events.js.map +0 -1
- package/dist/viewer/session-reducer.js.map +0 -1
- package/src/controllers/workflow-engine-scheduler.ts +0 -246
- package/src/extension/controller-host.ts +0 -167
- package/src/extension/deferred-turn-coordinator.ts +0 -171
- package/src/extension/deferred-turn.ts +0 -166
- package/src/extension/executor.ts +0 -411
- package/src/extension/follow-up-coordinator.ts +0 -151
- package/src/extension/recorder.ts +0 -655
- package/src/extension/session-events.ts +0 -121
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
1
2
|
import type {
|
|
2
3
|
WorkflowDefinitionSnapshot,
|
|
3
4
|
WorkflowRunState,
|
|
@@ -26,6 +27,9 @@ import { nodeTypeBadge } from "./node-type.js";
|
|
|
26
27
|
export type GraphView = {
|
|
27
28
|
state: WorkflowRunState;
|
|
28
29
|
snapshot: WorkflowDefinitionSnapshot | null;
|
|
30
|
+
graphScene?: GraphLayout;
|
|
31
|
+
graphSteps?: WorkflowStepRecord[];
|
|
32
|
+
takenTransitions?: string[];
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
type NodeStatus =
|
|
@@ -74,8 +78,12 @@ const STATUS_STYLES: Record<NodeStatus, CanvasStyle> = {
|
|
|
74
78
|
const CELL_GAP = 6;
|
|
75
79
|
const GUTTER_GAP = 2;
|
|
76
80
|
const GRAPH_SIDE_MARGIN = 2;
|
|
77
|
-
const CARD_MIN_CONTENT_WIDTH =
|
|
78
|
-
const
|
|
81
|
+
const CARD_MIN_CONTENT_WIDTH = 20;
|
|
82
|
+
const CARD_MAX_CONTENT_WIDTH = 28;
|
|
83
|
+
const CARD_CORE_HEIGHT = 7;
|
|
84
|
+
const CARD_MAX_BRANCH_ROWS = 3;
|
|
85
|
+
const CARD_WIDEST_STATUS_BADGE = "◆ replay focus";
|
|
86
|
+
const CARD_PAIRED_ROW_GAP = 1;
|
|
79
87
|
|
|
80
88
|
function nodeTypeStyle(nodeType: string): CanvasStyle {
|
|
81
89
|
switch (nodeType) {
|
|
@@ -92,14 +100,24 @@ function nodeTypeStyle(nodeType: string): CanvasStyle {
|
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
function fitText(text: string, width: number): string {
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
103
|
+
if (visibleWidth(text) <= width) return text;
|
|
104
|
+
if (width <= 0) return "";
|
|
105
|
+
const ellipsis = "…";
|
|
106
|
+
const available = Math.max(0, width - visibleWidth(ellipsis));
|
|
107
|
+
let fitted = "";
|
|
108
|
+
let used = 0;
|
|
109
|
+
for (const char of text) {
|
|
110
|
+
const charWidth = visibleWidth(char);
|
|
111
|
+
if (used + charWidth > available) break;
|
|
112
|
+
fitted += char;
|
|
113
|
+
used += charWidth;
|
|
114
|
+
}
|
|
115
|
+
return `${fitted}${ellipsis}`;
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
function centeredText(text: string, width: number): string {
|
|
101
119
|
const fitted = fitText(text, width);
|
|
102
|
-
const left = Math.max(0, Math.floor((width -
|
|
120
|
+
const left = Math.max(0, Math.floor((width - visibleWidth(fitted)) / 2));
|
|
103
121
|
return `${" ".repeat(left)}${fitted}`;
|
|
104
122
|
}
|
|
105
123
|
|
|
@@ -110,15 +128,13 @@ export type GraphRenderOptions = {
|
|
|
110
128
|
nodeStyle?: GraphNodeStyle;
|
|
111
129
|
};
|
|
112
130
|
|
|
113
|
-
type
|
|
131
|
+
type CardShape = {
|
|
114
132
|
width: number;
|
|
115
133
|
height: number;
|
|
116
|
-
contentWidth: number;
|
|
117
|
-
branchRows: number;
|
|
118
134
|
};
|
|
119
135
|
|
|
120
|
-
function cellHeight(nodeStyle: GraphNodeStyle,
|
|
121
|
-
return nodeStyle === "box" ?
|
|
136
|
+
function cellHeight(nodeStyle: GraphNodeStyle, cell: RenderedCell): number {
|
|
137
|
+
return nodeStyle === "box" ? cell.height : 1;
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
function paint(text: string, style: CanvasStyle): string {
|
|
@@ -201,39 +217,66 @@ function nodeBranchLabels(view: GraphView, nodeId: string): string[] {
|
|
|
201
217
|
).map(sanitizeText);
|
|
202
218
|
}
|
|
203
219
|
|
|
204
|
-
function
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
function cardShape(view: GraphView, nodeId: string): CardShape {
|
|
221
|
+
const node = view.snapshot?.nodes[nodeId];
|
|
222
|
+
const labels = nodeBranchLabels(view, nodeId);
|
|
223
|
+
const branchRows = Math.min(labels.length, CARD_MAX_BRANCH_ROWS);
|
|
224
|
+
const actionExecution = "actionExecution" in (node ?? {}) ? node?.actionExecution : undefined;
|
|
225
|
+
const humanAudience = node && "humanDecision" in node ? node.humanDecision.audience : undefined;
|
|
226
|
+
const configuredDetail = node?.statusDetail ?? node?.summary;
|
|
227
|
+
const assistantDetail =
|
|
228
|
+
node?.nodeType === "agent" &&
|
|
229
|
+
typeof node.expectedOutput === "object" &&
|
|
230
|
+
node.expectedOutput.kind === "assistant-message"
|
|
231
|
+
? "assistant response"
|
|
232
|
+
: undefined;
|
|
233
|
+
const cardDetail = [assistantDetail, configuredDetail].filter(Boolean).join(" · ");
|
|
234
|
+
const typeBadge = node ? nodeTypeBadge(node.nodeType, actionExecution) : "? unknown";
|
|
235
|
+
const candidates = [
|
|
236
|
+
hierarchicalNodeLabel(nodeId, node),
|
|
237
|
+
typeBadge,
|
|
238
|
+
`${typeBadge} ${CARD_WIDEST_STATUS_BADGE}`,
|
|
239
|
+
...(humanAudience === undefined ? [] : [`… human decision · ${humanAudience}`]),
|
|
240
|
+
...(cardDetail === "" ? [] : [`… ${cardDetail}`]),
|
|
241
|
+
...boundedBranchLines(labels),
|
|
242
|
+
];
|
|
243
|
+
const desiredWidth = Math.max(...candidates.map(visibleWidth));
|
|
244
|
+
const contentWidth = Math.max(
|
|
245
|
+
CARD_MIN_CONTENT_WIDTH,
|
|
246
|
+
Math.min(CARD_MAX_CONTENT_WIDTH, desiredWidth),
|
|
247
|
+
);
|
|
225
248
|
return {
|
|
226
249
|
width: contentWidth + 4,
|
|
227
|
-
height:
|
|
228
|
-
contentWidth,
|
|
229
|
-
branchRows,
|
|
250
|
+
height: CARD_CORE_HEIGHT + branchRows,
|
|
230
251
|
};
|
|
231
252
|
}
|
|
232
253
|
|
|
233
|
-
/** Canonical outer dimensions
|
|
234
|
-
export function graphCardSize(view: GraphView): { width: number; height: number } {
|
|
235
|
-
|
|
236
|
-
|
|
254
|
+
/** Canonical bounded outer dimensions for one boxed node card. */
|
|
255
|
+
export function graphCardSize(view: GraphView, nodeId: string): { width: number; height: number } {
|
|
256
|
+
return cardShape(view, nodeId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function boundedNodeLabel(
|
|
260
|
+
nodeId: string,
|
|
261
|
+
node: WorkflowDefinitionSnapshot["nodes"][string] | undefined,
|
|
262
|
+
contentWidth: number,
|
|
263
|
+
): string {
|
|
264
|
+
const full = hierarchicalNodeLabel(nodeId, node);
|
|
265
|
+
if (visibleWidth(full) <= contentWidth) return full;
|
|
266
|
+
const local = sanitizeText(node?.localNodeId ?? nodeId);
|
|
267
|
+
const suffix =
|
|
268
|
+
node?.includeTransition === "entry"
|
|
269
|
+
? "enter"
|
|
270
|
+
: node?.includeTransition === "exit"
|
|
271
|
+
? `${local} exit`
|
|
272
|
+
: local;
|
|
273
|
+
const candidate = `… › ${suffix}`;
|
|
274
|
+
return visibleWidth(candidate) <= contentWidth ? candidate : fitText(suffix, contentWidth);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function boundedBranchLines(labels: string[]): string[] {
|
|
278
|
+
if (labels.length <= CARD_MAX_BRANCH_ROWS) return labels.map((label) => `◇ ${label}`);
|
|
279
|
+
return [`◇ ${labels[0]}`, `◇ ${labels[1]}`, `+${labels.length - 2} branches`];
|
|
237
280
|
}
|
|
238
281
|
|
|
239
282
|
type RenderedCell = {
|
|
@@ -250,6 +293,7 @@ type RenderedCell = {
|
|
|
250
293
|
isStart: boolean;
|
|
251
294
|
isEnd: boolean;
|
|
252
295
|
width: number;
|
|
296
|
+
height: number;
|
|
253
297
|
};
|
|
254
298
|
|
|
255
299
|
function renderCellText(
|
|
@@ -259,7 +303,7 @@ function renderCellText(
|
|
|
259
303
|
atLatestStep: boolean,
|
|
260
304
|
now: Date,
|
|
261
305
|
nodeStyle: GraphNodeStyle,
|
|
262
|
-
|
|
306
|
+
shape: CardShape,
|
|
263
307
|
): RenderedCell {
|
|
264
308
|
if (cell.kind === "virtual") {
|
|
265
309
|
return {
|
|
@@ -276,6 +320,7 @@ function renderCellText(
|
|
|
276
320
|
isStart: false,
|
|
277
321
|
isEnd: false,
|
|
278
322
|
width: 1,
|
|
323
|
+
height: 1,
|
|
279
324
|
};
|
|
280
325
|
}
|
|
281
326
|
const state = view.state;
|
|
@@ -330,13 +375,7 @@ function renderCellText(
|
|
|
330
375
|
human === undefined
|
|
331
376
|
? undefined
|
|
332
377
|
: state.waitingOn === nodeId
|
|
333
|
-
? `human decision · ${sanitizeText(typeof waitingRequest?.audience === "string" ? waitingRequest.audience : human.audience)}
|
|
334
|
-
human.choices,
|
|
335
|
-
)
|
|
336
|
-
.map((choice) => sanitizeText(choice.label))
|
|
337
|
-
.join(
|
|
338
|
-
" / ",
|
|
339
|
-
)}${typeof waitingRequest?.presentationDigest === "string" ? ` · ${waitingRequest.presentationDigest.slice(7, 19)}` : ""}`
|
|
378
|
+
? `human decision · ${sanitizeText(typeof waitingRequest?.audience === "string" ? waitingRequest.audience : human.audience)}`
|
|
340
379
|
: selected !== undefined
|
|
341
380
|
? `human: ${sanitizeText(selected.label)}`
|
|
342
381
|
: undefined;
|
|
@@ -356,13 +395,11 @@ function renderCellText(
|
|
|
356
395
|
: "";
|
|
357
396
|
const nodeDetail = [assistantDetail, configuredDetail].filter(Boolean).join(" · ");
|
|
358
397
|
const detail = humanDetail ?? nodeDetail;
|
|
359
|
-
const branchLines =
|
|
360
|
-
labels[index] ? `◇ ${labels[index]}` : "",
|
|
361
|
-
);
|
|
398
|
+
const branchLines = boundedBranchLines(labels);
|
|
362
399
|
const count = atLatestStep && state.currentNode === nodeId ? Math.max(attempts, 1) : attempts;
|
|
363
400
|
const timing =
|
|
364
401
|
attempt || count > 0 ? `${count} attempt${count === 1 ? "" : "s"} · ${elapsed}` : "not visited";
|
|
365
|
-
const displayNodeId =
|
|
402
|
+
const displayNodeId = boundedNodeLabel(nodeId, node, shape.width - 4);
|
|
366
403
|
const text = `${displayNodeId} [${nodeType}] ${timing}`;
|
|
367
404
|
return {
|
|
368
405
|
cell,
|
|
@@ -377,7 +414,8 @@ function renderCellText(
|
|
|
377
414
|
branchLines,
|
|
378
415
|
isStart,
|
|
379
416
|
isEnd,
|
|
380
|
-
width: nodeStyle === "box" ?
|
|
417
|
+
width: nodeStyle === "box" ? shape.width : visibleWidth(text) + 2,
|
|
418
|
+
height: nodeStyle === "box" ? shape.height : 1,
|
|
381
419
|
};
|
|
382
420
|
}
|
|
383
421
|
|
|
@@ -397,12 +435,13 @@ type RankGeometry = {
|
|
|
397
435
|
centers: number[];
|
|
398
436
|
};
|
|
399
437
|
|
|
400
|
-
type PlacedRank = RankGeometry & { y: number };
|
|
438
|
+
type PlacedRank = RankGeometry & { y: number; height: number };
|
|
401
439
|
|
|
402
440
|
/** A strip segment with final pixel geometry and its assigned track row. */
|
|
403
441
|
type GeomSegment = {
|
|
404
442
|
edgeId: string;
|
|
405
443
|
label?: string | undefined;
|
|
444
|
+
fromCell: number;
|
|
406
445
|
fromX: number;
|
|
407
446
|
toX: number;
|
|
408
447
|
track: number;
|
|
@@ -441,18 +480,26 @@ export function renderGraphLines(
|
|
|
441
480
|
return [];
|
|
442
481
|
}
|
|
443
482
|
const nodeStyle = options.nodeStyle ?? "line";
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
const steps = view.
|
|
483
|
+
const layout = view.graphScene ?? layoutGraph(snapshot);
|
|
484
|
+
const stateSteps = view.state.steps;
|
|
485
|
+
const steps = view.graphSteps ?? stateSteps;
|
|
447
486
|
const boundedIndex = Math.min(Math.max(selectedStepIndex, -1), steps.length - 1);
|
|
448
487
|
const atLatestStep = boundedIndex >= steps.length - 1;
|
|
449
488
|
const visibleSteps = steps.slice(0, boundedIndex + 1);
|
|
450
|
-
const transitions = takenTransitions(visibleSteps);
|
|
489
|
+
const transitions = new Set(view.takenTransitions ?? takenTransitions(visibleSteps));
|
|
451
490
|
const activePair = derivePairInFlight(view, visibleSteps, atLatestStep);
|
|
452
491
|
|
|
453
492
|
const rendered = layout.ranks.map((rank) =>
|
|
454
493
|
rank.map((cell) =>
|
|
455
|
-
renderCellText(
|
|
494
|
+
renderCellText(
|
|
495
|
+
view,
|
|
496
|
+
cell,
|
|
497
|
+
visibleSteps,
|
|
498
|
+
atLatestStep,
|
|
499
|
+
now,
|
|
500
|
+
nodeStyle,
|
|
501
|
+
cell.kind === "node" ? cardShape(view, cell.nodeId) : { width: 1, height: 1 },
|
|
502
|
+
),
|
|
456
503
|
),
|
|
457
504
|
);
|
|
458
505
|
|
|
@@ -489,16 +536,17 @@ export function renderGraphLines(
|
|
|
489
536
|
const topLanes = lanes.above(0).length;
|
|
490
537
|
let y = topLanes > 0 ? topLanes + 1 : 0;
|
|
491
538
|
for (const [rankIndex, rank] of geometry.entries()) {
|
|
492
|
-
|
|
539
|
+
const height = Math.max(1, ...rank.cells.map((cell) => cellHeight(nodeStyle, cell)));
|
|
540
|
+
placed.push({ ...rank, y, height });
|
|
493
541
|
y +=
|
|
494
|
-
|
|
542
|
+
height +
|
|
495
543
|
lanes.below(rankIndex).length +
|
|
496
544
|
gapRows(strips[rankIndex] as StripGeometry, rankIndex, layout.ranks.length) +
|
|
497
545
|
lanes.above(rankIndex + 1).length;
|
|
498
546
|
}
|
|
499
547
|
|
|
500
548
|
const canvas = new CharCanvas();
|
|
501
|
-
drawNodes(canvas, placed, layout, transitions, nodeStyle
|
|
549
|
+
drawNodes(canvas, placed, layout, transitions, nodeStyle);
|
|
502
550
|
const labels = drawSegments(
|
|
503
551
|
canvas,
|
|
504
552
|
placed,
|
|
@@ -508,10 +556,9 @@ export function renderGraphLines(
|
|
|
508
556
|
activePair,
|
|
509
557
|
graphWidth,
|
|
510
558
|
nodeStyle,
|
|
511
|
-
metrics.height,
|
|
512
559
|
lanes,
|
|
513
560
|
);
|
|
514
|
-
drawBackEdges(canvas, placed, layout, transitions, graphWidth,
|
|
561
|
+
drawBackEdges(canvas, placed, layout, transitions, graphWidth, lanes);
|
|
515
562
|
// Labels go on last, once every line is on the canvas: placement can then
|
|
516
563
|
// guarantee no later stroke crosses through a label.
|
|
517
564
|
for (const label of labels) {
|
|
@@ -610,7 +657,14 @@ function computeStripGeometry(
|
|
|
610
657
|
if (targetIsNode && Math.abs(toX - fromX) <= 1) {
|
|
611
658
|
toX = fromX;
|
|
612
659
|
}
|
|
613
|
-
return {
|
|
660
|
+
return {
|
|
661
|
+
edgeId: segment.edgeId,
|
|
662
|
+
label: segment.label,
|
|
663
|
+
fromCell: segment.fromCell,
|
|
664
|
+
fromX,
|
|
665
|
+
toX,
|
|
666
|
+
targetIsNode,
|
|
667
|
+
};
|
|
614
668
|
});
|
|
615
669
|
|
|
616
670
|
// First-fit track assignment over pixel spans; straight unlabeled
|
|
@@ -697,7 +751,6 @@ function drawNodes(
|
|
|
697
751
|
layout: GraphLayout,
|
|
698
752
|
transitions: Set<string>,
|
|
699
753
|
nodeStyle: GraphNodeStyle,
|
|
700
|
-
boxHeight: number,
|
|
701
754
|
): void {
|
|
702
755
|
for (const rank of placed) {
|
|
703
756
|
for (const [index, rendered] of rank.cells.entries()) {
|
|
@@ -708,12 +761,7 @@ function drawNodes(
|
|
|
708
761
|
const taken = edge ? transitions.has(`${edge.from}->${edge.to}`) : false;
|
|
709
762
|
// Pass-through cells span the full cell height so the edge stays
|
|
710
763
|
// visually continuous across the rank row(s).
|
|
711
|
-
canvas.vline(
|
|
712
|
-
center,
|
|
713
|
-
rank.y,
|
|
714
|
-
rank.y + cellHeight(nodeStyle, boxHeight) - 1,
|
|
715
|
-
taken ? "taken" : "dim",
|
|
716
|
-
);
|
|
764
|
+
canvas.vline(center, rank.y, rank.y + rank.height - 1, taken ? "taken" : "dim");
|
|
717
765
|
continue;
|
|
718
766
|
}
|
|
719
767
|
const status = rendered.status ?? "queued";
|
|
@@ -765,9 +813,13 @@ function drawNodeBox(
|
|
|
765
813
|
rightStyle: CanvasStyle,
|
|
766
814
|
) => {
|
|
767
815
|
rowBorder(row);
|
|
768
|
-
|
|
769
|
-
const rightText = fitText(right,
|
|
770
|
-
|
|
816
|
+
const contentWidth = Math.max(0, innerWidth - 2);
|
|
817
|
+
const rightText = fitText(right, contentWidth);
|
|
818
|
+
const rightWidth = visibleWidth(rightText);
|
|
819
|
+
const gap = left === "" || rightText === "" ? 0 : CARD_PAIRED_ROW_GAP;
|
|
820
|
+
const leftText = fitText(left, Math.max(0, contentWidth - rightWidth - gap));
|
|
821
|
+
canvas.text(startX + 2, row, leftText, leftStyle);
|
|
822
|
+
canvas.text(startX + 2 + contentWidth - rightWidth, row, rightText, rightStyle);
|
|
771
823
|
};
|
|
772
824
|
|
|
773
825
|
canvas.text(startX, y, `${chars.tl}${horizontal}${chars.tr}`, borderStyle);
|
|
@@ -825,7 +877,6 @@ function drawSegments(
|
|
|
825
877
|
activePair: string | null,
|
|
826
878
|
graphWidth: number,
|
|
827
879
|
nodeStyle: GraphNodeStyle,
|
|
828
|
-
boxHeight: number,
|
|
829
880
|
lanes: BackEdgeLanes,
|
|
830
881
|
): PendingLabel[] {
|
|
831
882
|
const labels: PendingLabel[] = [];
|
|
@@ -839,11 +890,13 @@ function drawSegments(
|
|
|
839
890
|
// Forward lines start right below the source cell, cross any back-edge
|
|
840
891
|
// lane rows (as ┼ crossings), run their strip tracks, then cross the
|
|
841
892
|
// entry lanes to the arrow row directly above the target cell.
|
|
842
|
-
const
|
|
843
|
-
const stripTop = stubTop + lanes.below(rank).length;
|
|
893
|
+
const stripTop = top.y + top.height + lanes.below(rank).length;
|
|
844
894
|
const arrowY = bottom.y - 1;
|
|
845
895
|
const stripBottom = arrowY - 1 - lanes.above(rank + 1).length;
|
|
846
896
|
for (const segment of strip.segments) {
|
|
897
|
+
const source = top.cells[segment.fromCell];
|
|
898
|
+
const stubTop =
|
|
899
|
+
source?.cell.kind === "node" ? top.y + cellHeight(nodeStyle, source) : top.y + top.height;
|
|
847
900
|
const edge = layout.edges.find((candidate) => candidate.edgeId === segment.edgeId);
|
|
848
901
|
if (!edge) {
|
|
849
902
|
continue;
|
|
@@ -906,23 +959,25 @@ function drawSegmentLabel(canvas: CharCanvas, label: PendingLabel): void {
|
|
|
906
959
|
if (fromX !== toX) {
|
|
907
960
|
const runStart = Math.min(fromX, toX) + 1;
|
|
908
961
|
const runEnd = Math.max(fromX, toX) - 1;
|
|
909
|
-
const
|
|
962
|
+
const paddedWidth = visibleWidth(padded);
|
|
963
|
+
const center = Math.floor((runStart + runEnd) / 2) - Math.floor(paddedWidth / 2);
|
|
910
964
|
if (
|
|
911
|
-
runEnd - runStart + 1 >=
|
|
965
|
+
runEnd - runStart + 1 >= paddedWidth + 2 &&
|
|
912
966
|
canvas.textOverRun(center, trackY, padded, style)
|
|
913
967
|
) {
|
|
914
968
|
return;
|
|
915
969
|
}
|
|
916
970
|
}
|
|
971
|
+
const textWidth = visibleWidth(text);
|
|
917
972
|
const candidates: [number, number][] =
|
|
918
973
|
toX >= Math.floor(graphWidth / 2)
|
|
919
974
|
? [
|
|
920
|
-
[toX -
|
|
975
|
+
[toX - textWidth - 1, labelRow],
|
|
921
976
|
[toX + 2, labelRow],
|
|
922
977
|
]
|
|
923
978
|
: [
|
|
924
979
|
[toX + 2, labelRow],
|
|
925
|
-
[toX -
|
|
980
|
+
[toX - textWidth - 1, labelRow],
|
|
926
981
|
];
|
|
927
982
|
for (const [x, y] of candidates) {
|
|
928
983
|
if (canvas.textIfEmpty(x, y, text, style)) {
|
|
@@ -946,8 +1001,6 @@ function drawBackEdges(
|
|
|
946
1001
|
layout: GraphLayout,
|
|
947
1002
|
transitions: Set<string>,
|
|
948
1003
|
graphWidth: number,
|
|
949
|
-
nodeStyle: GraphNodeStyle,
|
|
950
|
-
boxHeight: number,
|
|
951
1004
|
lanes: BackEdgeLanes,
|
|
952
1005
|
): void {
|
|
953
1006
|
let gutterX = graphWidth + GUTTER_GAP;
|
|
@@ -965,14 +1018,16 @@ function drawBackEdges(
|
|
|
965
1018
|
continue;
|
|
966
1019
|
}
|
|
967
1020
|
const style: CanvasStyle = transitions.has(`${edge.from}->${edge.to}`) ? "taken" : "back";
|
|
968
|
-
const exitLaneY = from.y +
|
|
1021
|
+
const exitLaneY = from.y + from.height + exit.lane;
|
|
969
1022
|
const aboveCount = lanes.above(toRank).length;
|
|
970
1023
|
const arrowY = to.y - 1;
|
|
971
1024
|
const entryLaneY = arrowY - aboveCount + entry.lane;
|
|
972
1025
|
|
|
973
|
-
// Downward stub out of the source cell,
|
|
974
|
-
|
|
975
|
-
|
|
1026
|
+
// Downward stub out of the source cell, through any rank padding, then
|
|
1027
|
+
// right below the tallest card. A short card must not route an edge
|
|
1028
|
+
// through a taller card in the same rank.
|
|
1029
|
+
if (exitLaneY > from.y + exit.height) {
|
|
1030
|
+
canvas.vline(exit.x, from.y + exit.height, exitLaneY - 1, style);
|
|
976
1031
|
}
|
|
977
1032
|
canvas.put(exit.x, exitLaneY, "└", style);
|
|
978
1033
|
canvas.hline(exitLaneY, exit.x + 1, gutterX - 1, style);
|
|
@@ -992,7 +1047,7 @@ function drawBackEdges(
|
|
|
992
1047
|
canvas.text(gutterX + 2, entryLaneY, edge.label, style);
|
|
993
1048
|
}
|
|
994
1049
|
// Reserve horizontal room for this gutter and its label before the next.
|
|
995
|
-
gutterX += 2 + (edge.label === undefined ? 0 : edge.label
|
|
1050
|
+
gutterX += 2 + (edge.label === undefined ? 0 : visibleWidth(edge.label) + 1);
|
|
996
1051
|
}
|
|
997
1052
|
}
|
|
998
1053
|
|
|
@@ -1006,7 +1061,7 @@ function cellAnchor(
|
|
|
1006
1061
|
nodeId: string,
|
|
1007
1062
|
laneEdges: GraphEdge[],
|
|
1008
1063
|
edge: GraphEdge,
|
|
1009
|
-
): { x: number; lane: number } | null {
|
|
1064
|
+
): { x: number; lane: number; height: number } | null {
|
|
1010
1065
|
const index = rank.cells.findIndex(
|
|
1011
1066
|
(cell) => cell.cell.kind === "node" && cell.cell.nodeId === nodeId,
|
|
1012
1067
|
);
|
|
@@ -1017,5 +1072,5 @@ function cellAnchor(
|
|
|
1017
1072
|
const cell = rank.cells[index] as RenderedCell;
|
|
1018
1073
|
const center = rank.centers[index] as number;
|
|
1019
1074
|
const rightmost = center + Math.floor(cell.width / 2) - 1;
|
|
1020
|
-
return { x: Math.min(center + 2 + lane * 2, rightmost), lane };
|
|
1075
|
+
return { x: Math.min(center + 2 + lane * 2, rightmost), lane, height: cell.height };
|
|
1021
1076
|
}
|
package/src/state/database.ts
CHANGED
|
@@ -17,7 +17,7 @@ const DATABASE_FILE = "state.sqlite";
|
|
|
17
17
|
const BUSY_TIMEOUT_MS = 5_000;
|
|
18
18
|
const JOURNAL_SIZE_LIMIT = 64 * 1024 * 1024;
|
|
19
19
|
const RESET_INSTRUCTION =
|
|
20
|
-
"Pi Workflows durable state is incompatible.
|
|
20
|
+
"Pi Workflows durable state is incompatible. Back up and move state.sqlite with its -wal and -shm files, then start Pi Workflows to create a new state.sqlite database. The incompatible state was not changed.";
|
|
21
21
|
|
|
22
22
|
export type StateDatabaseMode = "read-write" | "read-only";
|
|
23
23
|
|
|
@@ -114,6 +114,7 @@ export class StateDatabase {
|
|
|
114
114
|
if (this.mode === "read-only") {
|
|
115
115
|
throw new Error("Cannot mutate a read-only Pi Workflows database");
|
|
116
116
|
}
|
|
117
|
+
if (this.connection.inTransaction) return operation();
|
|
117
118
|
this.connection.exec("BEGIN IMMEDIATE");
|
|
118
119
|
try {
|
|
119
120
|
const result = operation();
|
package/src/state/index.ts
CHANGED
|
@@ -19,6 +19,20 @@ export {
|
|
|
19
19
|
type ResourceType,
|
|
20
20
|
type WritePermit,
|
|
21
21
|
} from "./mutation.js";
|
|
22
|
+
export {
|
|
23
|
+
VIEWER_DELTA_RETENTION,
|
|
24
|
+
VIEWER_PAGE_SIZE,
|
|
25
|
+
initializeViewerRun,
|
|
26
|
+
readViewerDeltas,
|
|
27
|
+
recordViewerDeltas,
|
|
28
|
+
viewerTailPatch,
|
|
29
|
+
type ViewerCursorResult,
|
|
30
|
+
type ViewerDeltaDraft,
|
|
31
|
+
type ViewerDeltaRecord,
|
|
32
|
+
type ViewerPatchOperation,
|
|
33
|
+
type ViewerTailItem,
|
|
34
|
+
type ViewerTargetType,
|
|
35
|
+
} from "./viewer.js";
|
|
22
36
|
export {
|
|
23
37
|
STATE_APPLICATION_ID,
|
|
24
38
|
STATE_APP_VERSION,
|
package/src/state/prune.ts
CHANGED
|
@@ -10,6 +10,7 @@ const UNSETTLED_EFFECT_STATUSES = ["pending", "applying", "ambiguous"];
|
|
|
10
10
|
type RunAgeRow = {
|
|
11
11
|
runId: string;
|
|
12
12
|
parentRunId: string | null;
|
|
13
|
+
launchOptionsHash: Buffer;
|
|
13
14
|
status: string;
|
|
14
15
|
finishedAt: number | null;
|
|
15
16
|
};
|
|
@@ -52,7 +53,7 @@ export async function pruneState(
|
|
|
52
53
|
filePath: databasePath,
|
|
53
54
|
mode: options.apply ? "read-write" : "read-only",
|
|
54
55
|
});
|
|
55
|
-
const selection = selectRunTrees(state
|
|
56
|
+
const selection = selectRunTrees(state, cutoff);
|
|
56
57
|
const sizeBefore = databaseBytes(databasePath);
|
|
57
58
|
const base = {
|
|
58
59
|
cutoff: new Date(cutoff).toISOString(),
|
|
@@ -83,7 +84,7 @@ export async function pruneState(
|
|
|
83
84
|
let deletedBlobBytes = 0;
|
|
84
85
|
state.connection.exec("BEGIN EXCLUSIVE");
|
|
85
86
|
try {
|
|
86
|
-
const checked = selectRunTrees(state
|
|
87
|
+
const checked = selectRunTrees(state, cutoff);
|
|
87
88
|
if (
|
|
88
89
|
checked.runIds.join("\0") !== selection.runIds.join("\0") ||
|
|
89
90
|
checked.signature !== selection.signature
|
|
@@ -141,12 +142,14 @@ export async function pruneState(
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
function selectRunTrees(
|
|
144
|
-
|
|
145
|
+
state: StateDatabase,
|
|
145
146
|
cutoff: number,
|
|
146
147
|
): { candidateTrees: number; blockedTrees: number; runIds: string[]; signature: string } {
|
|
148
|
+
const { connection: database } = state;
|
|
147
149
|
const rows = database
|
|
148
150
|
.prepare(
|
|
149
|
-
`SELECT run_id AS runId, parent_run_id AS parentRunId,
|
|
151
|
+
`SELECT run_id AS runId, parent_run_id AS parentRunId,
|
|
152
|
+
launch_options_hash AS launchOptionsHash, status, finished_at AS finishedAt
|
|
150
153
|
FROM runs ORDER BY created_at, run_id`,
|
|
151
154
|
)
|
|
152
155
|
.all()
|
|
@@ -162,15 +165,25 @@ function selectRunTrees(
|
|
|
162
165
|
.map((row) => row.runId),
|
|
163
166
|
);
|
|
164
167
|
const children = new Map<string, string[]>();
|
|
168
|
+
const parents = new Map<string, string[]>();
|
|
165
169
|
for (const row of rows) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const runParents = new Set<string>();
|
|
171
|
+
if (row.parentRunId !== null) runParents.add(row.parentRunId);
|
|
172
|
+
const restartParentRunId = restartParentFromLaunchOptions(
|
|
173
|
+
state.readJson(row.launchOptionsHash),
|
|
174
|
+
);
|
|
175
|
+
if (restartParentRunId !== null) runParents.add(restartParentRunId);
|
|
176
|
+
parents.set(row.runId, [...runParents]);
|
|
177
|
+
for (const parentRunId of runParents) {
|
|
178
|
+
const values = children.get(parentRunId) ?? [];
|
|
179
|
+
values.push(row.runId);
|
|
180
|
+
children.set(parentRunId, values);
|
|
181
|
+
}
|
|
170
182
|
}
|
|
171
183
|
const roots = rows.filter(
|
|
172
184
|
(row) =>
|
|
173
|
-
eligible.has(row.runId) &&
|
|
185
|
+
eligible.has(row.runId) &&
|
|
186
|
+
(parents.get(row.runId) ?? []).every((parentRunId) => !eligible.has(parentRunId)),
|
|
174
187
|
);
|
|
175
188
|
const selected = new Set<string>();
|
|
176
189
|
let blockedTrees = 0;
|
|
@@ -407,15 +420,28 @@ function blobReferencePredicate(database: Database.Database): string {
|
|
|
407
420
|
|
|
408
421
|
function descendants(root: string, children: Map<string, string[]>): string[] {
|
|
409
422
|
const result: string[] = [];
|
|
423
|
+
const seen = new Set<string>();
|
|
410
424
|
const pending = [root];
|
|
411
425
|
while (pending.length !== 0) {
|
|
412
426
|
const runId = pending.pop() as string;
|
|
427
|
+
if (seen.has(runId)) continue;
|
|
428
|
+
seen.add(runId);
|
|
413
429
|
result.push(runId);
|
|
414
430
|
pending.push(...(children.get(runId) ?? []));
|
|
415
431
|
}
|
|
416
432
|
return result;
|
|
417
433
|
}
|
|
418
434
|
|
|
435
|
+
function restartParentFromLaunchOptions(value: unknown): string | null {
|
|
436
|
+
if (!isRecord(value)) throw new Error("Stored workflow launch options are invalid");
|
|
437
|
+
const lineage = value.restartLineage;
|
|
438
|
+
if (lineage === undefined) return null;
|
|
439
|
+
if (!isRecord(lineage) || typeof lineage.parentRunId !== "string") {
|
|
440
|
+
throw new Error("Stored workflow restart lineage is invalid");
|
|
441
|
+
}
|
|
442
|
+
return lineage.parentRunId;
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
function parseCutoff(value: string): number {
|
|
420
446
|
const cutoff = Date.parse(value);
|
|
421
447
|
if (!Number.isFinite(cutoff)) throw new Error("state prune --before requires a valid timestamp");
|