@osolmaz/pi-workflows 0.15.3 → 0.16.1

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 (207) hide show
  1. package/README.md +24 -8
  2. package/dist/channels/adapter-entry.d.ts +3 -0
  3. package/dist/channels/adapter-entry.js +203 -0
  4. package/dist/channels/adapter-entry.js.map +1 -0
  5. package/dist/channels/config.d.ts +43 -0
  6. package/dist/channels/config.js +234 -0
  7. package/dist/channels/config.js.map +1 -0
  8. package/dist/channels/protocol.d.ts +94 -0
  9. package/dist/channels/protocol.js +219 -0
  10. package/dist/channels/protocol.js.map +1 -0
  11. package/dist/channels/telegram.d.ts +46 -0
  12. package/dist/channels/telegram.js +285 -0
  13. package/dist/channels/telegram.js.map +1 -0
  14. package/dist/client/activity.d.ts +2 -0
  15. package/dist/client/activity.js +6 -0
  16. package/dist/client/activity.js.map +1 -0
  17. package/dist/client/client.d.ts +102 -0
  18. package/dist/client/client.js +737 -0
  19. package/dist/client/client.js.map +1 -0
  20. package/dist/client/index.d.ts +3 -0
  21. package/dist/client/index.js +3 -0
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/client/materialize.d.ts +7 -0
  24. package/dist/client/materialize.js +177 -0
  25. package/dist/client/materialize.js.map +1 -0
  26. package/dist/client/protocol.d.ts +60 -0
  27. package/dist/client/protocol.js +275 -0
  28. package/dist/client/protocol.js.map +1 -0
  29. package/dist/client/resolver.d.ts +23 -0
  30. package/dist/client/resolver.js +2 -0
  31. package/dist/client/resolver.js.map +1 -0
  32. package/dist/client/view.d.ts +144 -0
  33. package/dist/client/view.js +3 -0
  34. package/dist/client/view.js.map +1 -0
  35. package/dist/controllers/index.d.ts +1 -1
  36. package/dist/controllers/index.js.map +1 -1
  37. package/dist/controllers/sqlite.d.ts +47 -157
  38. package/dist/controllers/sqlite.js +230 -406
  39. package/dist/controllers/sqlite.js.map +1 -1
  40. package/dist/extension/index.d.ts +23 -1
  41. package/dist/extension/index.js +734 -555
  42. package/dist/extension/index.js.map +1 -1
  43. package/dist/extension/recorder.d.ts +95 -0
  44. package/dist/extension/recorder.js +530 -0
  45. package/dist/extension/recorder.js.map +1 -0
  46. package/dist/extension/remote-recorder-store.d.ts +25 -0
  47. package/dist/extension/remote-recorder-store.js +81 -0
  48. package/dist/extension/remote-recorder-store.js.map +1 -0
  49. package/dist/extension/session-events.d.ts +134 -0
  50. package/dist/extension/session-events.js +60 -0
  51. package/dist/extension/session-events.js.map +1 -0
  52. package/dist/extension/session-view.d.ts +9 -2
  53. package/dist/extension/session-view.js +117 -51
  54. package/dist/extension/session-view.js.map +1 -1
  55. package/dist/extension/step-message.d.ts +6 -4
  56. package/dist/extension/step-message.js +12 -6
  57. package/dist/extension/step-message.js.map +1 -1
  58. package/dist/extension/widget.d.ts +3 -2
  59. package/dist/extension/widget.js +41 -24
  60. package/dist/extension/widget.js.map +1 -1
  61. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  62. package/dist/extension/workflow-message-coordinator.js +294 -0
  63. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  64. package/dist/host/channel-effects.d.ts +72 -0
  65. package/dist/host/channel-effects.js +271 -0
  66. package/dist/host/channel-effects.js.map +1 -0
  67. package/dist/host/channel-supervisor.d.ts +21 -0
  68. package/dist/host/channel-supervisor.js +54 -0
  69. package/dist/host/channel-supervisor.js.map +1 -0
  70. package/dist/host/child-worker-supervisor.js +1 -1
  71. package/dist/host/child-worker-supervisor.js.map +1 -1
  72. package/dist/host/resolver-entry.d.ts +2 -23
  73. package/dist/host/resolver-entry.js +1 -1
  74. package/dist/host/resolver-entry.js.map +1 -1
  75. package/dist/host/runner.d.ts +54 -8
  76. package/dist/host/runner.js +1997 -305
  77. package/dist/host/runner.js.map +1 -1
  78. package/dist/host/state.d.ts +25 -19
  79. package/dist/host/state.js +303 -89
  80. package/dist/host/state.js.map +1 -1
  81. package/dist/host/view.d.ts +83 -0
  82. package/dist/host/view.js +975 -0
  83. package/dist/host/view.js.map +1 -0
  84. package/dist/host/worker-protocol.js +1 -1
  85. package/dist/host/worker-protocol.js.map +1 -1
  86. package/dist/state/database.d.ts +1 -0
  87. package/dist/state/database.js +15 -0
  88. package/dist/state/database.js.map +1 -1
  89. package/dist/state/prune.d.ts +3 -1
  90. package/dist/state/prune.js +9 -33
  91. package/dist/state/prune.js.map +1 -1
  92. package/dist/state/schema.js +88 -98
  93. package/dist/state/schema.js.map +1 -1
  94. package/dist/state/workflow-messages.d.ts +96 -0
  95. package/dist/state/workflow-messages.js +335 -0
  96. package/dist/state/workflow-messages.js.map +1 -0
  97. package/dist/viewer/backup.d.ts +2 -0
  98. package/dist/viewer/backup.js +28 -0
  99. package/dist/viewer/backup.js.map +1 -0
  100. package/dist/viewer/cli.d.ts +4 -0
  101. package/dist/viewer/cli.js +150 -170
  102. package/dist/viewer/cli.js.map +1 -1
  103. package/dist/viewer/render.js +1 -1
  104. package/dist/viewer/render.js.map +1 -1
  105. package/dist/viewer/tui.d.ts +5 -7
  106. package/dist/viewer/tui.js +245 -108
  107. package/dist/viewer/tui.js.map +1 -1
  108. package/dist/workflows/human-decision.d.ts +2 -0
  109. package/dist/workflows/human-decision.js +8 -2
  110. package/dist/workflows/human-decision.js.map +1 -1
  111. package/dist/workflows/index.d.ts +1 -1
  112. package/dist/workflows/index.js.map +1 -1
  113. package/dist/workflows/settings.d.ts +1 -8
  114. package/dist/workflows/settings.js.map +1 -1
  115. package/dist/workflows/store.d.ts +67 -20
  116. package/dist/workflows/store.js +451 -378
  117. package/dist/workflows/store.js.map +1 -1
  118. package/dist/workflows/tool-input.d.ts +21 -0
  119. package/dist/workflows/tool-input.js +23 -1
  120. package/dist/workflows/tool-input.js.map +1 -1
  121. package/dist/workflows/types.d.ts +5 -5
  122. package/dist/workflows/workflow-message-content.d.ts +38 -0
  123. package/dist/workflows/workflow-message-content.js +157 -0
  124. package/dist/workflows/workflow-message-content.js.map +1 -0
  125. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  126. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  127. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  128. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  129. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  130. package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
  131. package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
  132. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  133. package/docs/DEFERRED_TURNS.md +66 -294
  134. package/docs/HUMAN_DECISIONS.md +29 -30
  135. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  136. package/docs/SQLITE_STATE.md +31 -25
  137. package/docs/WORKFLOW_HOST.md +155 -77
  138. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  139. package/docs/development.md +2 -1
  140. package/docs/live-replay-protocol.md +74 -132
  141. package/docs/tui-viewer.md +12 -14
  142. package/docs/workflows.md +78 -28
  143. package/herdr-plugin.toml +1 -1
  144. package/package.json +9 -3
  145. package/plugins/herdr/viewer.mjs +14 -5
  146. package/protocol/client.v1.schema.json +137 -0
  147. package/protocol/fixtures/client-v1.json +23 -0
  148. package/src/channels/adapter-entry.ts +220 -0
  149. package/src/channels/config.ts +296 -0
  150. package/src/channels/protocol.ts +333 -0
  151. package/src/channels/telegram.ts +335 -0
  152. package/src/client/activity.ts +6 -0
  153. package/src/client/client.ts +939 -0
  154. package/src/client/index.ts +25 -0
  155. package/src/client/materialize.ts +228 -0
  156. package/src/client/protocol.ts +333 -0
  157. package/src/client/resolver.ts +26 -0
  158. package/src/client/view.ts +163 -0
  159. package/src/controllers/index.ts +0 -1
  160. package/src/controllers/sqlite.ts +339 -808
  161. package/src/extension/index.ts +853 -665
  162. package/src/extension/recorder.ts +687 -0
  163. package/src/extension/remote-recorder-store.ts +126 -0
  164. package/src/extension/session-events.ts +119 -0
  165. package/src/extension/session-view.ts +157 -51
  166. package/src/extension/step-message.ts +19 -9
  167. package/src/extension/widget.ts +53 -25
  168. package/src/extension/workflow-message-coordinator.ts +334 -0
  169. package/src/host/channel-effects.ts +465 -0
  170. package/src/host/channel-supervisor.ts +73 -0
  171. package/src/host/child-worker-supervisor.ts +1 -1
  172. package/src/host/resolver-entry.ts +11 -26
  173. package/src/host/runner.ts +2639 -450
  174. package/src/host/state.ts +404 -137
  175. package/src/host/view.ts +1212 -0
  176. package/src/host/worker-protocol.ts +1 -1
  177. package/src/state/database.ts +13 -0
  178. package/src/state/prune.ts +14 -42
  179. package/src/state/schema.ts +88 -98
  180. package/src/state/workflow-messages.ts +548 -0
  181. package/src/viewer/backup.ts +29 -0
  182. package/src/viewer/cli.ts +171 -185
  183. package/src/viewer/render.ts +1 -5
  184. package/src/viewer/tui.ts +261 -124
  185. package/src/workflows/human-decision.ts +18 -2
  186. package/src/workflows/index.ts +0 -1
  187. package/src/workflows/settings.ts +1 -20
  188. package/src/workflows/store.ts +650 -511
  189. package/src/workflows/tool-input.ts +36 -1
  190. package/src/workflows/types.ts +5 -5
  191. package/src/workflows/workflow-message-content.ts +197 -0
  192. package/dist/extension/decision-channels.d.ts +0 -134
  193. package/dist/extension/decision-channels.js +0 -1307
  194. package/dist/extension/decision-channels.js.map +0 -1
  195. package/dist/host/client.d.ts +0 -48
  196. package/dist/host/client.js +0 -216
  197. package/dist/host/client.js.map +0 -1
  198. package/dist/host/protocol.d.ts +0 -38
  199. package/dist/host/protocol.js +0 -156
  200. package/dist/host/protocol.js.map +0 -1
  201. package/dist/viewer/watch.d.ts +0 -6
  202. package/dist/viewer/watch.js +0 -46
  203. package/dist/viewer/watch.js.map +0 -1
  204. package/src/extension/decision-channels.ts +0 -1826
  205. package/src/host/client.ts +0 -293
  206. package/src/host/protocol.ts +0 -196
  207. package/src/viewer/watch.ts +0 -51
