@osolmaz/pi-workflows 0.15.2 → 0.16.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 +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +56 -3
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
package/src/workflows/store.ts
CHANGED
|
@@ -78,6 +78,36 @@ export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1" as const;
|
|
|
78
78
|
export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1" as const;
|
|
79
79
|
export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
|
|
80
80
|
|
|
81
|
+
const STEP_ROW_SELECT = `
|
|
82
|
+
SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
|
|
83
|
+
a.node_type AS nodeType, a.status,
|
|
84
|
+
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
85
|
+
s.output_override_hash AS outputOverrideHash,
|
|
86
|
+
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
87
|
+
prompt_entry.entry_hash AS promptEntryHash,
|
|
88
|
+
response_entry.entry_hash AS responseEntryHash,
|
|
89
|
+
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
90
|
+
a.settings_scope_id AS settingsScopeId,
|
|
91
|
+
a.settings_change_number AS settingsChangeNumber,
|
|
92
|
+
a.settings_hash AS settingsHash,
|
|
93
|
+
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
94
|
+
FROM run_steps s
|
|
95
|
+
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
96
|
+
LEFT JOIN attempt_entries prompt_link
|
|
97
|
+
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
98
|
+
LEFT JOIN session_entries prompt_entry
|
|
99
|
+
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
100
|
+
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
101
|
+
LEFT JOIN attempt_entries response_link
|
|
102
|
+
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
103
|
+
LEFT JOIN session_entries response_entry
|
|
104
|
+
ON response_entry.segment_id = response_link.segment_id
|
|
105
|
+
AND response_entry.entry_id = response_link.entry_id
|
|
106
|
+
LEFT JOIN attempt_entries first_link
|
|
107
|
+
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
108
|
+
LEFT JOIN attempt_entries last_link
|
|
109
|
+
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'`;
|
|
110
|
+
|
|
81
111
|
const FOLLOW_UP_ROW_SELECT = `
|
|
82
112
|
SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
|
|
83
113
|
f.queue_resource_id AS queueResourceId, f.run_id AS runId,
|
|
@@ -110,6 +140,12 @@ export type RunWriteAuthority = {
|
|
|
110
140
|
leaseMs?: number;
|
|
111
141
|
};
|
|
112
142
|
|
|
143
|
+
export type WorkflowRunDisplayState = {
|
|
144
|
+
status: WorkflowRunState["status"];
|
|
145
|
+
paused: boolean;
|
|
146
|
+
error: string | null;
|
|
147
|
+
};
|
|
148
|
+
|
|
113
149
|
export type WorkflowRunStoreOptions = {
|
|
114
150
|
authorityProvider?: (runId: string) => RunWriteAuthority | undefined;
|
|
115
151
|
/** Host-owned projection updates that must commit with each run snapshot. */
|
|
@@ -410,6 +446,48 @@ export type LoadedWorkflowRun = {
|
|
|
410
446
|
followUpQueue?: WorkflowFollowUpQueueRecord | null;
|
|
411
447
|
};
|
|
412
448
|
|
|
449
|
+
export type WorkflowRunViewCounts = {
|
|
450
|
+
steps: number;
|
|
451
|
+
trace: number;
|
|
452
|
+
sessionEntries: number;
|
|
453
|
+
sessionEvents: number;
|
|
454
|
+
settings: number;
|
|
455
|
+
followUps: number;
|
|
456
|
+
updates: number;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export type WorkflowRunViewRange = {
|
|
460
|
+
start: number;
|
|
461
|
+
limit: number;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export type WorkflowRunViewRead = {
|
|
465
|
+
runId: string;
|
|
466
|
+
state: WorkflowRunState;
|
|
467
|
+
snapshot: WorkflowDefinitionSnapshot;
|
|
468
|
+
traceEvents: WorkflowTraceEvent[];
|
|
469
|
+
sessionBinding: WorkflowSessionBinding | null;
|
|
470
|
+
sessionEntries: WorkflowSessionEntryRecord[];
|
|
471
|
+
sessionEvents: WorkflowSessionEventRecord[];
|
|
472
|
+
sessionCapture: WorkflowSessionCapture | null;
|
|
473
|
+
sessionIntegrity: SessionCaptureIntegrity;
|
|
474
|
+
settingsScopes: WorkflowSettingsScopeRecord[];
|
|
475
|
+
followUpQueue: WorkflowFollowUpQueueRecord | null;
|
|
476
|
+
graphSteps: WorkflowStepRecord[];
|
|
477
|
+
takenTransitions: string[];
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export type WorkflowRunViewReadOptions = {
|
|
481
|
+
steps: WorkflowRunViewRange;
|
|
482
|
+
trace: WorkflowRunViewRange;
|
|
483
|
+
sessionEntries: WorkflowRunViewRange;
|
|
484
|
+
sessionEvents: WorkflowRunViewRange;
|
|
485
|
+
settings: WorkflowRunViewRange;
|
|
486
|
+
followUps: WorkflowRunViewRange;
|
|
487
|
+
updates: WorkflowRunViewRange;
|
|
488
|
+
graphCursor: number;
|
|
489
|
+
};
|
|
490
|
+
|
|
413
491
|
export type ReadWorkflowRunOptions = {
|
|
414
492
|
includeTrace?: boolean;
|
|
415
493
|
};
|
|
@@ -1033,6 +1111,28 @@ export class WorkflowRunStore {
|
|
|
1033
1111
|
.map((row) => this.settingsScopeRecord(row));
|
|
1034
1112
|
}
|
|
1035
1113
|
|
|
1114
|
+
private readSettingsScopeRange(
|
|
1115
|
+
runId: string,
|
|
1116
|
+
range: WorkflowRunViewRange,
|
|
1117
|
+
): WorkflowSettingsScopeRecord[] {
|
|
1118
|
+
const originRunId = this.logicalOriginRunId(runId);
|
|
1119
|
+
return this.state.connection
|
|
1120
|
+
.prepare(
|
|
1121
|
+
`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
1122
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
1123
|
+
s.mount_path AS mountPath, s.invocation,
|
|
1124
|
+
s.current_hash AS currentHash, r.revision,
|
|
1125
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
1126
|
+
FROM workflow_settings s
|
|
1127
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
1128
|
+
WHERE s.origin_run_id = ? OR s.active_run_id = ?
|
|
1129
|
+
ORDER BY s.mount_path, s.invocation LIMIT ? OFFSET ?`,
|
|
1130
|
+
)
|
|
1131
|
+
.all(originRunId, runId, range.limit, range.start)
|
|
1132
|
+
.filter(isSettingsScopeRow)
|
|
1133
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1036
1136
|
findSettingsScope(
|
|
1037
1137
|
runId: string,
|
|
1038
1138
|
mountPath: string,
|
|
@@ -1357,6 +1457,36 @@ export class WorkflowRunStore {
|
|
|
1357
1457
|
};
|
|
1358
1458
|
}
|
|
1359
1459
|
|
|
1460
|
+
private readFollowUpQueueRange(
|
|
1461
|
+
runId: string,
|
|
1462
|
+
range: WorkflowRunViewRange,
|
|
1463
|
+
): WorkflowFollowUpQueueRecord | null {
|
|
1464
|
+
const queue = this.followUpQueueRow(runId);
|
|
1465
|
+
if (queue === undefined) return null;
|
|
1466
|
+
const followUps = this.state.connection
|
|
1467
|
+
.prepare(
|
|
1468
|
+
`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number LIMIT ? OFFSET ?`,
|
|
1469
|
+
)
|
|
1470
|
+
.all(runId, range.limit, range.start)
|
|
1471
|
+
.filter(isFollowUpRow)
|
|
1472
|
+
.map((row) => this.followUpRecord(row));
|
|
1473
|
+
return {
|
|
1474
|
+
runId,
|
|
1475
|
+
...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
|
|
1476
|
+
presentationState: assertPresentationState(queue.presentationState),
|
|
1477
|
+
...(queue.presentationEntryId !== null
|
|
1478
|
+
? { presentationEntryId: queue.presentationEntryId }
|
|
1479
|
+
: {}),
|
|
1480
|
+
...(queue.presentationAssistantEntryId !== null
|
|
1481
|
+
? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
|
|
1482
|
+
: {}),
|
|
1483
|
+
...(queue.presentationReasonHash !== null
|
|
1484
|
+
? { presentationReason: this.readText(queue.presentationReasonHash) }
|
|
1485
|
+
: {}),
|
|
1486
|
+
followUps,
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1360
1490
|
listPendingPresentations(targetSessionId: string): string[] {
|
|
1361
1491
|
return this.state.connection
|
|
1362
1492
|
.prepare(
|
|
@@ -2289,6 +2419,217 @@ export class WorkflowRunStore {
|
|
|
2289
2419
|
};
|
|
2290
2420
|
}
|
|
2291
2421
|
|
|
2422
|
+
readRunViewCounts(runId: string): WorkflowRunViewCounts | null {
|
|
2423
|
+
const row = this.readRunRow(runId);
|
|
2424
|
+
if (row === undefined) return null;
|
|
2425
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
2426
|
+
const settingsOrigin = this.logicalOriginRunId(runId);
|
|
2427
|
+
const scalar = (sql: string, ...params: unknown[]): number => {
|
|
2428
|
+
const count = this.state.connection.prepare(sql).get(...params);
|
|
2429
|
+
return isCountRow(count) ? count.count : 0;
|
|
2430
|
+
};
|
|
2431
|
+
return {
|
|
2432
|
+
steps: scalar("SELECT count(*) AS count FROM run_steps WHERE run_id = ?", runId),
|
|
2433
|
+
trace: scalar("SELECT count(*) AS count FROM events WHERE resource_id = ?", row.resourceId),
|
|
2434
|
+
sessionEntries: segment?.entryCount ?? 0,
|
|
2435
|
+
sessionEvents: segment?.eventCount ?? 0,
|
|
2436
|
+
settings: scalar(
|
|
2437
|
+
`SELECT count(*) AS count FROM workflow_settings
|
|
2438
|
+
WHERE origin_run_id = ? OR active_run_id = ?`,
|
|
2439
|
+
settingsOrigin,
|
|
2440
|
+
runId,
|
|
2441
|
+
),
|
|
2442
|
+
followUps: scalar(
|
|
2443
|
+
"SELECT count(*) AS count FROM workflow_follow_ups WHERE run_id = ?",
|
|
2444
|
+
runId,
|
|
2445
|
+
),
|
|
2446
|
+
updates: scalar(
|
|
2447
|
+
`SELECT count(*) AS count FROM (
|
|
2448
|
+
SELECT 1 FROM workflow_updates u
|
|
2449
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
2450
|
+
WHERE a.run_id = ? GROUP BY u.update_type, u.update_key
|
|
2451
|
+
)`,
|
|
2452
|
+
runId,
|
|
2453
|
+
),
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
readRunView(runId: string, options: WorkflowRunViewReadOptions): WorkflowRunViewRead | null {
|
|
2458
|
+
const row = this.readRunRow(runId);
|
|
2459
|
+
if (row === undefined) return null;
|
|
2460
|
+
const snapshot = this.readDefinition(row.definitionHash);
|
|
2461
|
+
const steps = this.readStepRange(runId, options.steps);
|
|
2462
|
+
const visibleUpdates = this.readCurrentUpdates(runId, options.updates);
|
|
2463
|
+
const state = this.buildRunState(
|
|
2464
|
+
row,
|
|
2465
|
+
snapshot,
|
|
2466
|
+
this.readLatestSteps(runId),
|
|
2467
|
+
steps,
|
|
2468
|
+
visibleUpdates,
|
|
2469
|
+
);
|
|
2470
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
2471
|
+
const loadedSegment =
|
|
2472
|
+
segment === undefined
|
|
2473
|
+
? emptySegment()
|
|
2474
|
+
: this.loadSegment(segment, state, {
|
|
2475
|
+
entries: options.sessionEntries,
|
|
2476
|
+
events: options.sessionEvents,
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
runId,
|
|
2480
|
+
state,
|
|
2481
|
+
snapshot,
|
|
2482
|
+
traceEvents: this.traceEvents(row, options.trace),
|
|
2483
|
+
sessionBinding: loadedSegment.binding,
|
|
2484
|
+
sessionEntries: loadedSegment.entries,
|
|
2485
|
+
sessionEvents: loadedSegment.events,
|
|
2486
|
+
sessionCapture: loadedSegment.capture,
|
|
2487
|
+
sessionIntegrity: loadedSegment.integrity,
|
|
2488
|
+
settingsScopes: this.readSettingsScopeRange(runId, options.settings),
|
|
2489
|
+
followUpQueue: this.readFollowUpQueueRange(runId, options.followUps),
|
|
2490
|
+
graphSteps: this.readLatestSteps(runId, options.graphCursor),
|
|
2491
|
+
takenTransitions: this.readTakenTransitions(runId, options.graphCursor),
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
traceCursorForStep(runId: string, stepCursor: number, traceTotal: number): number {
|
|
2496
|
+
const step = this.state.connection
|
|
2497
|
+
.prepare(
|
|
2498
|
+
`SELECT a.attempt_id AS attemptId, a.node_id AS nodeId
|
|
2499
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2500
|
+
WHERE s.run_id = ? AND s.step_index = ?`,
|
|
2501
|
+
)
|
|
2502
|
+
.get(runId, stepCursor);
|
|
2503
|
+
if (!isRecord(step) || typeof step.attemptId !== "string" || typeof step.nodeId !== "string") {
|
|
2504
|
+
return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
2505
|
+
}
|
|
2506
|
+
const run = this.readRunRow(runId);
|
|
2507
|
+
if (run === undefined) return 0;
|
|
2508
|
+
const exactAttempt = this.state.connection
|
|
2509
|
+
.prepare(
|
|
2510
|
+
`SELECT e.resource_revision AS revision FROM events e
|
|
2511
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
2512
|
+
WHERE e.resource_id = ?
|
|
2513
|
+
AND json_extract(CAST(b.content AS TEXT), '$.attemptId') = ?
|
|
2514
|
+
ORDER BY e.resource_revision DESC LIMIT 1`,
|
|
2515
|
+
)
|
|
2516
|
+
.get(run.resourceId, step.attemptId);
|
|
2517
|
+
const matched = isRevisionRow(exactAttempt)
|
|
2518
|
+
? exactAttempt
|
|
2519
|
+
: this.state.connection
|
|
2520
|
+
.prepare(
|
|
2521
|
+
`SELECT e.resource_revision AS revision FROM events e
|
|
2522
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
2523
|
+
WHERE e.resource_id = ?
|
|
2524
|
+
AND json_extract(CAST(b.content AS TEXT), '$.nodeId') = ?
|
|
2525
|
+
ORDER BY e.resource_revision DESC LIMIT 1`,
|
|
2526
|
+
)
|
|
2527
|
+
.get(run.resourceId, step.nodeId);
|
|
2528
|
+
if (!isRevisionRow(matched)) return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
2529
|
+
const index = this.state.connection
|
|
2530
|
+
.prepare(
|
|
2531
|
+
`SELECT count(*) - 1 AS count FROM events
|
|
2532
|
+
WHERE resource_id = ? AND resource_revision <= ?`,
|
|
2533
|
+
)
|
|
2534
|
+
.get(run.resourceId, matched.revision);
|
|
2535
|
+
return isCountRow(index) ? Math.max(0, index.count) : 0;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
persistViewContent(runId: string, content: Buffer, mediaType: string): string {
|
|
2539
|
+
if (this.readRunRow(runId) === undefined) throw new Error(`Workflow run not found: ${runId}`);
|
|
2540
|
+
return this.state.transaction(() => {
|
|
2541
|
+
const hash = createHash("sha256").update(content).digest();
|
|
2542
|
+
this.state.connection
|
|
2543
|
+
.prepare(
|
|
2544
|
+
`INSERT INTO run_view_content(
|
|
2545
|
+
run_id, content_hash, media_type, byte_length, content, created_at
|
|
2546
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
2547
|
+
ON CONFLICT(run_id, content_hash, media_type) DO NOTHING`,
|
|
2548
|
+
)
|
|
2549
|
+
.run(runId, hash, mediaType, content.byteLength, content, Date.now());
|
|
2550
|
+
const stored = this.state.connection
|
|
2551
|
+
.prepare(
|
|
2552
|
+
`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
2553
|
+
FROM run_view_content
|
|
2554
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`,
|
|
2555
|
+
)
|
|
2556
|
+
.get(runId, hash, mediaType);
|
|
2557
|
+
if (
|
|
2558
|
+
!isRecord(stored) ||
|
|
2559
|
+
stored.mediaType !== mediaType ||
|
|
2560
|
+
stored.byteLength !== content.byteLength ||
|
|
2561
|
+
!Buffer.isBuffer(stored.content) ||
|
|
2562
|
+
!stored.content.equals(content)
|
|
2563
|
+
) {
|
|
2564
|
+
throw new Error("Run-scoped view content conflict");
|
|
2565
|
+
}
|
|
2566
|
+
return hash.toString("hex");
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
readContentBlob(
|
|
2571
|
+
runId: string,
|
|
2572
|
+
digest: string,
|
|
2573
|
+
mediaType: string,
|
|
2574
|
+
): { mediaType: string; content: Buffer } | undefined {
|
|
2575
|
+
if (!/^[0-9a-f]{64}$/u.test(digest)) return undefined;
|
|
2576
|
+
const stored = this.state.connection
|
|
2577
|
+
.prepare(
|
|
2578
|
+
`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
2579
|
+
FROM run_view_content
|
|
2580
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`,
|
|
2581
|
+
)
|
|
2582
|
+
.get(runId, Buffer.from(digest, "hex"), mediaType);
|
|
2583
|
+
if (
|
|
2584
|
+
!isRecord(stored) ||
|
|
2585
|
+
stored.mediaType !== mediaType ||
|
|
2586
|
+
typeof stored.byteLength !== "number" ||
|
|
2587
|
+
!Buffer.isBuffer(stored.content) ||
|
|
2588
|
+
stored.content.byteLength !== stored.byteLength
|
|
2589
|
+
) {
|
|
2590
|
+
return undefined;
|
|
2591
|
+
}
|
|
2592
|
+
return { mediaType, content: stored.content };
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
readDisplayState(runId: string): WorkflowRunDisplayState | null {
|
|
2596
|
+
const row = this.readRunRow(runId);
|
|
2597
|
+
if (row === undefined) return null;
|
|
2598
|
+
return {
|
|
2599
|
+
status: row.status as WorkflowRunState["status"],
|
|
2600
|
+
paused: row.paused === 1,
|
|
2601
|
+
error: row.errorHash === null ? null : this.readText(row.errorHash),
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
readSessionReplayCheckpoint(runId: string, throughSequence: number): JsonValue | null {
|
|
2606
|
+
if (!Number.isSafeInteger(throughSequence) || throughSequence < 0) {
|
|
2607
|
+
throw new Error("Session replay checkpoint sequence must be a non-negative integer");
|
|
2608
|
+
}
|
|
2609
|
+
if (throughSequence === 0) return null;
|
|
2610
|
+
const row = this.state.connection
|
|
2611
|
+
.prepare(
|
|
2612
|
+
`SELECT event_seq AS eventSeq, state_hash AS stateHash
|
|
2613
|
+
FROM viewer_session_checkpoints
|
|
2614
|
+
WHERE run_id = ? AND event_seq <= ? ORDER BY event_seq DESC LIMIT 1`,
|
|
2615
|
+
)
|
|
2616
|
+
.get(runId, throughSequence);
|
|
2617
|
+
let checkpoint = reduceSessionEvents([], [], 0);
|
|
2618
|
+
if (isViewerSessionCheckpointRow(row)) {
|
|
2619
|
+
const saved = this.state.readJson(row.stateHash);
|
|
2620
|
+
if (!isTemporalSessionState(saved) || saved.throughSeq !== row.eventSeq) {
|
|
2621
|
+
throw new Error(`Viewer session checkpoint is invalid for ${runId}`);
|
|
2622
|
+
}
|
|
2623
|
+
checkpoint = saved;
|
|
2624
|
+
}
|
|
2625
|
+
if (checkpoint.throughSeq < throughSequence) {
|
|
2626
|
+
const events = this.sessionEventRecords(runId, checkpoint.throughSeq, throughSequence + 1);
|
|
2627
|
+
checkpoint = reduceSessionEventsFromCheckpoint([], events, throughSequence, checkpoint);
|
|
2628
|
+
checkpoint = boundedTemporalCheckpoint(checkpoint);
|
|
2629
|
+
}
|
|
2630
|
+
return parseJson(canonicalJson(checkpoint));
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2292
2633
|
listRuns(options: ReadWorkflowRunOptions = {}): LoadedWorkflowRun[] {
|
|
2293
2634
|
const rows = this.state.connection
|
|
2294
2635
|
.prepare("SELECT run_id AS runId FROM runs ORDER BY created_at DESC, run_id DESC")
|
|
@@ -3407,13 +3748,29 @@ export class WorkflowRunStore {
|
|
|
3407
3748
|
|
|
3408
3749
|
private readRunState(row: RunRow, snapshot: WorkflowDefinitionSnapshot): WorkflowRunState {
|
|
3409
3750
|
const steps = this.readSteps(row.runId);
|
|
3751
|
+
return this.buildRunState(
|
|
3752
|
+
row,
|
|
3753
|
+
snapshot,
|
|
3754
|
+
steps,
|
|
3755
|
+
steps,
|
|
3756
|
+
this.readUpdates(row.runId).updates ?? [],
|
|
3757
|
+
);
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
private buildRunState(
|
|
3761
|
+
row: RunRow,
|
|
3762
|
+
snapshot: WorkflowDefinitionSnapshot,
|
|
3763
|
+
projectionSteps: WorkflowStepRecord[],
|
|
3764
|
+
visibleSteps: WorkflowStepRecord[],
|
|
3765
|
+
visibleUpdates: WorkflowUpdateRecord[],
|
|
3766
|
+
): WorkflowRunState {
|
|
3410
3767
|
const sources = this.readRunSources(row.runId, snapshot);
|
|
3411
3768
|
const carriedStepCount = this.carriedStepCount(row.runId);
|
|
3412
3769
|
const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
|
|
3413
3770
|
const humanDecision = this.readHumanDecisionReceipt(row.runId);
|
|
3414
3771
|
const outputs: Record<string, unknown> = {};
|
|
3415
3772
|
const results: Record<string, WorkflowNodeResult> = {};
|
|
3416
|
-
for (const step of
|
|
3773
|
+
for (const step of projectionSteps) {
|
|
3417
3774
|
const result = resultForStep(step);
|
|
3418
3775
|
results[step.nodeId] = result;
|
|
3419
3776
|
if (step.outcome === "ok") outputs[step.nodeId] = step.output;
|
|
@@ -3449,8 +3806,8 @@ export class WorkflowRunStore {
|
|
|
3449
3806
|
input: this.state.readJson(row.inputHash),
|
|
3450
3807
|
outputs,
|
|
3451
3808
|
results,
|
|
3452
|
-
steps,
|
|
3453
|
-
...
|
|
3809
|
+
steps: visibleSteps,
|
|
3810
|
+
...(visibleUpdates.length === 0 ? {} : { updates: visibleUpdates }),
|
|
3454
3811
|
...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
|
|
3455
3812
|
...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
|
|
3456
3813
|
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
@@ -3472,8 +3829,8 @@ export class WorkflowRunStore {
|
|
|
3472
3829
|
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
3473
3830
|
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
3474
3831
|
...(row.paused === 0 ? {} : { paused: true }),
|
|
3475
|
-
...(row.status === "waiting" &&
|
|
3476
|
-
? { waitingOn:
|
|
3832
|
+
...(row.status === "waiting" && projectionSteps.at(-1) !== undefined
|
|
3833
|
+
? { waitingOn: projectionSteps.at(-1)?.nodeId as string }
|
|
3477
3834
|
: {}),
|
|
3478
3835
|
...(row.finalOutputHash === null
|
|
3479
3836
|
? {}
|
|
@@ -3483,42 +3840,65 @@ export class WorkflowRunStore {
|
|
|
3483
3840
|
}
|
|
3484
3841
|
|
|
3485
3842
|
private readSteps(runId: string): WorkflowStepRecord[] {
|
|
3843
|
+
const rows = this.state.connection
|
|
3844
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
3845
|
+
.all(runId)
|
|
3846
|
+
.filter(isStepRow);
|
|
3847
|
+
return this.mapStepRows(runId, rows, 0);
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
private readStepRange(runId: string, range: WorkflowRunViewRange): WorkflowStepRecord[] {
|
|
3851
|
+
const rows = this.state.connection
|
|
3852
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index LIMIT ? OFFSET ?`)
|
|
3853
|
+
.all(runId, range.limit, range.start)
|
|
3854
|
+
.filter(isStepRow);
|
|
3855
|
+
return this.mapStepRows(runId, rows, range.start);
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
private readLatestSteps(runId: string, through?: number): WorkflowStepRecord[] {
|
|
3859
|
+
const throughClause = through === undefined ? "" : " AND s2.step_index <= ?";
|
|
3860
|
+
const params = through === undefined ? [runId, runId] : [runId, runId, through];
|
|
3486
3861
|
const rows = this.state.connection
|
|
3487
3862
|
.prepare(
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
response_entry.entry_hash AS responseEntryHash,
|
|
3495
|
-
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
3496
|
-
a.settings_scope_id AS settingsScopeId,
|
|
3497
|
-
a.settings_change_number AS settingsChangeNumber,
|
|
3498
|
-
a.settings_hash AS settingsHash,
|
|
3499
|
-
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
3500
|
-
FROM run_steps s
|
|
3501
|
-
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
3502
|
-
LEFT JOIN attempt_entries prompt_link
|
|
3503
|
-
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
3504
|
-
LEFT JOIN session_entries prompt_entry
|
|
3505
|
-
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
3506
|
-
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
3507
|
-
LEFT JOIN attempt_entries response_link
|
|
3508
|
-
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
3509
|
-
LEFT JOIN session_entries response_entry
|
|
3510
|
-
ON response_entry.segment_id = response_link.segment_id
|
|
3511
|
-
AND response_entry.entry_id = response_link.entry_id
|
|
3512
|
-
LEFT JOIN attempt_entries first_link
|
|
3513
|
-
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
3514
|
-
LEFT JOIN attempt_entries last_link
|
|
3515
|
-
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'
|
|
3516
|
-
WHERE s.run_id = ? ORDER BY s.step_index`,
|
|
3863
|
+
`${STEP_ROW_SELECT}
|
|
3864
|
+
WHERE s.run_id = ? AND s.step_index IN (
|
|
3865
|
+
SELECT max(s2.step_index) FROM run_steps s2
|
|
3866
|
+
JOIN node_attempts a2 ON a2.attempt_id = s2.attempt_id
|
|
3867
|
+
WHERE s2.run_id = ?${throughClause} GROUP BY a2.node_id
|
|
3868
|
+
) ORDER BY s.step_index`,
|
|
3517
3869
|
)
|
|
3518
|
-
.all(
|
|
3870
|
+
.all(...params)
|
|
3519
3871
|
.filter(isStepRow);
|
|
3872
|
+
return this.mapStepRows(runId, rows);
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
private readTakenTransitions(runId: string, through: number): string[] {
|
|
3876
|
+
const rows = this.state.connection
|
|
3877
|
+
.prepare(
|
|
3878
|
+
`WITH ordered AS (
|
|
3879
|
+
SELECT s.step_index, a.node_id AS nodeId,
|
|
3880
|
+
lag(a.node_id) OVER (ORDER BY s.step_index) AS previousNodeId
|
|
3881
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
3882
|
+
WHERE s.run_id = ? AND s.step_index <= ?
|
|
3883
|
+
)
|
|
3884
|
+
SELECT DISTINCT previousNodeId, nodeId FROM ordered
|
|
3885
|
+
WHERE previousNodeId IS NOT NULL ORDER BY previousNodeId, nodeId`,
|
|
3886
|
+
)
|
|
3887
|
+
.all(runId, through);
|
|
3888
|
+
return rows.flatMap((row) =>
|
|
3889
|
+
isRecord(row) && typeof row.previousNodeId === "string" && typeof row.nodeId === "string"
|
|
3890
|
+
? [`${row.previousNodeId}->${row.nodeId}`]
|
|
3891
|
+
: [],
|
|
3892
|
+
);
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
private mapStepRows(
|
|
3896
|
+
runId: string,
|
|
3897
|
+
rows: StepRow[],
|
|
3898
|
+
expectedStart?: number,
|
|
3899
|
+
): WorkflowStepRecord[] {
|
|
3520
3900
|
return rows.map((row, index) => {
|
|
3521
|
-
if (row.stepIndex !== index)
|
|
3901
|
+
if (expectedStart !== undefined && row.stepIndex !== expectedStart + index)
|
|
3522
3902
|
throw new Error(`Workflow run step sequence has a gap: ${runId}`);
|
|
3523
3903
|
const receipt =
|
|
3524
3904
|
row.receiptHash === null ? {} : this.readJsonAs<StoredAttemptReceipt>(row.receiptHash);
|
|
@@ -3706,6 +4086,41 @@ export class WorkflowRunStore {
|
|
|
3706
4086
|
return updates === undefined ? {} : { updates };
|
|
3707
4087
|
}
|
|
3708
4088
|
|
|
4089
|
+
private readCurrentUpdates(runId: string, range?: WorkflowRunViewRange): WorkflowUpdateRecord[] {
|
|
4090
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
4091
|
+
const params = range === undefined ? [runId] : [runId, range.limit, range.start];
|
|
4092
|
+
const rows = this.state.connection
|
|
4093
|
+
.prepare(
|
|
4094
|
+
`WITH ranked AS (
|
|
4095
|
+
SELECT u.update_id AS updateId, u.run_revision AS runRevision,
|
|
4096
|
+
a.node_id AS nodeId, u.attempt_id AS attemptId,
|
|
4097
|
+
u.update_type AS updateType, u.update_key AS updateKey,
|
|
4098
|
+
u.data_hash AS dataHash, u.recorded_at AS recordedAt,
|
|
4099
|
+
row_number() OVER (
|
|
4100
|
+
PARTITION BY u.update_type, u.update_key ORDER BY u.run_revision DESC
|
|
4101
|
+
) AS rank
|
|
4102
|
+
FROM workflow_updates u
|
|
4103
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
4104
|
+
WHERE a.run_id = ?
|
|
4105
|
+
)
|
|
4106
|
+
SELECT updateId, runRevision, nodeId, attemptId, updateType, updateKey, dataHash, recordedAt
|
|
4107
|
+
FROM ranked WHERE rank = 1 ORDER BY runRevision${rangeClause}`,
|
|
4108
|
+
)
|
|
4109
|
+
.all(...params)
|
|
4110
|
+
.filter(isUpdateRow);
|
|
4111
|
+
return rows.map((row) => ({
|
|
4112
|
+
updateId: row.updateId,
|
|
4113
|
+
seq: row.runRevision,
|
|
4114
|
+
at: new Date(row.recordedAt).toISOString(),
|
|
4115
|
+
runId,
|
|
4116
|
+
nodeId: row.nodeId,
|
|
4117
|
+
attemptId: row.attemptId,
|
|
4118
|
+
type: row.updateType,
|
|
4119
|
+
key: row.updateKey,
|
|
4120
|
+
data: this.readJsonAs<Record<string, unknown>>(row.dataHash),
|
|
4121
|
+
}));
|
|
4122
|
+
}
|
|
4123
|
+
|
|
3709
4124
|
private readJsonAs<T>(hash: Buffer): T {
|
|
3710
4125
|
return this.state.readJson(hash) as T;
|
|
3711
4126
|
}
|
|
@@ -3726,14 +4141,17 @@ export class WorkflowRunStore {
|
|
|
3726
4141
|
return value as WorkflowDefinitionSnapshot;
|
|
3727
4142
|
}
|
|
3728
4143
|
|
|
3729
|
-
private traceEvents(run: RunRow): WorkflowTraceEvent[] {
|
|
4144
|
+
private traceEvents(run: RunRow, range?: WorkflowRunViewRange): WorkflowTraceEvent[] {
|
|
4145
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
4146
|
+
const params =
|
|
4147
|
+
range === undefined ? [run.resourceId] : [run.resourceId, range.limit, range.start];
|
|
3730
4148
|
const rows = this.state.connection
|
|
3731
4149
|
.prepare(
|
|
3732
4150
|
`SELECT resource_revision AS resourceRevision, event_type AS eventType,
|
|
3733
4151
|
payload_hash AS payloadHash, recorded_at AS recordedAt
|
|
3734
|
-
FROM events WHERE resource_id = ? ORDER BY resource_revision`,
|
|
4152
|
+
FROM events WHERE resource_id = ? ORDER BY resource_revision${rangeClause}`,
|
|
3735
4153
|
)
|
|
3736
|
-
.all(
|
|
4154
|
+
.all(...params);
|
|
3737
4155
|
const events: WorkflowTraceEvent[] = [];
|
|
3738
4156
|
for (const row of rows) {
|
|
3739
4157
|
/* istanbul ignore if -- exact schema and internal query shape */
|
|
@@ -3787,7 +4205,11 @@ export class WorkflowRunStore {
|
|
|
3787
4205
|
return row;
|
|
3788
4206
|
}
|
|
3789
4207
|
|
|
3790
|
-
private loadSegment(
|
|
4208
|
+
private loadSegment(
|
|
4209
|
+
segment: SegmentRow,
|
|
4210
|
+
runState: WorkflowRunState,
|
|
4211
|
+
ranges?: { entries: WorkflowRunViewRange; events: WorkflowRunViewRange },
|
|
4212
|
+
): SessionCaptureSegment {
|
|
3791
4213
|
const binding =
|
|
3792
4214
|
segment.bindingHash === null
|
|
3793
4215
|
? null
|
|
@@ -3795,9 +4217,15 @@ export class WorkflowRunStore {
|
|
|
3795
4217
|
const entries = this.state.connection
|
|
3796
4218
|
.prepare(
|
|
3797
4219
|
`SELECT entry_seq AS entrySeq, entry_hash AS entryHash, recorded_at AS recordedAt
|
|
3798
|
-
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq
|
|
4220
|
+
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq${
|
|
4221
|
+
ranges === undefined ? "" : " LIMIT ? OFFSET ?"
|
|
4222
|
+
}`,
|
|
4223
|
+
)
|
|
4224
|
+
.all(
|
|
4225
|
+
...(ranges === undefined
|
|
4226
|
+
? [segment.segmentId]
|
|
4227
|
+
: [segment.segmentId, ranges.entries.limit, ranges.entries.start]),
|
|
3799
4228
|
)
|
|
3800
|
-
.all(segment.segmentId)
|
|
3801
4229
|
.filter(isSessionEntryRow)
|
|
3802
4230
|
.map((row) => ({
|
|
3803
4231
|
seq: row.entrySeq,
|
|
@@ -3810,9 +4238,15 @@ export class WorkflowRunStore {
|
|
|
3810
4238
|
attempt_id AS attemptId, turn_id AS turnId, message_id AS messageId,
|
|
3811
4239
|
tool_call_id AS toolCallId, payload_hash AS payloadHash,
|
|
3812
4240
|
recorded_at AS recordedAt
|
|
3813
|
-
FROM session_events WHERE segment_id = ? ORDER BY event_seq
|
|
4241
|
+
FROM session_events WHERE segment_id = ? ORDER BY event_seq${
|
|
4242
|
+
ranges === undefined ? "" : " LIMIT ? OFFSET ?"
|
|
4243
|
+
}`,
|
|
4244
|
+
)
|
|
4245
|
+
.all(
|
|
4246
|
+
...(ranges === undefined
|
|
4247
|
+
? [segment.segmentId]
|
|
4248
|
+
: [segment.segmentId, ranges.events.limit, ranges.events.start]),
|
|
3814
4249
|
)
|
|
3815
|
-
.all(segment.segmentId)
|
|
3816
4250
|
.filter(isSessionEventRow)
|
|
3817
4251
|
.map((row) => ({
|
|
3818
4252
|
seq: row.eventSeq,
|
|
@@ -3839,7 +4273,17 @@ export class WorkflowRunStore {
|
|
|
3839
4273
|
...(failure === undefined ? {} : { failure }),
|
|
3840
4274
|
};
|
|
3841
4275
|
const diagnostics: string[] = [];
|
|
3842
|
-
|
|
4276
|
+
const durableCounts =
|
|
4277
|
+
ranges === undefined
|
|
4278
|
+
? { entries: entries.length, events: events.length }
|
|
4279
|
+
: {
|
|
4280
|
+
entries: sessionRecordCount(this.state, "session_entries", segment.segmentId),
|
|
4281
|
+
events: sessionRecordCount(this.state, "session_events", segment.segmentId),
|
|
4282
|
+
};
|
|
4283
|
+
if (
|
|
4284
|
+
durableCounts.entries !== segment.entryCount ||
|
|
4285
|
+
durableCounts.events !== segment.eventCount
|
|
4286
|
+
) {
|
|
3843
4287
|
diagnostics.push("session capture counts do not match durable rows");
|
|
3844
4288
|
}
|
|
3845
4289
|
if (runState.status !== "running" && segment.status === "recording") {
|
|
@@ -4219,6 +4663,17 @@ function workflowSourceFromRow(row: RunSourceRow): WorkflowSource {
|
|
|
4219
4663
|
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
4220
4664
|
}
|
|
4221
4665
|
|
|
4666
|
+
function sessionRecordCount(
|
|
4667
|
+
state: StateDatabase,
|
|
4668
|
+
table: "session_entries" | "session_events",
|
|
4669
|
+
segmentId: string,
|
|
4670
|
+
): number {
|
|
4671
|
+
const row = state.connection
|
|
4672
|
+
.prepare(`SELECT count(*) AS count FROM ${table} WHERE segment_id = ?`)
|
|
4673
|
+
.get(segmentId);
|
|
4674
|
+
return isCountRow(row) ? row.count : 0;
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4222
4677
|
function segmentIdFor(runId: string, attemptId?: string): string {
|
|
4223
4678
|
return `segment-${createHash("sha256")
|
|
4224
4679
|
.update(`${runId}\0${attemptId ?? ""}`)
|