@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/dist/workflows/store.js
CHANGED
|
@@ -16,6 +16,35 @@ export const SESSION_BINDING_SCHEMA = "pi-workflows.session-binding.v1";
|
|
|
16
16
|
export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1";
|
|
17
17
|
export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1";
|
|
18
18
|
export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
|
|
19
|
+
const STEP_ROW_SELECT = `
|
|
20
|
+
SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
|
|
21
|
+
a.node_type AS nodeType, a.status,
|
|
22
|
+
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
23
|
+
s.output_override_hash AS outputOverrideHash,
|
|
24
|
+
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
25
|
+
prompt_entry.entry_hash AS promptEntryHash,
|
|
26
|
+
response_entry.entry_hash AS responseEntryHash,
|
|
27
|
+
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
28
|
+
a.settings_scope_id AS settingsScopeId,
|
|
29
|
+
a.settings_change_number AS settingsChangeNumber,
|
|
30
|
+
a.settings_hash AS settingsHash,
|
|
31
|
+
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
32
|
+
FROM run_steps s
|
|
33
|
+
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
34
|
+
LEFT JOIN attempt_entries prompt_link
|
|
35
|
+
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
36
|
+
LEFT JOIN session_entries prompt_entry
|
|
37
|
+
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
38
|
+
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
39
|
+
LEFT JOIN attempt_entries response_link
|
|
40
|
+
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
41
|
+
LEFT JOIN session_entries response_entry
|
|
42
|
+
ON response_entry.segment_id = response_link.segment_id
|
|
43
|
+
AND response_entry.entry_id = response_link.entry_id
|
|
44
|
+
LEFT JOIN attempt_entries first_link
|
|
45
|
+
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
46
|
+
LEFT JOIN attempt_entries last_link
|
|
47
|
+
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'`;
|
|
19
48
|
const FOLLOW_UP_ROW_SELECT = `
|
|
20
49
|
SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
|
|
21
50
|
f.queue_resource_id AS queueResourceId, f.run_id AS runId,
|
|
@@ -435,6 +464,22 @@ export class WorkflowRunStore {
|
|
|
435
464
|
.filter(isSettingsScopeRow)
|
|
436
465
|
.map((row) => this.settingsScopeRecord(row));
|
|
437
466
|
}
|
|
467
|
+
readSettingsScopeRange(runId, range) {
|
|
468
|
+
const originRunId = this.logicalOriginRunId(runId);
|
|
469
|
+
return this.state.connection
|
|
470
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
471
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
472
|
+
s.mount_path AS mountPath, s.invocation,
|
|
473
|
+
s.current_hash AS currentHash, r.revision,
|
|
474
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
475
|
+
FROM workflow_settings s
|
|
476
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
477
|
+
WHERE s.origin_run_id = ? OR s.active_run_id = ?
|
|
478
|
+
ORDER BY s.mount_path, s.invocation LIMIT ? OFFSET ?`)
|
|
479
|
+
.all(originRunId, runId, range.limit, range.start)
|
|
480
|
+
.filter(isSettingsScopeRow)
|
|
481
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
482
|
+
}
|
|
438
483
|
findSettingsScope(runId, mountPath, invocation) {
|
|
439
484
|
const row = this.state.connection
|
|
440
485
|
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
@@ -666,6 +711,31 @@ export class WorkflowRunStore {
|
|
|
666
711
|
followUps,
|
|
667
712
|
};
|
|
668
713
|
}
|
|
714
|
+
readFollowUpQueueRange(runId, range) {
|
|
715
|
+
const queue = this.followUpQueueRow(runId);
|
|
716
|
+
if (queue === undefined)
|
|
717
|
+
return null;
|
|
718
|
+
const followUps = this.state.connection
|
|
719
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number LIMIT ? OFFSET ?`)
|
|
720
|
+
.all(runId, range.limit, range.start)
|
|
721
|
+
.filter(isFollowUpRow)
|
|
722
|
+
.map((row) => this.followUpRecord(row));
|
|
723
|
+
return {
|
|
724
|
+
runId,
|
|
725
|
+
...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
|
|
726
|
+
presentationState: assertPresentationState(queue.presentationState),
|
|
727
|
+
...(queue.presentationEntryId !== null
|
|
728
|
+
? { presentationEntryId: queue.presentationEntryId }
|
|
729
|
+
: {}),
|
|
730
|
+
...(queue.presentationAssistantEntryId !== null
|
|
731
|
+
? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
|
|
732
|
+
: {}),
|
|
733
|
+
...(queue.presentationReasonHash !== null
|
|
734
|
+
? { presentationReason: this.readText(queue.presentationReasonHash) }
|
|
735
|
+
: {}),
|
|
736
|
+
followUps,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
669
739
|
listPendingPresentations(targetSessionId) {
|
|
670
740
|
return this.state.connection
|
|
671
741
|
.prepare(`SELECT run_id AS runId FROM workflow_follow_up_queues
|
|
@@ -1368,6 +1438,177 @@ export class WorkflowRunStore {
|
|
|
1368
1438
|
followUpQueue: this.readFollowUpQueue(runId) ?? null,
|
|
1369
1439
|
};
|
|
1370
1440
|
}
|
|
1441
|
+
readRunViewCounts(runId) {
|
|
1442
|
+
const row = this.readRunRow(runId);
|
|
1443
|
+
if (row === undefined)
|
|
1444
|
+
return null;
|
|
1445
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
1446
|
+
const settingsOrigin = this.logicalOriginRunId(runId);
|
|
1447
|
+
const scalar = (sql, ...params) => {
|
|
1448
|
+
const count = this.state.connection.prepare(sql).get(...params);
|
|
1449
|
+
return isCountRow(count) ? count.count : 0;
|
|
1450
|
+
};
|
|
1451
|
+
return {
|
|
1452
|
+
steps: scalar("SELECT count(*) AS count FROM run_steps WHERE run_id = ?", runId),
|
|
1453
|
+
trace: scalar("SELECT count(*) AS count FROM events WHERE resource_id = ?", row.resourceId),
|
|
1454
|
+
sessionEntries: segment?.entryCount ?? 0,
|
|
1455
|
+
sessionEvents: segment?.eventCount ?? 0,
|
|
1456
|
+
settings: scalar(`SELECT count(*) AS count FROM workflow_settings
|
|
1457
|
+
WHERE origin_run_id = ? OR active_run_id = ?`, settingsOrigin, runId),
|
|
1458
|
+
followUps: scalar("SELECT count(*) AS count FROM workflow_follow_ups WHERE run_id = ?", runId),
|
|
1459
|
+
updates: scalar(`SELECT count(*) AS count FROM (
|
|
1460
|
+
SELECT 1 FROM workflow_updates u
|
|
1461
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
1462
|
+
WHERE a.run_id = ? GROUP BY u.update_type, u.update_key
|
|
1463
|
+
)`, runId),
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
readRunView(runId, options) {
|
|
1467
|
+
const row = this.readRunRow(runId);
|
|
1468
|
+
if (row === undefined)
|
|
1469
|
+
return null;
|
|
1470
|
+
const snapshot = this.readDefinition(row.definitionHash);
|
|
1471
|
+
const steps = this.readStepRange(runId, options.steps);
|
|
1472
|
+
const visibleUpdates = this.readCurrentUpdates(runId, options.updates);
|
|
1473
|
+
const state = this.buildRunState(row, snapshot, this.readLatestSteps(runId), steps, visibleUpdates);
|
|
1474
|
+
const segment = this.segmentRow(segmentIdFor(runId));
|
|
1475
|
+
const loadedSegment = segment === undefined
|
|
1476
|
+
? emptySegment()
|
|
1477
|
+
: this.loadSegment(segment, state, {
|
|
1478
|
+
entries: options.sessionEntries,
|
|
1479
|
+
events: options.sessionEvents,
|
|
1480
|
+
});
|
|
1481
|
+
return {
|
|
1482
|
+
runId,
|
|
1483
|
+
state,
|
|
1484
|
+
snapshot,
|
|
1485
|
+
traceEvents: this.traceEvents(row, options.trace),
|
|
1486
|
+
sessionBinding: loadedSegment.binding,
|
|
1487
|
+
sessionEntries: loadedSegment.entries,
|
|
1488
|
+
sessionEvents: loadedSegment.events,
|
|
1489
|
+
sessionCapture: loadedSegment.capture,
|
|
1490
|
+
sessionIntegrity: loadedSegment.integrity,
|
|
1491
|
+
settingsScopes: this.readSettingsScopeRange(runId, options.settings),
|
|
1492
|
+
followUpQueue: this.readFollowUpQueueRange(runId, options.followUps),
|
|
1493
|
+
graphSteps: this.readLatestSteps(runId, options.graphCursor),
|
|
1494
|
+
takenTransitions: this.readTakenTransitions(runId, options.graphCursor),
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
traceCursorForStep(runId, stepCursor, traceTotal) {
|
|
1498
|
+
const step = this.state.connection
|
|
1499
|
+
.prepare(`SELECT a.attempt_id AS attemptId, a.node_id AS nodeId
|
|
1500
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
1501
|
+
WHERE s.run_id = ? AND s.step_index = ?`)
|
|
1502
|
+
.get(runId, stepCursor);
|
|
1503
|
+
if (!isRecord(step) || typeof step.attemptId !== "string" || typeof step.nodeId !== "string") {
|
|
1504
|
+
return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
1505
|
+
}
|
|
1506
|
+
const run = this.readRunRow(runId);
|
|
1507
|
+
if (run === undefined)
|
|
1508
|
+
return 0;
|
|
1509
|
+
const exactAttempt = this.state.connection
|
|
1510
|
+
.prepare(`SELECT e.resource_revision AS revision FROM events e
|
|
1511
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
1512
|
+
WHERE e.resource_id = ?
|
|
1513
|
+
AND json_extract(CAST(b.content AS TEXT), '$.attemptId') = ?
|
|
1514
|
+
ORDER BY e.resource_revision DESC LIMIT 1`)
|
|
1515
|
+
.get(run.resourceId, step.attemptId);
|
|
1516
|
+
const matched = isRevisionRow(exactAttempt)
|
|
1517
|
+
? exactAttempt
|
|
1518
|
+
: this.state.connection
|
|
1519
|
+
.prepare(`SELECT e.resource_revision AS revision FROM events e
|
|
1520
|
+
JOIN blobs b ON b.blob_hash = e.payload_hash
|
|
1521
|
+
WHERE e.resource_id = ?
|
|
1522
|
+
AND json_extract(CAST(b.content AS TEXT), '$.nodeId') = ?
|
|
1523
|
+
ORDER BY e.resource_revision DESC LIMIT 1`)
|
|
1524
|
+
.get(run.resourceId, step.nodeId);
|
|
1525
|
+
if (!isRevisionRow(matched))
|
|
1526
|
+
return Math.min(stepCursor, Math.max(0, traceTotal - 1));
|
|
1527
|
+
const index = this.state.connection
|
|
1528
|
+
.prepare(`SELECT count(*) - 1 AS count FROM events
|
|
1529
|
+
WHERE resource_id = ? AND resource_revision <= ?`)
|
|
1530
|
+
.get(run.resourceId, matched.revision);
|
|
1531
|
+
return isCountRow(index) ? Math.max(0, index.count) : 0;
|
|
1532
|
+
}
|
|
1533
|
+
persistViewContent(runId, content, mediaType) {
|
|
1534
|
+
if (this.readRunRow(runId) === undefined)
|
|
1535
|
+
throw new Error(`Workflow run not found: ${runId}`);
|
|
1536
|
+
return this.state.transaction(() => {
|
|
1537
|
+
const hash = createHash("sha256").update(content).digest();
|
|
1538
|
+
this.state.connection
|
|
1539
|
+
.prepare(`INSERT INTO run_view_content(
|
|
1540
|
+
run_id, content_hash, media_type, byte_length, content, created_at
|
|
1541
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
1542
|
+
ON CONFLICT(run_id, content_hash, media_type) DO NOTHING`)
|
|
1543
|
+
.run(runId, hash, mediaType, content.byteLength, content, Date.now());
|
|
1544
|
+
const stored = this.state.connection
|
|
1545
|
+
.prepare(`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
1546
|
+
FROM run_view_content
|
|
1547
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`)
|
|
1548
|
+
.get(runId, hash, mediaType);
|
|
1549
|
+
if (!isRecord(stored) ||
|
|
1550
|
+
stored.mediaType !== mediaType ||
|
|
1551
|
+
stored.byteLength !== content.byteLength ||
|
|
1552
|
+
!Buffer.isBuffer(stored.content) ||
|
|
1553
|
+
!stored.content.equals(content)) {
|
|
1554
|
+
throw new Error("Run-scoped view content conflict");
|
|
1555
|
+
}
|
|
1556
|
+
return hash.toString("hex");
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
readContentBlob(runId, digest, mediaType) {
|
|
1560
|
+
if (!/^[0-9a-f]{64}$/u.test(digest))
|
|
1561
|
+
return undefined;
|
|
1562
|
+
const stored = this.state.connection
|
|
1563
|
+
.prepare(`SELECT media_type AS mediaType, byte_length AS byteLength, content
|
|
1564
|
+
FROM run_view_content
|
|
1565
|
+
WHERE run_id = ? AND content_hash = ? AND media_type = ?`)
|
|
1566
|
+
.get(runId, Buffer.from(digest, "hex"), mediaType);
|
|
1567
|
+
if (!isRecord(stored) ||
|
|
1568
|
+
stored.mediaType !== mediaType ||
|
|
1569
|
+
typeof stored.byteLength !== "number" ||
|
|
1570
|
+
!Buffer.isBuffer(stored.content) ||
|
|
1571
|
+
stored.content.byteLength !== stored.byteLength) {
|
|
1572
|
+
return undefined;
|
|
1573
|
+
}
|
|
1574
|
+
return { mediaType, content: stored.content };
|
|
1575
|
+
}
|
|
1576
|
+
readDisplayState(runId) {
|
|
1577
|
+
const row = this.readRunRow(runId);
|
|
1578
|
+
if (row === undefined)
|
|
1579
|
+
return null;
|
|
1580
|
+
return {
|
|
1581
|
+
status: row.status,
|
|
1582
|
+
paused: row.paused === 1,
|
|
1583
|
+
error: row.errorHash === null ? null : this.readText(row.errorHash),
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
readSessionReplayCheckpoint(runId, throughSequence) {
|
|
1587
|
+
if (!Number.isSafeInteger(throughSequence) || throughSequence < 0) {
|
|
1588
|
+
throw new Error("Session replay checkpoint sequence must be a non-negative integer");
|
|
1589
|
+
}
|
|
1590
|
+
if (throughSequence === 0)
|
|
1591
|
+
return null;
|
|
1592
|
+
const row = this.state.connection
|
|
1593
|
+
.prepare(`SELECT event_seq AS eventSeq, state_hash AS stateHash
|
|
1594
|
+
FROM viewer_session_checkpoints
|
|
1595
|
+
WHERE run_id = ? AND event_seq <= ? ORDER BY event_seq DESC LIMIT 1`)
|
|
1596
|
+
.get(runId, throughSequence);
|
|
1597
|
+
let checkpoint = reduceSessionEvents([], [], 0);
|
|
1598
|
+
if (isViewerSessionCheckpointRow(row)) {
|
|
1599
|
+
const saved = this.state.readJson(row.stateHash);
|
|
1600
|
+
if (!isTemporalSessionState(saved) || saved.throughSeq !== row.eventSeq) {
|
|
1601
|
+
throw new Error(`Viewer session checkpoint is invalid for ${runId}`);
|
|
1602
|
+
}
|
|
1603
|
+
checkpoint = saved;
|
|
1604
|
+
}
|
|
1605
|
+
if (checkpoint.throughSeq < throughSequence) {
|
|
1606
|
+
const events = this.sessionEventRecords(runId, checkpoint.throughSeq, throughSequence + 1);
|
|
1607
|
+
checkpoint = reduceSessionEventsFromCheckpoint([], events, throughSequence, checkpoint);
|
|
1608
|
+
checkpoint = boundedTemporalCheckpoint(checkpoint);
|
|
1609
|
+
}
|
|
1610
|
+
return parseJson(canonicalJson(checkpoint));
|
|
1611
|
+
}
|
|
1371
1612
|
listRuns(options = {}) {
|
|
1372
1613
|
const rows = this.state.connection
|
|
1373
1614
|
.prepare("SELECT run_id AS runId FROM runs ORDER BY created_at DESC, run_id DESC")
|
|
@@ -2192,13 +2433,16 @@ export class WorkflowRunStore {
|
|
|
2192
2433
|
}
|
|
2193
2434
|
readRunState(row, snapshot) {
|
|
2194
2435
|
const steps = this.readSteps(row.runId);
|
|
2436
|
+
return this.buildRunState(row, snapshot, steps, steps, this.readUpdates(row.runId).updates ?? []);
|
|
2437
|
+
}
|
|
2438
|
+
buildRunState(row, snapshot, projectionSteps, visibleSteps, visibleUpdates) {
|
|
2195
2439
|
const sources = this.readRunSources(row.runId, snapshot);
|
|
2196
2440
|
const carriedStepCount = this.carriedStepCount(row.runId);
|
|
2197
2441
|
const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
|
|
2198
2442
|
const humanDecision = this.readHumanDecisionReceipt(row.runId);
|
|
2199
2443
|
const outputs = {};
|
|
2200
2444
|
const results = {};
|
|
2201
|
-
for (const step of
|
|
2445
|
+
for (const step of projectionSteps) {
|
|
2202
2446
|
const result = resultForStep(step);
|
|
2203
2447
|
results[step.nodeId] = result;
|
|
2204
2448
|
if (step.outcome === "ok")
|
|
@@ -2235,8 +2479,8 @@ export class WorkflowRunStore {
|
|
|
2235
2479
|
input: this.state.readJson(row.inputHash),
|
|
2236
2480
|
outputs,
|
|
2237
2481
|
results,
|
|
2238
|
-
steps,
|
|
2239
|
-
...
|
|
2482
|
+
steps: visibleSteps,
|
|
2483
|
+
...(visibleUpdates.length === 0 ? {} : { updates: visibleUpdates }),
|
|
2240
2484
|
...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
|
|
2241
2485
|
...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
|
|
2242
2486
|
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
@@ -2253,8 +2497,8 @@ export class WorkflowRunStore {
|
|
|
2253
2497
|
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
2254
2498
|
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
2255
2499
|
...(row.paused === 0 ? {} : { paused: true }),
|
|
2256
|
-
...(row.status === "waiting" &&
|
|
2257
|
-
? { waitingOn:
|
|
2500
|
+
...(row.status === "waiting" && projectionSteps.at(-1) !== undefined
|
|
2501
|
+
? { waitingOn: projectionSteps.at(-1)?.nodeId }
|
|
2258
2502
|
: {}),
|
|
2259
2503
|
...(row.finalOutputHash === null
|
|
2260
2504
|
? {}
|
|
@@ -2264,39 +2508,50 @@ export class WorkflowRunStore {
|
|
|
2264
2508
|
}
|
|
2265
2509
|
readSteps(runId) {
|
|
2266
2510
|
const rows = this.state.connection
|
|
2267
|
-
.prepare(
|
|
2268
|
-
a.node_type AS nodeType, a.status,
|
|
2269
|
-
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
2270
|
-
s.output_override_hash AS outputOverrideHash,
|
|
2271
|
-
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
2272
|
-
prompt_entry.entry_hash AS promptEntryHash,
|
|
2273
|
-
response_entry.entry_hash AS responseEntryHash,
|
|
2274
|
-
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
2275
|
-
a.settings_scope_id AS settingsScopeId,
|
|
2276
|
-
a.settings_change_number AS settingsChangeNumber,
|
|
2277
|
-
a.settings_hash AS settingsHash,
|
|
2278
|
-
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
2279
|
-
FROM run_steps s
|
|
2280
|
-
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2281
|
-
LEFT JOIN attempt_entries prompt_link
|
|
2282
|
-
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
2283
|
-
LEFT JOIN session_entries prompt_entry
|
|
2284
|
-
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
2285
|
-
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
2286
|
-
LEFT JOIN attempt_entries response_link
|
|
2287
|
-
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
2288
|
-
LEFT JOIN session_entries response_entry
|
|
2289
|
-
ON response_entry.segment_id = response_link.segment_id
|
|
2290
|
-
AND response_entry.entry_id = response_link.entry_id
|
|
2291
|
-
LEFT JOIN attempt_entries first_link
|
|
2292
|
-
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
2293
|
-
LEFT JOIN attempt_entries last_link
|
|
2294
|
-
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'
|
|
2295
|
-
WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
2511
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
2296
2512
|
.all(runId)
|
|
2297
2513
|
.filter(isStepRow);
|
|
2514
|
+
return this.mapStepRows(runId, rows, 0);
|
|
2515
|
+
}
|
|
2516
|
+
readStepRange(runId, range) {
|
|
2517
|
+
const rows = this.state.connection
|
|
2518
|
+
.prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index LIMIT ? OFFSET ?`)
|
|
2519
|
+
.all(runId, range.limit, range.start)
|
|
2520
|
+
.filter(isStepRow);
|
|
2521
|
+
return this.mapStepRows(runId, rows, range.start);
|
|
2522
|
+
}
|
|
2523
|
+
readLatestSteps(runId, through) {
|
|
2524
|
+
const throughClause = through === undefined ? "" : " AND s2.step_index <= ?";
|
|
2525
|
+
const params = through === undefined ? [runId, runId] : [runId, runId, through];
|
|
2526
|
+
const rows = this.state.connection
|
|
2527
|
+
.prepare(`${STEP_ROW_SELECT}
|
|
2528
|
+
WHERE s.run_id = ? AND s.step_index IN (
|
|
2529
|
+
SELECT max(s2.step_index) FROM run_steps s2
|
|
2530
|
+
JOIN node_attempts a2 ON a2.attempt_id = s2.attempt_id
|
|
2531
|
+
WHERE s2.run_id = ?${throughClause} GROUP BY a2.node_id
|
|
2532
|
+
) ORDER BY s.step_index`)
|
|
2533
|
+
.all(...params)
|
|
2534
|
+
.filter(isStepRow);
|
|
2535
|
+
return this.mapStepRows(runId, rows);
|
|
2536
|
+
}
|
|
2537
|
+
readTakenTransitions(runId, through) {
|
|
2538
|
+
const rows = this.state.connection
|
|
2539
|
+
.prepare(`WITH ordered AS (
|
|
2540
|
+
SELECT s.step_index, a.node_id AS nodeId,
|
|
2541
|
+
lag(a.node_id) OVER (ORDER BY s.step_index) AS previousNodeId
|
|
2542
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2543
|
+
WHERE s.run_id = ? AND s.step_index <= ?
|
|
2544
|
+
)
|
|
2545
|
+
SELECT DISTINCT previousNodeId, nodeId FROM ordered
|
|
2546
|
+
WHERE previousNodeId IS NOT NULL ORDER BY previousNodeId, nodeId`)
|
|
2547
|
+
.all(runId, through);
|
|
2548
|
+
return rows.flatMap((row) => isRecord(row) && typeof row.previousNodeId === "string" && typeof row.nodeId === "string"
|
|
2549
|
+
? [`${row.previousNodeId}->${row.nodeId}`]
|
|
2550
|
+
: []);
|
|
2551
|
+
}
|
|
2552
|
+
mapStepRows(runId, rows, expectedStart) {
|
|
2298
2553
|
return rows.map((row, index) => {
|
|
2299
|
-
if (row.stepIndex !== index)
|
|
2554
|
+
if (expectedStart !== undefined && row.stepIndex !== expectedStart + index)
|
|
2300
2555
|
throw new Error(`Workflow run step sequence has a gap: ${runId}`);
|
|
2301
2556
|
const receipt = row.receiptHash === null ? {} : this.readJsonAs(row.receiptHash);
|
|
2302
2557
|
const prompt = row.promptEntryHash !== null
|
|
@@ -2441,6 +2696,38 @@ export class WorkflowRunStore {
|
|
|
2441
2696
|
}
|
|
2442
2697
|
return updates === undefined ? {} : { updates };
|
|
2443
2698
|
}
|
|
2699
|
+
readCurrentUpdates(runId, range) {
|
|
2700
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
2701
|
+
const params = range === undefined ? [runId] : [runId, range.limit, range.start];
|
|
2702
|
+
const rows = this.state.connection
|
|
2703
|
+
.prepare(`WITH ranked AS (
|
|
2704
|
+
SELECT u.update_id AS updateId, u.run_revision AS runRevision,
|
|
2705
|
+
a.node_id AS nodeId, u.attempt_id AS attemptId,
|
|
2706
|
+
u.update_type AS updateType, u.update_key AS updateKey,
|
|
2707
|
+
u.data_hash AS dataHash, u.recorded_at AS recordedAt,
|
|
2708
|
+
row_number() OVER (
|
|
2709
|
+
PARTITION BY u.update_type, u.update_key ORDER BY u.run_revision DESC
|
|
2710
|
+
) AS rank
|
|
2711
|
+
FROM workflow_updates u
|
|
2712
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
2713
|
+
WHERE a.run_id = ?
|
|
2714
|
+
)
|
|
2715
|
+
SELECT updateId, runRevision, nodeId, attemptId, updateType, updateKey, dataHash, recordedAt
|
|
2716
|
+
FROM ranked WHERE rank = 1 ORDER BY runRevision${rangeClause}`)
|
|
2717
|
+
.all(...params)
|
|
2718
|
+
.filter(isUpdateRow);
|
|
2719
|
+
return rows.map((row) => ({
|
|
2720
|
+
updateId: row.updateId,
|
|
2721
|
+
seq: row.runRevision,
|
|
2722
|
+
at: new Date(row.recordedAt).toISOString(),
|
|
2723
|
+
runId,
|
|
2724
|
+
nodeId: row.nodeId,
|
|
2725
|
+
attemptId: row.attemptId,
|
|
2726
|
+
type: row.updateType,
|
|
2727
|
+
key: row.updateKey,
|
|
2728
|
+
data: this.readJsonAs(row.dataHash),
|
|
2729
|
+
}));
|
|
2730
|
+
}
|
|
2444
2731
|
readJsonAs(hash) {
|
|
2445
2732
|
return this.state.readJson(hash);
|
|
2446
2733
|
}
|
|
@@ -2458,12 +2745,14 @@ export class WorkflowRunStore {
|
|
|
2458
2745
|
}
|
|
2459
2746
|
return value;
|
|
2460
2747
|
}
|
|
2461
|
-
traceEvents(run) {
|
|
2748
|
+
traceEvents(run, range) {
|
|
2749
|
+
const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
|
|
2750
|
+
const params = range === undefined ? [run.resourceId] : [run.resourceId, range.limit, range.start];
|
|
2462
2751
|
const rows = this.state.connection
|
|
2463
2752
|
.prepare(`SELECT resource_revision AS resourceRevision, event_type AS eventType,
|
|
2464
2753
|
payload_hash AS payloadHash, recorded_at AS recordedAt
|
|
2465
|
-
FROM events WHERE resource_id = ? ORDER BY resource_revision`)
|
|
2466
|
-
.all(
|
|
2754
|
+
FROM events WHERE resource_id = ? ORDER BY resource_revision${rangeClause}`)
|
|
2755
|
+
.all(...params);
|
|
2467
2756
|
const events = [];
|
|
2468
2757
|
for (const row of rows) {
|
|
2469
2758
|
/* istanbul ignore if -- exact schema and internal query shape */
|
|
@@ -2512,14 +2801,16 @@ export class WorkflowRunStore {
|
|
|
2512
2801
|
throw new Error(`Session capture segment is missing: ${segmentId}`);
|
|
2513
2802
|
return row;
|
|
2514
2803
|
}
|
|
2515
|
-
loadSegment(segment, runState) {
|
|
2804
|
+
loadSegment(segment, runState, ranges) {
|
|
2516
2805
|
const binding = segment.bindingHash === null
|
|
2517
2806
|
? null
|
|
2518
2807
|
: this.state.readJson(segment.bindingHash);
|
|
2519
2808
|
const entries = this.state.connection
|
|
2520
2809
|
.prepare(`SELECT entry_seq AS entrySeq, entry_hash AS entryHash, recorded_at AS recordedAt
|
|
2521
|
-
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq`)
|
|
2522
|
-
.all(
|
|
2810
|
+
FROM session_entries WHERE segment_id = ? ORDER BY entry_seq${ranges === undefined ? "" : " LIMIT ? OFFSET ?"}`)
|
|
2811
|
+
.all(...(ranges === undefined
|
|
2812
|
+
? [segment.segmentId]
|
|
2813
|
+
: [segment.segmentId, ranges.entries.limit, ranges.entries.start]))
|
|
2523
2814
|
.filter(isSessionEntryRow)
|
|
2524
2815
|
.map((row) => ({
|
|
2525
2816
|
seq: row.entrySeq,
|
|
@@ -2531,8 +2822,10 @@ export class WorkflowRunStore {
|
|
|
2531
2822
|
attempt_id AS attemptId, turn_id AS turnId, message_id AS messageId,
|
|
2532
2823
|
tool_call_id AS toolCallId, payload_hash AS payloadHash,
|
|
2533
2824
|
recorded_at AS recordedAt
|
|
2534
|
-
FROM session_events WHERE segment_id = ? ORDER BY event_seq`)
|
|
2535
|
-
.all(
|
|
2825
|
+
FROM session_events WHERE segment_id = ? ORDER BY event_seq${ranges === undefined ? "" : " LIMIT ? OFFSET ?"}`)
|
|
2826
|
+
.all(...(ranges === undefined
|
|
2827
|
+
? [segment.segmentId]
|
|
2828
|
+
: [segment.segmentId, ranges.events.limit, ranges.events.start]))
|
|
2536
2829
|
.filter(isSessionEventRow)
|
|
2537
2830
|
.map((row) => ({
|
|
2538
2831
|
seq: row.eventSeq,
|
|
@@ -2558,7 +2851,14 @@ export class WorkflowRunStore {
|
|
|
2558
2851
|
...(failure === undefined ? {} : { failure }),
|
|
2559
2852
|
};
|
|
2560
2853
|
const diagnostics = [];
|
|
2561
|
-
|
|
2854
|
+
const durableCounts = ranges === undefined
|
|
2855
|
+
? { entries: entries.length, events: events.length }
|
|
2856
|
+
: {
|
|
2857
|
+
entries: sessionRecordCount(this.state, "session_entries", segment.segmentId),
|
|
2858
|
+
events: sessionRecordCount(this.state, "session_events", segment.segmentId),
|
|
2859
|
+
};
|
|
2860
|
+
if (durableCounts.entries !== segment.entryCount ||
|
|
2861
|
+
durableCounts.events !== segment.eventCount) {
|
|
2562
2862
|
diagnostics.push("session capture counts do not match durable rows");
|
|
2563
2863
|
}
|
|
2564
2864
|
if (runState.status !== "running" && segment.status === "recording") {
|
|
@@ -2847,6 +3147,12 @@ function workflowSourceFromRow(row) {
|
|
|
2847
3147
|
? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
|
|
2848
3148
|
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
2849
3149
|
}
|
|
3150
|
+
function sessionRecordCount(state, table, segmentId) {
|
|
3151
|
+
const row = state.connection
|
|
3152
|
+
.prepare(`SELECT count(*) AS count FROM ${table} WHERE segment_id = ?`)
|
|
3153
|
+
.get(segmentId);
|
|
3154
|
+
return isCountRow(row) ? row.count : 0;
|
|
3155
|
+
}
|
|
2850
3156
|
function segmentIdFor(runId, attemptId) {
|
|
2851
3157
|
return `segment-${createHash("sha256")
|
|
2852
3158
|
.update(`${runId}\0${attemptId ?? ""}`)
|