@@ -3,6 +3,7 @@ import { StateDatabase, workflowStatePath } from "../state/database.js";
3
3
  import { canonicalJson, parseJson } from "../state/json.js";
4
4
  import { StateMutationStore, StaleResourceError, resourceIdFor, tokenHash, } from "../state/mutation.js";
5
5
  import { initializeViewerRun, recordViewerDeltas, viewerTailPatch, VIEWER_PAGE_SIZE, } from "../state/viewer.js";
6
+ import { WorkflowMessageStore, workflowMessageIdFor } from "../state/workflow-messages.js";
6
7
  import { compositionMetadata } from "./composition.js";
7
8
  import { ClaimLostError } from "./errors.js";
8
9
  import { HumanDecisionStore } from "./human-decision.js";
@@ -10,22 +11,49 @@ import { applyJsonPatch, validateJsonPatch } from "./json-patch.js";
10
11
  import { reduceSessionEvents, reduceSessionEventsFromCheckpoint, } from "./session-reducer.js";
11
12
  import { applyWorkflowSettingsPatch, workflowSettingsScopeId, } from "./settings.js";
12
13
  import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
14
+ import { followUpWorkflowMessageContent } from "./workflow-message-content.js";
13
15
  export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1";
14
16
  export const DEFINITION_SNAPSHOT_SCHEMA = "pi-workflows.definition-snapshot.v1";
15
17
  export const SESSION_BINDING_SCHEMA = "pi-workflows.session-binding.v1";
16
18
  export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1";
17
19
  export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1";
18
20
  export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
21
+ const STEP_ROW_SELECT = `
22
+ SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
23
+ a.node_type AS nodeType, a.status,
24
+ a.prompt_hash AS promptHash, a.output_hash AS outputHash,
25
+ s.output_override_hash AS outputOverrideHash,
26
+ a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
27
+ prompt_entry.entry_hash AS promptEntryHash,
28
+ response_entry.entry_hash AS responseEntryHash,
29
+ first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
30
+ a.settings_scope_id AS settingsScopeId,
31
+ a.settings_change_number AS settingsChangeNumber,
32
+ a.settings_hash AS settingsHash,
33
+ a.started_at AS startedAt, a.finished_at AS finishedAt
34
+ FROM run_steps s
35
+ JOIN node_attempts a ON a.attempt_id = s.attempt_id
36
+ LEFT JOIN attempt_entries prompt_link
37
+ ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
38
+ LEFT JOIN session_entries prompt_entry
39
+ ON prompt_entry.segment_id = prompt_link.segment_id
40
+ AND prompt_entry.entry_id = prompt_link.entry_id
41
+ LEFT JOIN attempt_entries response_link
42
+ ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
43
+ LEFT JOIN session_entries response_entry
44
+ ON response_entry.segment_id = response_link.segment_id
45
+ AND response_entry.entry_id = response_link.entry_id
46
+ LEFT JOIN attempt_entries first_link
47
+ ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
48
+ LEFT JOIN attempt_entries last_link
49
+ ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'`;
19
50
  const FOLLOW_UP_ROW_SELECT = `
20
51
  SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
21
- f.queue_resource_id AS queueResourceId, f.run_id AS runId,
22
- f.request_id AS requestId, f.order_number AS orderNumber,
52
+ f.run_id AS runId, f.request_id AS requestId, f.order_number AS orderNumber,
23
53
  f.target_session_id AS targetSessionId, f.actor_type AS actorType,
24
54
  f.actor_id AS actorId, f.source_type AS sourceType,
25
- f.prompt_hash AS promptHash, f.status,
26
- f.session_entry_id AS sessionEntryId, f.reason_hash AS reasonHash,
27
- f.created_at AS createdAt, f.updated_at AS updatedAt, f.sent_at AS sentAt,
28
- r.revision
55
+ f.prompt_hash AS promptHash, f.status, f.reason_hash AS reasonHash,
56
+ f.created_at AS createdAt, f.updated_at AS updatedAt, r.revision
29
57
  FROM workflow_follow_ups f
30
58
  JOIN resources r ON r.resource_id = f.resource_id`;
