@osolmaz/pi-workflows 0.15.3 → 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.
Files changed (118) hide show
  1. package/README.md +6 -6
  2. package/dist/client/activity.d.ts +2 -0
  3. package/dist/client/activity.js +6 -0
  4. package/dist/client/activity.js.map +1 -0
  5. package/dist/client/client.d.ts +101 -0
  6. package/dist/client/client.js +733 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +3 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/materialize.d.ts +7 -0
  12. package/dist/client/materialize.js +177 -0
  13. package/dist/client/materialize.js.map +1 -0
  14. package/dist/client/protocol.d.ts +60 -0
  15. package/dist/client/protocol.js +269 -0
  16. package/dist/client/protocol.js.map +1 -0
  17. package/dist/client/resolver.d.ts +23 -0
  18. package/dist/client/resolver.js +2 -0
  19. package/dist/client/resolver.js.map +1 -0
  20. package/dist/client/view.d.ts +118 -0
  21. package/dist/client/view.js +3 -0
  22. package/dist/client/view.js.map +1 -0
  23. package/dist/controllers/sqlite.d.ts +43 -0
  24. package/dist/controllers/sqlite.js +137 -2
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +278 -183
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-view.d.ts +7 -2
  30. package/dist/extension/session-view.js +60 -52
  31. package/dist/extension/session-view.js.map +1 -1
  32. package/dist/extension/widget.d.ts +2 -1
  33. package/dist/extension/widget.js +14 -7
  34. package/dist/extension/widget.js.map +1 -1
  35. package/dist/host/child-worker-supervisor.js +1 -1
  36. package/dist/host/child-worker-supervisor.js.map +1 -1
  37. package/dist/host/resolver-entry.d.ts +2 -23
  38. package/dist/host/resolver-entry.js +1 -1
  39. package/dist/host/resolver-entry.js.map +1 -1
  40. package/dist/host/runner.d.ts +11 -0
  41. package/dist/host/runner.js +473 -17
  42. package/dist/host/runner.js.map +1 -1
  43. package/dist/host/state.d.ts +6 -3
  44. package/dist/host/state.js +52 -26
  45. package/dist/host/state.js.map +1 -1
  46. package/dist/host/view.d.ts +73 -0
  47. package/dist/host/view.js +871 -0
  48. package/dist/host/view.js.map +1 -0
  49. package/dist/host/worker-protocol.js +1 -1
  50. package/dist/host/worker-protocol.js.map +1 -1
  51. package/dist/state/database.d.ts +1 -0
  52. package/dist/state/database.js +15 -0
  53. package/dist/state/database.js.map +1 -1
  54. package/dist/state/prune.d.ts +3 -1
  55. package/dist/state/prune.js +6 -8
  56. package/dist/state/prune.js.map +1 -1
  57. package/dist/state/schema.js +12 -1
  58. package/dist/state/schema.js.map +1 -1
  59. package/dist/viewer/backup.d.ts +2 -0
  60. package/dist/viewer/backup.js +28 -0
  61. package/dist/viewer/backup.js.map +1 -0
  62. package/dist/viewer/cli.d.ts +4 -0
  63. package/dist/viewer/cli.js +150 -170
  64. package/dist/viewer/cli.js.map +1 -1
  65. package/dist/viewer/tui.d.ts +5 -7
  66. package/dist/viewer/tui.js +188 -108
  67. package/dist/viewer/tui.js.map +1 -1
  68. package/dist/workflows/store.d.ts +62 -1
  69. package/dist/workflows/store.js +350 -44
  70. package/dist/workflows/store.js.map +1 -1
  71. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  72. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  73. package/docs/SQLITE_STATE.md +13 -11
  74. package/docs/WORKFLOW_HOST.md +74 -61
  75. package/docs/development.md +2 -1
  76. package/docs/live-replay-protocol.md +70 -132
  77. package/docs/tui-viewer.md +10 -14
  78. package/docs/workflows.md +44 -1
  79. package/herdr-plugin.toml +1 -1
  80. package/package.json +9 -3
  81. package/protocol/client.v1.schema.json +137 -0
  82. package/protocol/fixtures/client-v1.json +23 -0
  83. package/src/client/activity.ts +6 -0
  84. package/src/client/client.ts +935 -0
  85. package/src/client/index.ts +24 -0
  86. package/src/client/materialize.ts +228 -0
  87. package/src/client/protocol.ts +327 -0
  88. package/src/client/resolver.ts +26 -0
  89. package/src/client/view.ts +138 -0
  90. package/src/controllers/sqlite.ts +213 -2
  91. package/src/extension/index.ts +349 -208
  92. package/src/extension/session-view.ts +73 -50
  93. package/src/extension/widget.ts +16 -8
  94. package/src/host/child-worker-supervisor.ts +1 -1
  95. package/src/host/resolver-entry.ts +11 -26
  96. package/src/host/runner.ts +648 -48
  97. package/src/host/state.ts +71 -43
  98. package/src/host/view.ts +1084 -0
  99. package/src/host/worker-protocol.ts +1 -1
  100. package/src/state/database.ts +13 -0
  101. package/src/state/prune.ts +11 -11
  102. package/src/state/schema.ts +12 -1
  103. package/src/viewer/backup.ts +29 -0
  104. package/src/viewer/cli.ts +171 -185
  105. package/src/viewer/tui.ts +196 -124
  106. package/src/workflows/store.ts +500 -45
  107. package/dist/host/client.d.ts +0 -48
  108. package/dist/host/client.js +0 -216
  109. package/dist/host/client.js.map +0 -1
  110. package/dist/host/protocol.d.ts +0 -38
  111. package/dist/host/protocol.js +0 -156
  112. package/dist/host/protocol.js.map +0 -1
  113. package/dist/viewer/watch.d.ts +0 -6
  114. package/dist/viewer/watch.js +0 -46
  115. package/dist/viewer/watch.js.map +0 -1
  116. package/src/host/client.ts +0 -293
  117. package/src/host/protocol.ts +0 -196
  118. package/src/viewer/watch.ts +0 -51
@@ -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 steps) {
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
- ...this.readUpdates(row.runId),
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" && steps.at(-1) !== undefined
2257
- ? { waitingOn: steps.at(-1)?.nodeId }
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(`SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
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(run.resourceId);
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(segment.segmentId)
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(segment.segmentId)
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
- if (entries.length !== segment.entryCount || events.length !== segment.eventCount) {
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 ?? ""}`)