31
59
  export function workflowStateDatabasePath(homeDir) {
@@ -40,12 +68,15 @@ export class WorkflowRunStore {
40
68
  state;
41
69
  authorityProvider;
42
70
  snapshotLifecycle;
71
+ allowHostSessionRecording;
43
72
  contexts = new Map();
44
73
  ownsState;
45
74
  mutations;
75
+ workflowMessages;
46
76
  constructor(databasePath = workflowStatePath(), options = {}) {
47
77
  this.authorityProvider = options.authorityProvider;
48
78
  this.snapshotLifecycle = options.snapshotLifecycle;
79
+ this.allowHostSessionRecording = options.allowHostSessionRecording === true;
49
80
  this.ownsState = options.state === undefined;
50
81
  this.state =
51
82
  options.state ??
@@ -56,6 +87,7 @@ export class WorkflowRunStore {
56
87
  });
57
88
  this.databasePath = this.state.filePath;
58
89
  this.mutations = new StateMutationStore(this.state);
90
+ this.workflowMessages = new WorkflowMessageStore(this.state);
59
91
  }
60
92
  close() {
61
93
  if (this.ownsState)
@@ -304,6 +336,15 @@ export class WorkflowRunStore {
304
336
  return;
305
337
  }
306
338
  const definitionHash = this.state.putJson(snapshot, now);
339
+ const parentLineage = state.parentRunId === undefined
340
+ ? undefined
341
+ : this.state.connection
342
+ .prepare(`SELECT root_run_id AS rootRunId, restart_number AS restartNumber
343
+ FROM runs WHERE run_id = ?`)
344
+ .get(state.parentRunId);
345
+ if (state.parentRunId !== undefined && !isRunLineageRow(parentLineage)) {
346
+ throw new Error(`Workflow parent run is missing: ${state.parentRunId}`);
347
+ }
307
348
  this.state.connection
308
349
  .prepare(`INSERT INTO workflow_definitions(
309
350
  definition_digest, workflow_name, definition_hash, created_at
@@ -326,12 +367,13 @@ export class WorkflowRunStore {
326
367
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
327
368
  this.state.connection
328
369
  .prepare(`INSERT INTO runs(
329
- run_id, resource_id, project_id, parent_run_id, definition_digest,
370
+ run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
371
+ restart_number, parent_terminal_fingerprint, definition_digest,
330
372
  workflow_ref, launch_options_hash, title, status, paused,
331
373
  status_detail, input_hash, final_output_hash, error_hash,
332
374
  created_at, updated_at, finished_at
333
- ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
334
- .run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName, launchOptionsHash, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, finalOutputHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
375
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
376
+ .run(state.runId, resourceId, state.parentRunId ?? null, isRunLineageRow(parentLineage) ? parentLineage.rootRunId : state.runId, state.parentRunId === undefined ? null : "continuation", isRunLineageRow(parentLineage) ? parentLineage.restartNumber : 0, definitionDigest, state.workflowName, launchOptionsHash, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, finalOutputHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
335
377
  initializeViewerRun(this.state, state.runId, now);
336
378
  this.insertRunSources(state.runId, source, state.workflowSources ?? []);
337
379
  this.syncContinuationIdentity(state);
@@ -435,6 +477,22 @@ export class WorkflowRunStore {
435
477
  .filter(isSettingsScopeRow)
436
478
  .map((row) => this.settingsScopeRecord(row));
437
479
  }
480
+ readSettingsScopeRange(runId, range) {
481
+ const originRunId = this.logicalOriginRunId(runId);
482
+ return this.state.connection
483
+ .prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
484
+ s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
485
+ s.mount_path AS mountPath, s.invocation,
486
+ s.current_hash AS currentHash, r.revision,
487
+ s.created_at AS createdAt, s.updated_at AS updatedAt
488
+ FROM workflow_settings s
489
+ JOIN resources r ON r.resource_id = s.resource_id
490
+ WHERE s.origin_run_id = ? OR s.active_run_id = ?
491
+ ORDER BY s.mount_path, s.invocation LIMIT ? OFFSET ?`)
492
+ .all(originRunId, runId, range.limit, range.start)
493
+ .filter(isSettingsScopeRow)
494
+ .map((row) => this.settingsScopeRecord(row));
495
+ }
438
496
  findSettingsScope(runId, mountPath, invocation) {
439
497
  const row = this.state.connection
440
498
  .prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
@@ -540,59 +598,49 @@ export class WorkflowRunStore {
540
598
  if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
541
599
  throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
542
600
  }
543
- for (let attempt = 0; attempt < 8; attempt += 1) {
601
+ return this.state.transaction(() => {
544
602
  const prior = this.followUpRowByRequest(request.runId, request.requestId);
545
603
  if (prior !== undefined) {
546
604
  this.assertSameFollowUp(prior, request);
547
605
  return { followUp: this.followUpRecord(prior), adopted: true };
548
606
  }
549
607
  this.requireRunAcceptsSettings(request.runId, true);
550
- const queue = this.requireFollowUpQueueRow(request.runId);
551
- try {
552
- const result = this.mutations.mutate({
553
- resourceId: queue.resourceId,
554
- operation: "follow-up.queue",
555
- actor: request.actor,
556
- expectedRevision: queue.revision,
557
- }, "follow-up.queued", ({ database, now }) => {
558
- this.requireRunAcceptsSettings(request.runId, true);
559
- const currentQueue = this.requireFollowUpQueueRow(request.runId);
560
- if (currentQueue.originSessionId !== null &&
561
- currentQueue.originSessionId !== request.targetSessionId) {
562
- throw new Error("Workflow follow-ups must target the run's origin Pi session");
563
- }
564
- if (currentQueue.originSessionId === null) {
565
- database.connection
566
- .prepare(`UPDATE workflow_follow_up_queues
567
- SET origin_session_id = ?, updated_at = ? WHERE run_id = ?`)
568
- .run(request.targetSessionId, now, request.runId);
569
- }
570
- const order = this.nextFollowUpOrder(request.runId);
571
- const followUpId = followUpIdFor(request.runId, request.requestId);
572
- const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
573
- const promptHash = database.putText(request.prompt, now);
574
- database.connection
575
- .prepare(`INSERT INTO workflow_follow_ups(
576
- follow_up_id, resource_id, queue_resource_id, run_id,
577
- request_id, order_number, target_session_id,
578
- actor_type, actor_id, source_type, prompt_hash, status,
579
- created_at, updated_at
580
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
581
- .run(followUpId, resourceId, queue.resourceId, request.runId, request.requestId, order, request.targetSessionId, request.actor.type, request.actor.id ?? null, request.source, promptHash, now, now);
582
- recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
583
- return followUpId;
584
- }, { payload: { runId: request.runId, requestId: request.requestId } });
585
- return {
586
- followUp: this.followUpRecord(this.requireFollowUpRow(result.value)),
587
- adopted: false,
588
- };
608
+ if (this.originSessionId(request.runId) !== request.targetSessionId) {
609
+ throw new Error("Workflow follow-ups must target the run's origin Pi session");
589
610
  }
590
- catch (error) {
591
- if (!(error instanceof StaleResourceError))
592
- throw error;
593
- }
594
- }
595
- throw new StaleResourceError("Workflow follow-up queue kept changing; retry the request");
611
+ const now = Date.now();
612
+ const order = this.nextFollowUpOrder(request.runId);
613
+ const followUpId = followUpIdFor(request.runId, request.requestId);
614
+ const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
615
+ const promptHash = this.state.putText(request.prompt, now);
616
+ this.state.connection
617
+ .prepare(`INSERT INTO workflow_follow_ups(
618
+ follow_up_id, resource_id, run_id, request_id, order_number,
619
+ target_session_id, actor_type, actor_id, source_type, prompt_hash,
620
+ status, created_at, updated_at
621
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
622
+ .run(followUpId, resourceId, request.runId, request.requestId, order, request.targetSessionId, request.actor.type, request.actor.id ?? null, request.source, promptHash, now, now);
623
+ const workflowMessageId = workflowMessageIdFor("followUp", followUpId, "initial");
624
+ this.workflowMessages.create({
625
+ workflowMessageId,
626
+ runId: request.runId,
627
+ targetSessionId: request.targetSessionId,
628
+ kind: "followUp",
629
+ sourceId: followUpId,
630
+ idempotencyKey: "initial",
631
+ content: followUpWorkflowMessageContent({
632
+ workflowMessageId,
633
+ followUpId,
634
+ runId: request.runId,
635
+ prompt: request.prompt,
636
+ }),
637
+ now,
638
+ });
639
+ this.bumpResource(resourceId, 0, now);
640
+ insertGenericEvent(this.state, resourceId, 1, "follow-up.queued", request.actor.type, request.actor.id ?? null, this.state.putJson({ runId: request.runId, requestId: request.requestId }, now), now);
641
+ recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
642
+ return { followUp: this.followUpRecord(this.requireFollowUpRow(followUpId)), adopted: false };
643
+ });
596
644
  }
597
645
  removeFollowUp(request) {
598
646
  assertSourceType(request.source);
@@ -602,12 +650,13 @@ export class WorkflowRunStore {
602
650
  }
603
651
  if (row.status === "removed")
604
652
  return this.followUpRecord(row);
605
- if (row.status === "sent" || row.status === "cancelled") {
606
- throw new Error(`Workflow follow-up cannot be removed after it is ${row.status}`);
653
+ if (row.status === "cancelled") {
654
+ throw new Error("A cancelled workflow follow-up cannot be removed");
607
655
  }
608
656
  this.assertFollowUpRemovalAuthority(row, request.actor, request.source);
609
- if (this.hasLiveLease(row.resourceId)) {
610
- throw new Error("Workflow follow-up is being delivered and cannot be removed");
657
+ const message = this.workflowMessages.latestForSource("followUp", request.followUpId);
658
+ if (message?.status === "sent") {
659
+ throw new Error("A sent workflow follow-up cannot be removed");
611
660
  }
612
661
  const reason = "Removed before delivery";
613
662
  this.mutations.mutate({
@@ -620,21 +669,20 @@ export class WorkflowRunStore {
620
669
  const update = database.connection
621
670
  .prepare(`UPDATE workflow_follow_ups
622
671
  SET status = 'removed', reason_hash = ?, updated_at = ?
623
- WHERE follow_up_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`)
672
+ WHERE follow_up_id = ? AND status = 'queued'`)
624
673
  .run(reasonHash, now, request.followUpId);
625
674
  if (update.changes !== 1) {
626
675
  throw new StaleResourceError("Workflow follow-up changed before removal");
627
676
  }
677
+ this.workflowMessages.cancelPendingForSource(request.followUpId, "followUp", now);
628
678
  recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
629
679
  }, { payload: { followUpId: request.followUpId, source: request.source } });
630
680
  return this.followUpRecord(this.requireFollowUpRow(request.followUpId));
631
681
  }
632
682
  originSessionId(runId) {
633
683
  const row = this.state.connection
634
- .prepare(`SELECT COALESCE(q.origin_session_id, b.origin_session_id) AS originSessionId
635
- FROM runs r
636
- LEFT JOIN workflow_follow_up_queues q ON q.run_id = r.run_id
637
- LEFT JOIN run_bindings b ON b.run_id = r.run_id
684
+ .prepare(`SELECT b.origin_session_id AS originSessionId
685
+ FROM runs r LEFT JOIN run_bindings b ON b.run_id = r.run_id
638
686
  WHERE r.run_id = ?`)
639
687
  .get(runId);
640
688
  return isRecord(row) && typeof row.originSessionId === "string"
@@ -642,151 +690,26 @@ export class WorkflowRunStore {
642
690
  : undefined;
643
691
  }
644
692
  readFollowUpQueue(runId) {
645
- const queue = this.followUpQueueRow(runId);
646
- if (queue === undefined)
693
+ const originSessionId = this.originSessionId(runId);
694
+ if (originSessionId === undefined)
647
695
  return undefined;
648
696
  const followUps = this.state.connection
649
697
  .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
650
698
  .all(runId)
651
699
  .filter(isFollowUpRow)
652
700
  .map((row) => this.followUpRecord(row));
653
- return {
654
- runId,
655
- ...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
656
- presentationState: assertPresentationState(queue.presentationState),
657
- ...(queue.presentationEntryId !== null
658
- ? { presentationEntryId: queue.presentationEntryId }
659
- : {}),
660
- ...(queue.presentationAssistantEntryId !== null
661
- ? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
662
- : {}),
663
- ...(queue.presentationReasonHash !== null
664
- ? { presentationReason: this.readText(queue.presentationReasonHash) }
665
- : {}),
666
- followUps,
667
- };
668
- }
669
- listPendingPresentations(targetSessionId) {
670
- return this.state.connection
671
- .prepare(`SELECT run_id AS runId FROM workflow_follow_up_queues
672
- WHERE origin_session_id = ? AND presentation_state = 'pending'
673
- ORDER BY created_at`)
674
- .all(targetSessionId)
675
- .flatMap((row) => (isRecord(row) && typeof row.runId === "string" ? [row.runId] : []));
676
- }
677
- settleFollowUpPresentation(options) {
678
- const queue = this.requireFollowUpQueueRow(options.runId);
679
- if (queue.presentationState === options.state) {
680
- return this.readFollowUpQueue(options.runId);
681
- }
682
- if (queue.presentationState !== "pending") {
683
- throw new Error(`Workflow presentation is ${queue.presentationState}, not pending for ${options.runId}`);
684
- }
685
- if (options.state === "settled" &&
686
- (options.presentationEntryId === undefined || options.assistantEntryId === undefined)) {
687
- throw new Error("Settled workflow presentation requires both session entry IDs");
688
- }
689
- if (options.state === "unavailable" && !options.reason?.trim()) {
690
- throw new Error("Unavailable workflow presentation requires a reason");
691
- }
692
- try {
693
- this.mutations.mutate({
694
- resourceId: queue.resourceId,
695
- operation: "follow-up.presentation",
696
- actor: {
697
- type: "session",
698
- ...(queue.originSessionId !== null ? { id: queue.originSessionId } : {}),
699
- },
700
- expectedRevision: queue.revision,
701
- }, `follow-up.presentation-${options.state}`, ({ database, now }) => {
702
- const reasonHash = options.state === "unavailable"
703
- ? database.putText(options.reason, now)
704
- : null;
705
- database.connection
706
- .prepare(`UPDATE workflow_follow_up_queues
707
- SET presentation_state = ?, presentation_entry_id = ?,
708
- presentation_assistant_entry_id = ?, presentation_reason_hash = ?,
709
- presentation_updated_at = ?, updated_at = ?
710
- WHERE run_id = ? AND presentation_state = 'pending'`)
711
- .run(options.state, options.presentationEntryId ?? null, options.assistantEntryId ?? null, reasonHash, now, now, options.runId);
712
- database.connection
713
- .prepare(`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
714
- WHERE run_id = ? AND status = 'pending_presentation'`)
715
- .run(now, options.runId);
716
- recordViewerDeltas(this.state, options.runId, this.viewerInspectorTargets(options.runId), now);
717
- }, { payload: { runId: options.runId, state: options.state } });
718
- }
719
- catch (error) {
720
- if (error instanceof StaleResourceError) {
721
- const current = this.requireFollowUpQueueRow(options.runId);
722
- if (current.presentationState === options.state) {
723
- return this.readFollowUpQueue(options.runId);
724
- }
725
- }
726
- throw error;
727
- }
728
- return this.readFollowUpQueue(options.runId);
729
- }
730
- claimNextFollowUp(targetSessionId, ownerId, leaseMs = 30_000) {
731
- const rows = this.state.connection
732
- .prepare(`${FOLLOW_UP_ROW_SELECT}
733
- WHERE f.target_session_id = ? AND f.status = 'ready'
734
- ORDER BY f.created_at, f.order_number LIMIT 16`)
735
- .all(targetSessionId)
736
- .filter(isFollowUpRow);
737
- for (const row of rows) {
738
- let claim;
739
- try {
740
- claim = this.mutations.claim({
741
- resourceId: row.resourceId,
742
- ownerType: "session",
743
- ownerId,
744
- expectedRevision: row.revision,
745
- leaseMs,
746
- });
747
- }
748
- catch (error) {
749
- if (error instanceof StaleResourceError)
750
- continue;
751
- throw error;
752
- }
753
- if (claim === undefined)
754
- continue;
755
- const current = this.requireFollowUpRow(row.followUpId);
756
- if (current.status !== "ready") {
757
- this.mutations.release(claim, claim.resourceRevision);
758
- continue;
759
- }
760
- return { followUp: this.followUpRecord(current), claim };
761
- }
762
- return undefined;
701
+ return { runId, originSessionId, followUps };
763
702
  }
764
- markFollowUpSent(followUpId, claim, sessionEntryId) {
765
- const row = this.requireFollowUpRow(followUpId);
766
- if (row.status === "sent")
767
- return this.followUpRecord(row);
768
- this.mutations.mutate({
769
- resourceId: row.resourceId,
770
- operation: "follow-up.sent",
771
- actor: { type: "session", id: claim.ownerId },
772
- expectedRevision: claim.resourceRevision,
773
- lease: claim,
774
- }, "follow-up.sent", ({ database, now }) => {
775
- const update = database.connection
776
- .prepare(`UPDATE workflow_follow_ups
777
- SET status = 'sent', session_entry_id = ?, sent_at = ?, updated_at = ?
778
- WHERE follow_up_id = ? AND status = 'ready'`)
779
- .run(sessionEntryId, now, now, followUpId);
780
- if (update.changes !== 1) {
781
- throw new StaleResourceError("Workflow follow-up changed before delivery completed");
782
- }
783
- recordViewerDeltas(this.state, row.runId, this.viewerInspectorTargets(row.runId), now);
784
- }, { payload: { followUpId, sessionEntryId } });
785
- return this.followUpRecord(this.requireFollowUpRow(followUpId));
786
- }
787
- releaseFollowUpClaim(claim) {
788
- const revision = this.requireResourceRevision(claim.resourceId);
789
- this.mutations.release(claim, revision);
703
+ readFollowUpQueueRange(runId, range) {
704
+ const originSessionId = this.originSessionId(runId);
705
+ if (originSessionId === undefined)
706
+ return null;
707
+ const followUps = this.state.connection
708
+ .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number LIMIT ? OFFSET ?`)
709
+ .all(runId, range.limit, range.start)
710
+ .filter(isFollowUpRow)
711
+ .map((row) => this.followUpRecord(row));
712
+ return { runId, originSessionId, followUps };
790
713
  }
791
714
  async prepareRunResume(runId) {
792
715
  const loaded = this.readRun(runId, { includeTrace: true });
@@ -798,7 +721,6 @@ export class WorkflowRunStore {
798
721
  this.verifySettingsProjections(runId);
799
722
  const revision = this.resourceRevision(runId);
800
723
  this.contexts.set(runId, { revision, lock: Promise.resolve() });
801
- this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
802
724
  this.state.transaction(() => {
803
725
  const row = this.requireRunRow(runId);
804
726
  const context = this.contextFor(runId);
@@ -981,7 +903,7 @@ export class WorkflowRunStore {
981
903
  this.state.transaction(() => {
982
904
  const run = this.requireRunRow(runId);
983
905
  const context = this.contextFor(runId);
984
- this.assertWriteAuthority(run, context.revision);
906
+ this.assertSessionWriteAuthority(runId);
985
907
  const now = Date.parse(binding.boundAt);
986
908
  const resourceId = resourceIdFor("session", segmentId);
987
909
  const bindingHash = this.state.putJson(binding, now);
@@ -1005,11 +927,6 @@ export class WorkflowRunStore {
1005
927
  VALUES (?, ?, 'interactive', ?)
1006
928
  ON CONFLICT(run_id) DO NOTHING`)
1007
929
  .run(runId, binding.piSessionId, now);
1008
- this.state.connection
1009
- .prepare(`UPDATE workflow_follow_up_queues
1010
- SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
1011
- WHERE run_id = ?`)
1012
- .run(binding.piSessionId, now, runId);
1013
930
  const revision = context.revision + 1;
1014
931
  const at = new Date(now).toISOString();
1015
932
  const traceEvent = {
@@ -1368,6 +1285,177 @@ export class WorkflowRunStore {
1368
1285
  followUpQueue: this.readFollowUpQueue(runId) ?? null,
1369
1286
  };
1370
1287
  }
1288
+ readRunViewCounts(runId) {
1289
+ const row = this.readRunRow(runId);
1290
+ if (row === undefined)
1291
+ return null;
1292
+ const segment = this.segmentRow(segmentIdFor(runId));
1293
+ const settingsOrigin = this.logicalOriginRunId(runId);
1294
+ const scalar = (sql, ...params) => {
1295
+ const count = this.state.connection.prepare(sql).get(...params);
1296
+ return isCountRow(count) ? count.count : 0;
1297
+ };
1298
+ return {
1299
+ steps: scalar("SELECT count(*) AS count FROM run_steps WHERE run_id = ?", runId),
1300
+ trace: scalar("SELECT count(*) AS count FROM events WHERE resource_id = ?", row.resourceId),
1301
+ sessionEntries: segment?.entryCount ?? 0,
1302
+ sessionEvents: segment?.eventCount ?? 0,
1303
+ settings: scalar(`SELECT count(*) AS count FROM workflow_settings
1304
+ WHERE origin_run_id = ? OR active_run_id = ?`, settingsOrigin, runId),
1305
+ followUps: scalar("SELECT count(*) AS count FROM workflow_follow_ups WHERE run_id = ?", runId),
1306
+ updates: scalar(`SELECT count(*) AS count FROM (
1307
+ SELECT 1 FROM workflow_updates u
1308
+ JOIN node_attempts a ON a.attempt_id = u.attempt_id
1309
+ WHERE a.run_id = ? GROUP BY u.update_type, u.update_key
1310
+ )`, runId),
1311
+ };
1312
+ }
1313
+ readRunView(runId, options) {
1314
+ const row = this.readRunRow(runId);
1315
+ if (row === undefined)
1316
+ return null;
1317
+ const snapshot = this.readDefinition(row.definitionHash);
1318
+ const steps = this.readStepRange(runId, options.steps);
1319
+ const visibleUpdates = this.readCurrentUpdates(runId, options.updates);
1320
+ const state = this.buildRunState(row, snapshot, this.readLatestSteps(runId), steps, visibleUpdates);
1321
+ const segment = this.segmentRow(segmentIdFor(runId));
1322
+ const loadedSegment = segment === undefined
1323
+ ? emptySegment()
1324
+ : this.loadSegment(segment, state, {
1325
+ entries: options.sessionEntries,
1326
+ events: options.sessionEvents,
1327
+ });
1328
+ return {
1329
+ runId,
1330
+ state,
1331
+ snapshot,
1332
+ traceEvents: this.traceEvents(row, options.trace),
1333
+ sessionBinding: loadedSegment.binding,
1334
+ sessionEntries: loadedSegment.entries,
1335
+ sessionEvents: loadedSegment.events,
1336
+ sessionCapture: loadedSegment.capture,
1337
+ sessionIntegrity: loadedSegment.integrity,
1338
+ settingsScopes: this.readSettingsScopeRange(runId, options.settings),
1339
+ followUpQueue: this.readFollowUpQueueRange(runId, options.followUps),
1340
+ graphSteps: this.readLatestSteps(runId, options.graphCursor),
1341
+ takenTransitions: this.readTakenTransitions(runId, options.graphCursor),
1342
+ };
1343
+ }
1344
+ traceCursorForStep(runId, stepCursor, traceTotal) {
1345
+ const step = this.state.connection
1346
+ .prepare(`SELECT a.attempt_id AS attemptId, a.node_id AS nodeId
1347
+ FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
1348
+ WHERE s.run_id = ? AND s.step_index = ?`)
1349
+ .get(runId, stepCursor);
1350
+ if (!isRecord(step) || typeof step.attemptId !== "string" || typeof step.nodeId !== "string") {
1351
+ return Math.min(stepCursor, Math.max(0, traceTotal - 1));
1352
+ }
1353
+ const run = this.readRunRow(runId);
1354
+ if (run === undefined)
1355
+ return 0;
1356
+ const exactAttempt = this.state.connection
1357
+ .prepare(`SELECT e.resource_revision AS revision FROM events e
1358
+ JOIN blobs b ON b.blob_hash = e.payload_hash
1359
+ WHERE e.resource_id = ?
1360
+ AND json_extract(CAST(b.content AS TEXT), '$.attemptId') = ?
1361
+ ORDER BY e.resource_revision DESC LIMIT 1`)
1362
+ .get(run.resourceId, step.attemptId);
1363
+ const matched = isRevisionRow(exactAttempt)
1364
+ ? exactAttempt
1365
+ : this.state.connection
1366
+ .prepare(`SELECT e.resource_revision AS revision FROM events e
1367
+ JOIN blobs b ON b.blob_hash = e.payload_hash
1368
+ WHERE e.resource_id = ?
1369
+ AND json_extract(CAST(b.content AS TEXT), '$.nodeId') = ?
1370
+ ORDER BY e.resource_revision DESC LIMIT 1`)
1371
+ .get(run.resourceId, step.nodeId);
1372
+ if (!isRevisionRow(matched))
1373
+ return Math.min(stepCursor, Math.max(0, traceTotal - 1));
1374
+ const index = this.state.connection
1375
+ .prepare(`SELECT count(*) - 1 AS count FROM events
1376
+ WHERE resource_id = ? AND resource_revision <= ?`)
1377
+ .get(run.resourceId, matched.revision);
1378
+ return isCountRow(index) ? Math.max(0, index.count) : 0;
1379
+ }
1380
+ persistViewContent(runId, content, mediaType) {
1381
+ if (this.readRunRow(runId) === undefined)
1382
+ throw new Error(`Workflow run not found: ${runId}`);
1383
+ return this.state.transaction(() => {
1384
+ const hash = createHash("sha256").update(content).digest();
1385
+ this.state.connection
1386
+ .prepare(`INSERT INTO run_view_content(
1387
+ run_id, content_hash, media_type, byte_length, content, created_at
1388
+ ) VALUES (?, ?, ?, ?, ?, ?)
1389
+ ON CONFLICT(run_id, content_hash, media_type) DO NOTHING`)
1390
+ .run(runId, hash, mediaType, content.byteLength, content, Date.now());
1391
+ const stored = this.state.connection
1392
+ .prepare(`SELECT media_type AS mediaType, byte_length AS byteLength, content
1393
+ FROM run_view_content
1394
+ WHERE run_id = ? AND content_hash = ? AND media_type = ?`)
1395
+ .get(runId, hash, mediaType);
1396
+ if (!isRecord(stored) ||
1397
+ stored.mediaType !== mediaType ||
1398
+ stored.byteLength !== content.byteLength ||
1399
+ !Buffer.isBuffer(stored.content) ||
1400
+ !stored.content.equals(content)) {
1401
+ throw new Error("Run-scoped view content conflict");
1402
+ }
1403
+ return hash.toString("hex");
1404
+ });
1405
+ }
1406
+ readContentBlob(runId, digest, mediaType) {
1407
+ if (!/^[0-9a-f]{64}$/u.test(digest))
1408
+ return undefined;
1409
+ const stored = this.state.connection
1410
+ .prepare(`SELECT media_type AS mediaType, byte_length AS byteLength, content
1411
+ FROM run_view_content
1412
+ WHERE run_id = ? AND content_hash = ? AND media_type = ?`)
1413
+ .get(runId, Buffer.from(digest, "hex"), mediaType);
1414
+ if (!isRecord(stored) ||
1415
+ stored.mediaType !== mediaType ||
1416
+ typeof stored.byteLength !== "number" ||
1417
+ !Buffer.isBuffer(stored.content) ||
1418
+ stored.content.byteLength !== stored.byteLength) {
1419
+ return undefined;
1420
+ }
1421
+ return { mediaType, content: stored.content };
1422
+ }
1423
+ readDisplayState(runId) {
1424
+ const row = this.readRunRow(runId);
1425
+ if (row === undefined)
1426
+ return null;
1427
+ return {
1428
+ status: row.status,
1429
+ paused: row.paused === 1,
1430
+ error: row.errorHash === null ? null : this.readText(row.errorHash),
1431
+ };
1432
+ }
1433
+ readSessionReplayCheckpoint(runId, throughSequence) {
1434
+ if (!Number.isSafeInteger(throughSequence) || throughSequence < 0) {
1435
+ throw new Error("Session replay checkpoint sequence must be a non-negative integer");
1436
+ }
1437
+ if (throughSequence === 0)
1438
+ return null;
1439
+ const row = this.state.connection
1440
+ .prepare(`SELECT event_seq AS eventSeq, state_hash AS stateHash
1441
+ FROM viewer_session_checkpoints
1442
+ WHERE run_id = ? AND event_seq <= ? ORDER BY event_seq DESC LIMIT 1`)
1443
+ .get(runId, throughSequence);
1444
+ let checkpoint = reduceSessionEvents([], [], 0);
1445
+ if (isViewerSessionCheckpointRow(row)) {
1446
+ const saved = this.state.readJson(row.stateHash);
1447
+ if (!isTemporalSessionState(saved) || saved.throughSeq !== row.eventSeq) {
1448
+ throw new Error(`Viewer session checkpoint is invalid for ${runId}`);
1449
+ }
1450
+ checkpoint = saved;
1451
+ }
1452
+ if (checkpoint.throughSeq < throughSequence) {
1453
+ const events = this.sessionEventRecords(runId, checkpoint.throughSeq, throughSequence + 1);
1454
+ checkpoint = reduceSessionEventsFromCheckpoint([], events, throughSequence, checkpoint);
1455
+ checkpoint = boundedTemporalCheckpoint(checkpoint);
1456
+ }
1457
+ return parseJson(canonicalJson(checkpoint));
1458
+ }
1371
1459
  listRuns(options = {}) {
1372
1460
  const rows = this.state.connection
1373
1461
  .prepare("SELECT run_id AS runId FROM runs ORDER BY created_at DESC, run_id DESC")
@@ -1418,6 +1506,8 @@ export class WorkflowRunStore {
1418
1506
  }
1419
1507
  assertSessionWriteAuthority(runId) {
1420
1508
  const run = this.requireRunRow(runId);
1509
+ if (this.allowHostSessionRecording)
1510
+ return;
1421
1511
  this.assertWriteAuthority(run, this.contextFor(runId).revision);
1422
1512
  }
1423
1513
  resourceRevision(runId) {
@@ -1567,10 +1657,8 @@ export class WorkflowRunStore {
1567
1657
  order: row.orderNumber,
1568
1658
  state: row.status,
1569
1659
  source: row.sourceType,
1570
- sessionEntryId: row.sessionEntryId,
1571
1660
  }));
1572
1661
  const followUpStart = Math.max(0, followUps.length - VIEWER_PAGE_SIZE);
1573
- const queue = this.requireFollowUpQueueRow(runId);
1574
1662
  return [
1575
1663
  {
1576
1664
  targetType: "inspector",
@@ -1592,10 +1680,7 @@ export class WorkflowRunStore {
1592
1680
  {
1593
1681
  op: "replace",
1594
1682
  path: "/followUpQueue",
1595
- value: parseJson(canonicalJson({
1596
- presentationState: queue.presentationState,
1597
- items: followUps.slice(followUpStart),
1598
- })),
1683
+ value: parseJson(canonicalJson({ items: followUps.slice(followUpStart) })),
1599
1684
  },
1600
1685
  { op: "replace", path: "/followUpStart", value: followUpStart },
1601
1686
  { op: "replace", path: "/followUpTotal", value: followUps.length },
@@ -1604,21 +1689,7 @@ export class WorkflowRunStore {
1604
1689
  ];
1605
1690
  }
1606
1691
  initializeRunSettingsAndFollowUps(state, initialSettings, now) {
1607
- const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
1608
1692
  if (state.parentRunId !== undefined) {
1609
- const parentQueue = this.followUpQueueRow(state.parentRunId);
1610
- if (parentQueue !== undefined) {
1611
- this.state.connection
1612
- .prepare(`UPDATE workflow_follow_up_queues
1613
- SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
1614
- WHERE run_id = ?`)
1615
- .run(parentQueue.originSessionId, now, state.runId);
1616
- this.state.connection
1617
- .prepare(`UPDATE workflow_follow_ups
1618
- SET run_id = ?, queue_resource_id = ?, updated_at = ?
1619
- WHERE run_id = ? AND status = 'queued'`)
1620
- .run(state.runId, childQueueResourceId, now, state.parentRunId);
1621
- }
1622
1693
  this.state.connection
1623
1694
  .prepare(`UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`)
1624
1695
  .run(state.runId, now, state.parentRunId);
@@ -1643,16 +1714,6 @@ export class WorkflowRunStore {
1643
1714
  .run(scopeId, resourceId, originRunId, state.runId, scope.mountPath, scope.invocation, settingsHash, settingsHash, now, now);
1644
1715
  }
1645
1716
  }
1646
- ensureFollowUpQueue(runId, now) {
1647
- const resourceId = this.mutations.ensureResource("follow_up_queue", runId, now);
1648
- this.state.connection
1649
- .prepare(`INSERT INTO workflow_follow_up_queues(
1650
- run_id, resource_id, created_at, updated_at
1651
- ) VALUES (?, ?, ?, ?)
1652
- ON CONFLICT(run_id) DO NOTHING`)
1653
- .run(runId, resourceId, now, now);
1654
- return resourceId;
1655
- }
1656
1717
  logicalOriginRunId(runId) {
1657
1718
  let current = runId;
1658
1719
  for (let depth = 0; depth < 100; depth += 1) {
@@ -1785,27 +1846,6 @@ export class WorkflowRunStore {
1785
1846
  throw new Error(`Workflow settings request ID was reused with different content`);
1786
1847
  }
1787
1848
  }
1788
- followUpQueueRow(runId) {
1789
- const row = this.state.connection
1790
- .prepare(`SELECT q.run_id AS runId, q.resource_id AS resourceId,
1791
- q.origin_session_id AS originSessionId,
1792
- q.presentation_state AS presentationState,
1793
- q.presentation_entry_id AS presentationEntryId,
1794
- q.presentation_assistant_entry_id AS presentationAssistantEntryId,
1795
- q.presentation_reason_hash AS presentationReasonHash,
1796
- r.revision
1797
- FROM workflow_follow_up_queues q
1798
- JOIN resources r ON r.resource_id = q.resource_id
1799
- WHERE q.run_id = ?`)
1800
- .get(runId);
1801
- return isFollowUpQueueRow(row) ? row : undefined;
1802
- }
1803
- requireFollowUpQueueRow(runId) {
1804
- const row = this.followUpQueueRow(runId);
1805
- if (row === undefined)
1806
- throw new Error(`Workflow follow-up queue not found: ${runId}`);
1807
- return row;
1808
- }
1809
1849
  followUpRowByRequest(runId, requestId) {
1810
1850
  const row = this.state.connection
1811
1851
  .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
@@ -1838,11 +1878,9 @@ export class WorkflowRunStore {
1838
1878
  source: row.sourceType,
1839
1879
  prompt: this.readText(row.promptHash),
1840
1880
  state: assertFollowUpState(row.status),
1841
- ...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
1842
1881
  ...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
1843
1882
  createdAt: new Date(row.createdAt).toISOString(),
1844
1883
  updatedAt: new Date(row.updatedAt).toISOString(),
1845
- ...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
1846
1884
  };
1847
1885
  }
1848
1886
  assertSameFollowUp(row, request) {
@@ -1866,12 +1904,6 @@ export class WorkflowRunStore {
1866
1904
  }
1867
1905
  throw new Error("Workflow follow-up can be removed only by its source or a verified human");
1868
1906
  }
1869
- hasLiveLease(resourceId) {
1870
- const row = this.state.connection
1871
- .prepare("SELECT expires_at AS expiresAt FROM leases WHERE resource_id = ?")
1872
- .get(resourceId);
1873
- return isLeaseExpiryRow(row) && row.expiresAt !== null && row.expiresAt > Date.now();
1874
- }
1875
1907
  nextFollowUpOrder(runId) {
1876
1908
  const row = this.state.connection
1877
1909
  .prepare(`SELECT COALESCE(MAX(order_number), 0) + 1 AS nextOrder
@@ -1896,37 +1928,37 @@ export class WorkflowRunStore {
1896
1928
  throw new StaleResourceError(`Settings route ${event.nodeId} used change ${changeNumber}, current change is ${scope.revision}`);
1897
1929
  }
1898
1930
  }
1899
- transitionFollowUpsForRunState(state, event, now) {
1900
- if (state.status === "running" || state.status === "waiting")
1901
- return;
1902
- const queue = this.followUpQueueRow(state.runId);
1903
- if (queue === undefined || queue.presentationState !== "none")
1931
+ transitionFollowUpsForRunState(state, _event, now) {
1932
+ if (state.status === "running" || state.status === "waiting" || state.status === "completed") {
1904
1933
  return;
1905
- let presentationState = "not-needed";
1906
- let followUpState = "cancelled";
1907
- let reasonHash = null;
1908
- if (state.status === "completed") {
1909
- const required = event.payload.presentationRequired === true;
1910
- presentationState = required ? "pending" : "not-needed";
1911
- followUpState = required ? "pending_presentation" : "ready";
1912
1934
  }
1913
- else {
1914
- reasonHash = this.state.putText(state.error ?? `Workflow ended with status ${state.status}`, now);
1935
+ const rows = this.state.connection
1936
+ .prepare(`${FOLLOW_UP_ROW_SELECT}
1937
+ WHERE f.run_id IN (
1938
+ WITH RECURSIVE ancestors(run_id, parent_run_id) AS (
1939
+ SELECT run_id, parent_run_id FROM runs WHERE run_id = ?
1940
+ UNION ALL
1941
+ SELECT parent.run_id, parent.parent_run_id
1942
+ FROM runs parent JOIN ancestors ON ancestors.parent_run_id = parent.run_id
1943
+ )
1944
+ SELECT run_id FROM ancestors
1945
+ ) AND f.status = 'queued'`)
1946
+ .all(state.runId)
1947
+ .filter(isFollowUpRow);
1948
+ for (const row of rows) {
1949
+ const reason = state.error ?? `Workflow ended with status ${state.status}`;
1950
+ const reasonHash = this.state.putText(reason, now);
1951
+ const update = this.state.connection
1952
+ .prepare(`UPDATE workflow_follow_ups
1953
+ SET status = 'cancelled', reason_hash = ?, updated_at = ?
1954
+ WHERE follow_up_id = ? AND status = 'queued'`)
1955
+ .run(reasonHash, now, row.followUpId);
1956
+ if (update.changes !== 1)
1957
+ continue;
1958
+ this.workflowMessages.cancelPendingForSource(row.followUpId, "followUp", now);
1959
+ this.bumpResource(row.resourceId, row.revision, now);
1960
+ insertGenericEvent(this.state, row.resourceId, row.revision + 1, "follow-up.cancelled", "system", null, this.state.putJson({ runId: state.runId, status: state.status, reason }, now), now);
1915
1961
  }
1916
- this.state.connection
1917
- .prepare(`UPDATE workflow_follow_up_queues
1918
- SET presentation_state = ?, presentation_updated_at = ?, updated_at = ?
1919
- WHERE run_id = ? AND presentation_state = 'none'`)
1920
- .run(presentationState, now, now, state.runId);
1921
- this.state.connection
1922
- .prepare(`UPDATE workflow_follow_ups
1923
- SET status = ?, reason_hash = ?, updated_at = ?
1924
- WHERE run_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`)
1925
- .run(followUpState, reasonHash, now, state.runId);
1926
- const revision = this.requireResourceRevision(queue.resourceId);
1927
- this.bumpResource(queue.resourceId, revision, now);
1928
- const payloadHash = this.state.putJson({ runId: state.runId, state: presentationState, followUps: followUpState }, now);
1929
- insertGenericEvent(this.state, queue.resourceId, revision + 1, "follow-up.run-finished", "system", null, payloadHash, now);
1930
1962
  }
1931
1963
  syncNodeAttempts(state, snapshot, now) {
1932
1964
  for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
@@ -2059,7 +2091,7 @@ export class WorkflowRunStore {
2059
2091
  JOIN session_segments s ON s.segment_id = e.segment_id
2060
2092
  JOIN blobs b ON b.blob_hash = e.entry_hash
2061
2093
  WHERE s.run_id = ?
2062
- AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-agent-step'
2094
+ AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-step'
2063
2095
  AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
2064
2096
  ORDER BY e.recorded_at DESC LIMIT 1`)
2065
2097
  .get(runId, attemptId);
@@ -2192,13 +2224,22 @@ export class WorkflowRunStore {
2192
2224
  }
2193
2225
  readRunState(row, snapshot) {
2194
2226
  const steps = this.readSteps(row.runId);
2227
+ return this.buildRunState(row, snapshot, steps, steps, this.readUpdates(row.runId).updates ?? []);
2228
+ }
2229
+ buildRunState(row, snapshot, projectionSteps, visibleSteps, visibleUpdates) {
2195
2230
  const sources = this.readRunSources(row.runId, snapshot);
2196
2231
  const carriedStepCount = this.carriedStepCount(row.runId);
2197
- const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
2232
+ const activeAttempt = row.status === "running" || row.status === "waiting"
2233
+ ? this.readActiveAttempt(row.runId)
2234
+ : undefined;
2235
+ const runningAttempt = row.status === "running" ? activeAttempt : undefined;
2236
+ const waitingNode = row.status === "waiting"
2237
+ ? (activeAttempt?.nodeId ?? projectionSteps.at(-1)?.nodeId)
2238
+ : undefined;
2198
2239
  const humanDecision = this.readHumanDecisionReceipt(row.runId);
2199
2240
  const outputs = {};
2200
2241
  const results = {};
2201
- for (const step of steps) {
2242
+ for (const step of projectionSteps) {
2202
2243
  const result = resultForStep(step);
2203
2244
  results[step.nodeId] = result;
2204
2245
  if (step.outcome === "ok")
@@ -2235,9 +2276,9 @@ export class WorkflowRunStore {
2235
2276
  input: this.state.readJson(row.inputHash),
2236
2277
  outputs,
2237
2278
  results,
2238
- steps,
2239
- ...this.readUpdates(row.runId),
2240
- ...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
2279
+ steps: visibleSteps,
2280
+ ...(visibleUpdates.length === 0 ? {} : { updates: visibleUpdates }),
2281
+ ...(runningAttempt === undefined ? {} : { currentNode: runningAttempt.nodeId }),
2241
2282
  ...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
2242
2283
  ...(activeAttempt?.startedAt === null || activeAttempt === undefined
2243
2284
  ? {}
@@ -2253,9 +2294,7 @@ export class WorkflowRunStore {
2253
2294
  ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
2254
2295
  ...(humanDecision === undefined ? {} : { humanDecision }),
2255
2296
  ...(row.paused === 0 ? {} : { paused: true }),
2256
- ...(row.status === "waiting" && steps.at(-1) !== undefined
2257
- ? { waitingOn: steps.at(-1)?.nodeId }
2258
- : {}),
2297
+ ...(waitingNode === undefined ? {} : { waitingOn: waitingNode }),
2259
2298
  ...(row.finalOutputHash === null
2260
2299
  ? {}
2261
2300
  : { finalOutput: this.state.readJson(row.finalOutputHash) }),
@@ -2264,39 +2303,50 @@ export class WorkflowRunStore {
2264
2303
  }
2265
2304
  readSteps(runId) {
2266
2305
  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`)
2306
+ .prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index`)
2296
2307
  .all(runId)
2297
2308
  .filter(isStepRow);
2309
+ return this.mapStepRows(runId, rows, 0);
2310
+ }
2311
+ readStepRange(runId, range) {
2312
+ const rows = this.state.connection
2313
+ .prepare(`${STEP_ROW_SELECT} WHERE s.run_id = ? ORDER BY s.step_index LIMIT ? OFFSET ?`)
2314
+ .all(runId, range.limit, range.start)
2315
+ .filter(isStepRow);
2316
+ return this.mapStepRows(runId, rows, range.start);
2317
+ }
2318
+ readLatestSteps(runId, through) {
2319
+ const throughClause = through === undefined ? "" : " AND s2.step_index <= ?";
2320
+ const params = through === undefined ? [runId, runId] : [runId, runId, through];
2321
+ const rows = this.state.connection
2322
+ .prepare(`${STEP_ROW_SELECT}
2323
+ WHERE s.run_id = ? AND s.step_index IN (
2324
+ SELECT max(s2.step_index) FROM run_steps s2
2325
+ JOIN node_attempts a2 ON a2.attempt_id = s2.attempt_id
2326
+ WHERE s2.run_id = ?${throughClause} GROUP BY a2.node_id
2327
+ ) ORDER BY s.step_index`)
2328
+ .all(...params)
2329
+ .filter(isStepRow);
2330
+ return this.mapStepRows(runId, rows);
2331
+ }
2332
+ readTakenTransitions(runId, through) {
2333
+ const rows = this.state.connection
2334
+ .prepare(`WITH ordered AS (
2335
+ SELECT s.step_index, a.node_id AS nodeId,
2336
+ lag(a.node_id) OVER (ORDER BY s.step_index) AS previousNodeId
2337
+ FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
2338
+ WHERE s.run_id = ? AND s.step_index <= ?
2339
+ )
2340
+ SELECT DISTINCT previousNodeId, nodeId FROM ordered
2341
+ WHERE previousNodeId IS NOT NULL ORDER BY previousNodeId, nodeId`)
2342
+ .all(runId, through);
2343
+ return rows.flatMap((row) => isRecord(row) && typeof row.previousNodeId === "string" && typeof row.nodeId === "string"
2344
+ ? [`${row.previousNodeId}->${row.nodeId}`]
2345
+ : []);
2346
+ }
2347
+ mapStepRows(runId, rows, expectedStart) {
2298
2348
  return rows.map((row, index) => {
2299
- if (row.stepIndex !== index)
2349
+ if (expectedStart !== undefined && row.stepIndex !== expectedStart + index)
2300
2350
  throw new Error(`Workflow run step sequence has a gap: ${runId}`);
2301
2351
  const receipt = row.receiptHash === null ? {} : this.readJsonAs(row.receiptHash);
2302
2352
  const prompt = row.promptEntryHash !== null
@@ -2441,6 +2491,38 @@ export class WorkflowRunStore {
2441
2491
  }
2442
2492
  return updates === undefined ? {} : { updates };
2443
2493
  }
2494
+ readCurrentUpdates(runId, range) {
2495
+ const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
2496
+ const params = range === undefined ? [runId] : [runId, range.limit, range.start];
2497
+ const rows = this.state.connection
2498
+ .prepare(`WITH ranked AS (
2499
+ SELECT u.update_id AS updateId, u.run_revision AS runRevision,
2500
+ a.node_id AS nodeId, u.attempt_id AS attemptId,
2501
+ u.update_type AS updateType, u.update_key AS updateKey,
2502
+ u.data_hash AS dataHash, u.recorded_at AS recordedAt,
2503
+ row_number() OVER (
2504
+ PARTITION BY u.update_type, u.update_key ORDER BY u.run_revision DESC
2505
+ ) AS rank
2506
+ FROM workflow_updates u
2507
+ JOIN node_attempts a ON a.attempt_id = u.attempt_id
2508
+ WHERE a.run_id = ?
2509
+ )
2510
+ SELECT updateId, runRevision, nodeId, attemptId, updateType, updateKey, dataHash, recordedAt
2511
+ FROM ranked WHERE rank = 1 ORDER BY runRevision${rangeClause}`)
2512
+ .all(...params)
2513
+ .filter(isUpdateRow);
2514
+ return rows.map((row) => ({
2515
+ updateId: row.updateId,
2516
+ seq: row.runRevision,
2517
+ at: new Date(row.recordedAt).toISOString(),
2518
+ runId,
2519
+ nodeId: row.nodeId,
2520
+ attemptId: row.attemptId,
2521
+ type: row.updateType,
2522
+ key: row.updateKey,
2523
+ data: this.readJsonAs(row.dataHash),
2524
+ }));
2525
+ }
2444
2526
  readJsonAs(hash) {
2445
2527
  return this.state.readJson(hash);
2446
2528
  }
@@ -2458,12 +2540,14 @@ export class WorkflowRunStore {
2458
2540
  }
2459
2541
  return value;
2460
2542
  }
2461
- traceEvents(run) {
2543
+ traceEvents(run, range) {
2544
+ const rangeClause = range === undefined ? "" : " LIMIT ? OFFSET ?";
2545
+ const params = range === undefined ? [run.resourceId] : [run.resourceId, range.limit, range.start];
2462
2546
  const rows = this.state.connection
2463
2547
  .prepare(`SELECT resource_revision AS resourceRevision, event_type AS eventType,
2464
2548
  payload_hash AS payloadHash, recorded_at AS recordedAt
2465
- FROM events WHERE resource_id = ? ORDER BY resource_revision`)
2466
- .all(run.resourceId);
2549
+ FROM events WHERE resource_id = ? ORDER BY resource_revision${rangeClause}`)
2550
+ .all(...params);
2467
2551
  const events = [];
2468
2552
  for (const row of rows) {
2469
2553
  /* istanbul ignore if -- exact schema and internal query shape */
@@ -2512,14 +2596,16 @@ export class WorkflowRunStore {
2512
2596
  throw new Error(`Session capture segment is missing: ${segmentId}`);
2513
2597
  return row;
2514
2598
  }
2515
- loadSegment(segment, runState) {
2599
+ loadSegment(segment, runState, ranges) {
2516
2600
  const binding = segment.bindingHash === null
2517
2601
  ? null
2518
2602
  : this.state.readJson(segment.bindingHash);
2519
2603
  const entries = this.state.connection
2520
2604
  .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)
2605
+ FROM session_entries WHERE segment_id = ? ORDER BY entry_seq${ranges === undefined ? "" : " LIMIT ? OFFSET ?"}`)
2606
+ .all(...(ranges === undefined
2607
+ ? [segment.segmentId]
2608
+ : [segment.segmentId, ranges.entries.limit, ranges.entries.start]))
2523
2609
  .filter(isSessionEntryRow)
2524
2610
  .map((row) => ({
2525
2611
  seq: row.entrySeq,
@@ -2531,8 +2617,10 @@ export class WorkflowRunStore {
2531
2617
  attempt_id AS attemptId, turn_id AS turnId, message_id AS messageId,
2532
2618
  tool_call_id AS toolCallId, payload_hash AS payloadHash,
2533
2619
  recorded_at AS recordedAt
2534
- FROM session_events WHERE segment_id = ? ORDER BY event_seq`)
2535
- .all(segment.segmentId)
2620
+ FROM session_events WHERE segment_id = ? ORDER BY event_seq${ranges === undefined ? "" : " LIMIT ? OFFSET ?"}`)
2621
+ .all(...(ranges === undefined
2622
+ ? [segment.segmentId]
2623
+ : [segment.segmentId, ranges.events.limit, ranges.events.start]))
2536
2624
  .filter(isSessionEventRow)
2537
2625
  .map((row) => ({
2538
2626
  seq: row.eventSeq,
@@ -2558,7 +2646,14 @@ export class WorkflowRunStore {
2558
2646
  ...(failure === undefined ? {} : { failure }),
2559
2647
  };
2560
2648
  const diagnostics = [];
2561
- if (entries.length !== segment.entryCount || events.length !== segment.eventCount) {
2649
+ const durableCounts = ranges === undefined
2650
+ ? { entries: entries.length, events: events.length }
2651
+ : {
2652
+ entries: sessionRecordCount(this.state, "session_entries", segment.segmentId),
2653
+ events: sessionRecordCount(this.state, "session_events", segment.segmentId),
2654
+ };
2655
+ if (durableCounts.entries !== segment.entryCount ||
2656
+ durableCounts.events !== segment.eventCount) {
2562
2657
  diagnostics.push("session capture counts do not match durable rows");
2563
2658
  }
2564
2659
  if (runState.status !== "running" && segment.status === "recording") {
@@ -2847,6 +2942,12 @@ function workflowSourceFromRow(row) {
2847
2942
  ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
2848
2943
  : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
2849
2944
  }
2945
+ function sessionRecordCount(state, table, segmentId) {
2946
+ const row = state.connection
2947
+ .prepare(`SELECT count(*) AS count FROM ${table} WHERE segment_id = ?`)
2948
+ .get(segmentId);
2949
+ return isCountRow(row) ? row.count : 0;
2950
+ }
2850
2951
  function segmentIdFor(runId, attemptId) {
2851
2952
  return `segment-${createHash("sha256")
2852
2953
  .update(`${runId}\0${attemptId ?? ""}`)
@@ -3028,26 +3129,11 @@ function assertActorType(value) {
3028
3129
  return value;
3029
3130
  }
3030
3131
  function assertFollowUpState(value) {
3031
- if (value !== "queued" &&
3032
- value !== "pending_presentation" &&
3033
- value !== "ready" &&
3034
- value !== "sent" &&
3035
- value !== "removed" &&
3036
- value !== "cancelled") {
3132
+ if (value !== "queued" && value !== "removed" && value !== "cancelled") {
3037
3133
  throw new Error(`Unknown workflow follow-up state: ${value}`);
3038
3134
  }
3039
3135
  return value;
3040
3136
  }
3041
- function assertPresentationState(value) {
3042
- if (value !== "none" &&
3043
- value !== "not-needed" &&
3044
- value !== "pending" &&
3045
- value !== "settled" &&
3046
- value !== "unavailable") {
3047
- throw new Error(`Unknown workflow presentation state: ${value}`);
3048
- }
3049
- return value;
3050
- }
3051
3137
  function isSettingsScopeRow(value) {
3052
3138
  return (isRecord(value) &&
3053
3139
  typeof value.scopeId === "string" &&
@@ -3089,23 +3175,10 @@ function isSettingsChangeRow(value) {
3089
3175
  Buffer.isBuffer(value.afterHash) &&
3090
3176
  typeof value.acceptedAt === "number");
3091
3177
  }
3092
- function isFollowUpQueueRow(value) {
3093
- return (isRecord(value) &&
3094
- typeof value.runId === "string" &&
3095
- typeof value.resourceId === "string" &&
3096
- (typeof value.originSessionId === "string" || value.originSessionId === null) &&
3097
- typeof value.presentationState === "string" &&
3098
- (typeof value.presentationEntryId === "string" || value.presentationEntryId === null) &&
3099
- (typeof value.presentationAssistantEntryId === "string" ||
3100
- value.presentationAssistantEntryId === null) &&
3101
- (Buffer.isBuffer(value.presentationReasonHash) || value.presentationReasonHash === null) &&
3102
- typeof value.revision === "number");
3103
- }
3104
3178
  function isFollowUpRow(value) {
3105
3179
  return (isRecord(value) &&
3106
3180
  typeof value.followUpId === "string" &&
3107
3181
  typeof value.resourceId === "string" &&
3108
- typeof value.queueResourceId === "string" &&
3109
3182
  typeof value.runId === "string" &&
3110
3183
  typeof value.requestId === "string" &&
3111
3184
  typeof value.orderNumber === "number" &&
@@ -3115,16 +3188,19 @@ function isFollowUpRow(value) {
3115
3188
  typeof value.sourceType === "string" &&
3116
3189
  Buffer.isBuffer(value.promptHash) &&
3117
3190
  typeof value.status === "string" &&
3118
- (typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
3119
3191
  (Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
3120
3192
  typeof value.createdAt === "number" &&
3121
3193
  typeof value.updatedAt === "number" &&
3122
- (typeof value.sentAt === "number" || value.sentAt === null) &&
3123
3194
  typeof value.revision === "number");
3124
3195
  }
3125
3196
  function isParentRunRow(value) {
3126
3197
  return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
3127
3198
  }
3199
+ function isRunLineageRow(value) {
3200
+ return (isRecord(value) &&
3201
+ typeof value.rootRunId === "string" &&
3202
+ typeof value.restartNumber === "number");
3203
+ }
3128
3204
  function isStatusRow(value) {
3129
3205
  return isRecord(value) && typeof value.status === "string";
3130
3206
  }
@@ -3134,9 +3210,6 @@ function isCountRow(value) {
3134
3210
  function isStepIndexRow(value) {
3135
3211
  return isRecord(value) && typeof value.stepIndex === "number";
3136
3212
  }
3137
- function isLeaseExpiryRow(value) {
3138
- return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
3139
- }
3140
3213
  function isNextOrderRow(value) {
3141
3214
  return isRecord(value) && typeof value.nextOrder === "number";
3142
3215
  }