@osolmaz/pi-workflows 0.16.0 → 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 (154) hide show
  1. package/README.md +18 -2
  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/client.d.ts +1 -0
  15. package/dist/client/client.js +5 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/client/index.d.ts +1 -1
  18. package/dist/client/protocol.d.ts +1 -1
  19. package/dist/client/protocol.js +11 -5
  20. package/dist/client/protocol.js.map +1 -1
  21. package/dist/client/view.d.ts +42 -16
  22. package/dist/client/view.js.map +1 -1
  23. package/dist/controllers/index.d.ts +1 -1
  24. package/dist/controllers/index.js.map +1 -1
  25. package/dist/controllers/sqlite.d.ts +12 -165
  26. package/dist/controllers/sqlite.js +124 -435
  27. package/dist/controllers/sqlite.js.map +1 -1
  28. package/dist/extension/index.d.ts +23 -2
  29. package/dist/extension/index.js +619 -535
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/recorder.d.ts +95 -0
  32. package/dist/extension/recorder.js +530 -0
  33. package/dist/extension/recorder.js.map +1 -0
  34. package/dist/extension/remote-recorder-store.d.ts +25 -0
  35. package/dist/extension/remote-recorder-store.js +81 -0
  36. package/dist/extension/remote-recorder-store.js.map +1 -0
  37. package/dist/extension/session-events.d.ts +134 -0
  38. package/dist/extension/session-events.js +60 -0
  39. package/dist/extension/session-events.js.map +1 -0
  40. package/dist/extension/session-view.d.ts +2 -0
  41. package/dist/extension/session-view.js +60 -2
  42. package/dist/extension/session-view.js.map +1 -1
  43. package/dist/extension/step-message.d.ts +6 -4
  44. package/dist/extension/step-message.js +12 -6
  45. package/dist/extension/step-message.js.map +1 -1
  46. package/dist/extension/widget.d.ts +2 -2
  47. package/dist/extension/widget.js +28 -18
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  50. package/dist/extension/workflow-message-coordinator.js +294 -0
  51. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  52. package/dist/host/channel-effects.d.ts +72 -0
  53. package/dist/host/channel-effects.js +271 -0
  54. package/dist/host/channel-effects.js.map +1 -0
  55. package/dist/host/channel-supervisor.d.ts +21 -0
  56. package/dist/host/channel-supervisor.js +54 -0
  57. package/dist/host/channel-supervisor.js.map +1 -0
  58. package/dist/host/runner.d.ts +43 -8
  59. package/dist/host/runner.js +1567 -331
  60. package/dist/host/runner.js.map +1 -1
  61. package/dist/host/state.d.ts +19 -16
  62. package/dist/host/state.js +251 -63
  63. package/dist/host/state.js.map +1 -1
  64. package/dist/host/view.d.ts +19 -9
  65. package/dist/host/view.js +222 -118
  66. package/dist/host/view.js.map +1 -1
  67. package/dist/state/prune.js +3 -25
  68. package/dist/state/prune.js.map +1 -1
  69. package/dist/state/schema.js +76 -97
  70. package/dist/state/schema.js.map +1 -1
  71. package/dist/state/workflow-messages.d.ts +96 -0
  72. package/dist/state/workflow-messages.js +335 -0
  73. package/dist/state/workflow-messages.js.map +1 -0
  74. package/dist/viewer/render.js +1 -1
  75. package/dist/viewer/render.js.map +1 -1
  76. package/dist/viewer/tui.d.ts +1 -1
  77. package/dist/viewer/tui.js +65 -8
  78. package/dist/viewer/tui.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +2 -0
  80. package/dist/workflows/human-decision.js +8 -2
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +1 -1
  83. package/dist/workflows/index.js.map +1 -1
  84. package/dist/workflows/settings.d.ts +1 -8
  85. package/dist/workflows/settings.js.map +1 -1
  86. package/dist/workflows/store.d.ts +5 -19
  87. package/dist/workflows/store.js +121 -354
  88. package/dist/workflows/store.js.map +1 -1
  89. package/dist/workflows/tool-input.d.ts +21 -0
  90. package/dist/workflows/tool-input.js +23 -1
  91. package/dist/workflows/tool-input.js.map +1 -1
  92. package/dist/workflows/types.d.ts +5 -5
  93. package/dist/workflows/workflow-message-content.d.ts +38 -0
  94. package/dist/workflows/workflow-message-content.js +157 -0
  95. package/dist/workflows/workflow-message-content.js.map +1 -0
  96. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  97. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  98. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  99. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  100. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  101. package/docs/2026-09-01-unified-workflow-client-plan.md +5 -1
  102. package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
  103. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  104. package/docs/DEFERRED_TURNS.md +66 -294
  105. package/docs/HUMAN_DECISIONS.md +29 -30
  106. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  107. package/docs/SQLITE_STATE.md +19 -15
  108. package/docs/WORKFLOW_HOST.md +93 -28
  109. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  110. package/docs/live-replay-protocol.md +8 -4
  111. package/docs/tui-viewer.md +3 -1
  112. package/docs/workflows.md +34 -27
  113. package/herdr-plugin.toml +1 -1
  114. package/package.json +1 -1
  115. package/plugins/herdr/viewer.mjs +14 -5
  116. package/src/channels/adapter-entry.ts +220 -0
  117. package/src/channels/config.ts +296 -0
  118. package/src/channels/protocol.ts +333 -0
  119. package/src/channels/telegram.ts +335 -0
  120. package/src/client/client.ts +6 -2
  121. package/src/client/index.ts +2 -1
  122. package/src/client/protocol.ts +11 -5
  123. package/src/client/view.ts +43 -18
  124. package/src/controllers/index.ts +0 -1
  125. package/src/controllers/sqlite.ts +164 -844
  126. package/src/extension/index.ts +693 -646
  127. package/src/extension/recorder.ts +687 -0
  128. package/src/extension/remote-recorder-store.ts +126 -0
  129. package/src/extension/session-events.ts +119 -0
  130. package/src/extension/session-view.ts +86 -3
  131. package/src/extension/step-message.ts +19 -9
  132. package/src/extension/widget.ts +37 -17
  133. package/src/extension/workflow-message-coordinator.ts +334 -0
  134. package/src/host/channel-effects.ts +465 -0
  135. package/src/host/channel-supervisor.ts +73 -0
  136. package/src/host/runner.ts +2120 -531
  137. package/src/host/state.ts +333 -94
  138. package/src/host/view.ts +249 -121
  139. package/src/state/prune.ts +3 -31
  140. package/src/state/schema.ts +76 -97
  141. package/src/state/workflow-messages.ts +548 -0
  142. package/src/viewer/render.ts +1 -5
  143. package/src/viewer/tui.ts +71 -6
  144. package/src/workflows/human-decision.ts +18 -2
  145. package/src/workflows/index.ts +0 -1
  146. package/src/workflows/settings.ts +1 -20
  147. package/src/workflows/store.ts +173 -489
  148. package/src/workflows/tool-input.ts +36 -1
  149. package/src/workflows/types.ts +5 -5
  150. package/src/workflows/workflow-message-content.ts +197 -0
  151. package/dist/extension/decision-channels.d.ts +0 -134
  152. package/dist/extension/decision-channels.js +0 -1307
  153. package/dist/extension/decision-channels.js.map +0 -1
  154. package/src/extension/decision-channels.ts +0 -1826
@@ -6,7 +6,6 @@ import { canonicalJson } from "../state/json.js";
6
6
  import { resourceIdFor, tokenHash } from "../state/mutation.js";
7
7
  import { initializeViewerRun, recordViewerDeltas } from "../state/viewer.js";
8
8
  import { EffectRequestConflictError, ResourceConflictError, ResourceNotFoundError, WorkflowRequestConflictError, } from "./errors.js";
9
- const TURN_INTENT_FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
10
9
  export class SqliteControllerStore {
11
10
  filePath;
12
11
  state;
@@ -520,32 +519,65 @@ export class SqliteControllerStore {
520
519
  if (this.getWorkflowRun(options.runId) !== undefined) {
521
520
  throw new Error(`Workflow run already reserved: ${options.runId}`);
522
521
  }
522
+ const lineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
523
+ if (options.parentRunId === undefined && options.lineageKind !== undefined) {
524
+ throw new Error("A root workflow run cannot declare lineage");
525
+ }
526
+ let rootRunId = options.runId;
527
+ let restartNumber = 0;
528
+ let parentTerminalFingerprint = null;
523
529
  if (options.parentRunId !== undefined) {
524
530
  const parent = this.requireWorkflowRunRow(options.parentRunId);
525
531
  if (parent.originSessionId !== options.originSessionId) {
526
- throw new Error("Continuation parent belongs to another Pi session");
532
+ throw new Error("Workflow parent belongs to another Pi session");
527
533
  }
528
- if (parent.status === "parked") {
529
- const parentLease = this.requireLease(parent.resourceId);
530
- if (parentLease.ownerId !== null) {
531
- throw new Error("Continuation parent still has an active owner");
534
+ rootRunId = parent.rootRunId;
535
+ if (lineageKind === "restart") {
536
+ if (!["completed", "failed", "timed_out", "cancelled"].includes(parent.runStatus) ||
537
+ !["done", "failed", "cancelled"].includes(parent.status)) {
538
+ throw new Error("Restart parent is not terminal");
539
+ }
540
+ if (options.parentTerminalFingerprint === undefined) {
541
+ throw new Error("Restart requires the parent terminal fingerprint");
542
+ }
543
+ parentTerminalFingerprint = terminalFingerprintBuffer(options.parentTerminalFingerprint);
544
+ restartNumber = options.restartNumber ?? parent.restartNumber + 1;
545
+ if (restartNumber !== parent.restartNumber + 1) {
546
+ throw new Error("Restart number does not follow its parent");
532
547
  }
533
- this.state.connection
534
- .prepare(`UPDATE run_queue
535
- SET status = 'done', updated_at = ?, finished_at = ?
536
- WHERE run_id = ? AND status = 'parked'`)
537
- .run(now, now, parent.runId);
538
- const parentRevision = this.resourceRevision(parent.resourceId);
539
- this.bumpResource(parent.resourceId, parentRevision, now);
540
- this.insertEvent(parent.resourceId, parentRevision + 1, "run.queue_done_for_continuation", "session", options.originSessionId, { continuationRunId: options.runId }, now);
541
- }
542
- else if (parent.status === "done") {
543
- throw new Error("Continuation parent already has a reserved continuation");
544
548
  }
545
549
  else {
546
- throw new Error(`Continuation parent queue is ${parent.status}`);
550
+ if (options.parentTerminalFingerprint !== undefined ||
551
+ (options.restartNumber !== undefined && options.restartNumber !== parent.restartNumber)) {
552
+ throw new Error("Continuation lineage conflicts with restart metadata");
553
+ }
554
+ restartNumber = parent.restartNumber;
555
+ if (parent.status === "parked") {
556
+ const parentLease = this.requireLease(parent.resourceId);
557
+ if (parentLease.ownerId !== null) {
558
+ throw new Error("Continuation parent still has an active owner");
559
+ }
560
+ this.state.connection
561
+ .prepare(`UPDATE run_queue
562
+ SET status = 'done', updated_at = ?, finished_at = ?
563
+ WHERE run_id = ? AND status = 'parked'`)
564
+ .run(now, now, parent.runId);
565
+ const parentRevision = this.resourceRevision(parent.resourceId);
566
+ this.bumpResource(parent.resourceId, parentRevision, now);
567
+ this.insertEvent(parent.resourceId, parentRevision + 1, "run.queue_done_for_continuation", "session", options.originSessionId, { continuationRunId: options.runId }, now);
568
+ }
569
+ else if (parent.status === "done") {
570
+ throw new Error("Continuation parent already has a reserved continuation");
571
+ }
572
+ else {
573
+ throw new Error(`Continuation parent queue is ${parent.status}`);
574
+ }
547
575
  }
548
576
  }
577
+ else if (options.restartNumber !== undefined ||
578
+ options.parentTerminalFingerprint !== undefined) {
579
+ throw new Error("A root workflow run cannot declare restart metadata");
580
+ }
549
581
  const resourceId = resourceIdFor("run", options.runId);
550
582
  const definitionHash = this.state.putJson(options.definitionSnapshot, now);
551
583
  const queuedSource = queuedWorkflowSource(options.workflowSource);
@@ -566,11 +598,12 @@ export class SqliteControllerStore {
566
598
  .run(resourceId);
567
599
  this.state.connection
568
600
  .prepare(`INSERT INTO runs(
569
- run_id, resource_id, project_id, parent_run_id, definition_digest,
601
+ run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
602
+ restart_number, parent_terminal_fingerprint, definition_digest,
570
603
  workflow_ref, launch_options_hash, status, paused,
571
604
  input_hash, created_at, updated_at
572
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`)
573
- .run(options.runId, resourceId, this.requireProjectId(), options.parentRunId ?? null, definitionDigest, options.workflowSourceRef, launchHash, inputHash, now, now);
605
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`)
606
+ .run(options.runId, resourceId, this.requireProjectId(), options.parentRunId ?? null, rootRunId, lineageKind, restartNumber, parentTerminalFingerprint, definitionDigest, options.workflowSourceRef, launchHash, inputHash, now, now);
574
607
  initializeViewerRun(this.state, options.runId, now);
575
608
  insertQueuedRunSources(this.state, options.runId, queuedSource);
576
609
  this.state.connection
@@ -621,6 +654,11 @@ export class SqliteControllerStore {
621
654
  runnerId: options.runnerId,
622
655
  originSessionId: options.originSessionId,
623
656
  ...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
657
+ ...(options.lineageKind === undefined ? {} : { lineageKind: options.lineageKind }),
658
+ ...(options.restartNumber === undefined ? {} : { restartNumber: options.restartNumber }),
659
+ ...(options.parentTerminalFingerprint === undefined
660
+ ? {}
661
+ : { parentTerminalFingerprint: options.parentTerminalFingerprint }),
624
662
  ...(options.now === undefined ? {} : { now: options.now }),
625
663
  });
626
664
  }
@@ -736,17 +774,23 @@ export class SqliteControllerStore {
736
774
  .prepare(`SELECT i.request_id AS requestId, i.attempt_id AS attemptId
737
775
  FROM interactive_requests i
738
776
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
739
- WHERE i.run_id = ? AND i.status IN ('pending', 'presenting')
777
+ JOIN runs r ON r.run_id = i.run_id
778
+ WHERE i.run_id = ? AND i.target_session_id = ?
779
+ AND i.status = 'pending' AND r.paused = 0
740
780
  AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
781
+ AND EXISTS (
782
+ SELECT 1 FROM workflow_messages m
783
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
784
+ WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
785
+ )
741
786
  ORDER BY a.deadline_at, i.request_id LIMIT 1`)
742
- .get(options.runId, now);
787
+ .get(options.runId, options.targetSessionId, now);
743
788
  if (!isExpiredInteractionRow(interaction))
744
789
  return false;
745
790
  const request = this.state.connection
746
791
  .prepare(`UPDATE interactive_requests
747
- SET status = 'cancelled', presenter_id = NULL,
748
- presentation_claim_expires_at = NULL, revision = revision + 1, updated_at = ?
749
- WHERE request_id = ? AND run_id = ? AND status IN ('pending', 'presenting')`)
792
+ SET status = 'cancelled', revision = revision + 1, updated_at = ?
793
+ WHERE request_id = ? AND run_id = ? AND status = 'pending'`)
750
794
  .run(now, interaction.requestId, options.runId);
751
795
  const run = this.state.connection
752
796
  .prepare(`UPDATE runs
@@ -763,7 +807,7 @@ export class SqliteControllerStore {
763
807
  if (request.changes !== 1 || run.changes !== 1 || queue.changes !== 1) {
764
808
  throw new Error(`Workflow run ${options.runId} changed during interaction timeout`);
765
809
  }
766
- const error = "Workflow node deadline expired before origin-session input arrived";
810
+ const error = "Workflow node model-turn deadline expired";
767
811
  const receiptHash = this.state.putJson({ status: "rejected", error }, now);
768
812
  this.state.connection
769
813
  .prepare(`UPDATE interactive_submissions SET outcome = 'rejected', receipt_hash = ?
@@ -788,7 +832,7 @@ export class SqliteControllerStore {
788
832
  "(l.owner_id IS NULL OR l.expires_at <= ? OR l.owner_id = ?)",
789
833
  "(q.affinity_runner_id IS NULL OR q.affinity_runner_id = ? OR q.status IN ('parked', 'starting', 'running'))",
790
834
  "NOT (q.status = 'parked' AND (r.status = 'waiting' OR r.paused = 1))",
791
- "NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status IN ('pending', 'presenting'))",
835
+ "NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status = 'pending')",
792
836
  "(q.error_code IS NULL OR q.error_code <> 'workflowSourceChanged')",
793
837
  ];
794
838
  const params = [now, now, options.runnerId, options.runnerId];
@@ -874,7 +918,7 @@ export class SqliteControllerStore {
874
918
  return false;
875
919
  const pending = this.state.connection
876
920
  .prepare(`SELECT 1 FROM interactive_requests
877
- WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
921
+ WHERE run_id = ? AND status = 'pending' LIMIT 1`)
878
922
  .get(options.runId);
879
923
  if (pending === undefined)
880
924
  return false;
@@ -910,7 +954,7 @@ export class SqliteControllerStore {
910
954
  return false;
911
955
  const pending = this.state.connection
912
956
  .prepare(`SELECT 1 FROM interactive_requests
913
- WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
957
+ WHERE run_id = ? AND status = 'pending' LIMIT 1`)
914
958
  .get(options.runId);
915
959
  if (pending === undefined)
916
960
  return false;
@@ -1170,215 +1214,6 @@ export class SqliteControllerStore {
1170
1214
  payload: row.payloadHash === null ? {} : this.state.readJson(row.payloadHash),
1171
1215
  }));
1172
1216
  }
1173
- ensureWorkflowTurnIntent(options) {
1174
- const existing = this.turnIntentBySource(options.sourceEventId);
1175
- if (existing !== undefined)
1176
- return existing;
1177
- const now = epoch(validTimestamp(options.requestedAt));
1178
- const run = this.requireWorkflowRunRow(options.runId);
1179
- const intentId = options.intentId;
1180
- const resourceId = resourceIdFor("turn_intent", intentId);
1181
- const effectId = `effect-${randomUUID()}`;
1182
- this.state.transaction(() => {
1183
- const factsHash = this.state.putJson(options.fallbackFacts, now);
1184
- this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "turn.deliver", intentId, factsHash, "run", now);
1185
- this.state.connection
1186
- .prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'turn_intent', ?, 1, ?, ?)")
1187
- .run(resourceId, intentId, now, now);
1188
- this.state.connection
1189
- .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
1190
- .run(resourceId);
1191
- this.state.connection
1192
- .prepare(`INSERT INTO turn_intents(
1193
- turn_intent_id, resource_id, effect_id, source_event_id, run_id,
1194
- workflow_ref, target_session_id, cause, node_id, attempt_id,
1195
- facts_hash, requested_at, eligible_at, created_at
1196
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1197
- .run(intentId, resourceId, effectId, options.sourceEventId, options.runId, options.workflowRef, options.targetSessionId, options.cause, options.nodeId ?? null, options.attemptId ?? null, factsHash, now, options.eligible ? now : null, now);
1198
- this.insertEvent(resourceId, 1, "turn.requested", "session", options.targetSessionId, {}, now);
1199
- });
1200
- return this.requireTurnIntent(intentId);
1201
- }
1202
- getWorkflowTurnIntent(intentId) {
1203
- return this.turnIntent(intentId);
1204
- }
1205
- findPendingWorkflowTurnIntent(options) {
1206
- const row = this.state.connection
1207
- .prepare(turnIntentSelect("WHERE t.run_id = ? AND t.target_session_id = ? AND t.resolved_at IS NULL ORDER BY t.requested_at DESC LIMIT 1"))
1208
- .get(options.runId, options.targetSessionId);
1209
- return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
1210
- }
1211
- isWorkflowTurnIntentClaimLive(options) {
1212
- const row = this.turnIntentRow(options.intentId);
1213
- return (row !== undefined &&
1214
- row.targetSessionId === options.targetSessionId &&
1215
- row.resolvedAt === null &&
1216
- this.verifyEffectToken(row.effectId, options.claimToken, options.now));
1217
- }
1218
- claimWorkflowTurnIntent(options) {
1219
- const intent = this.turnIntent(options.intentId);
1220
- if (intent?.targetSessionId !== options.targetSessionId)
1221
- return undefined;
1222
- return this.claimEffectForTurn(options.intentId, options.claimToken, options.leaseMs, options.now)
1223
- ? this.requireTurnIntent(options.intentId)
1224
- : undefined;
1225
- }
1226
- claimEligibleWorkflowTurnIntents(options) {
1227
- const now = epoch(validTimestamp(options.now));
1228
- const rows = this.state.connection
1229
- .prepare(turnIntentSelect("WHERE t.target_session_id = ? AND t.resolved_at IS NULL AND t.eligible_at IS NOT NULL AND t.eligible_at <= ? ORDER BY t.eligible_at LIMIT ?"))
1230
- .all(options.targetSessionId, now, options.limit ?? 10);
1231
- const claimed = [];
1232
- for (const row of rows) {
1233
- if (isTurnIntentRow(row) &&
1234
- this.claimEffectForTurn(row.intentId, options.claimToken, options.leaseMs, options.now)) {
1235
- claimed.push(this.requireTurnIntent(row.intentId));
1236
- }
1237
- }
1238
- return claimed;
1239
- }
1240
- hasClaimableWorkflowTurnIntent(options) {
1241
- const now = epoch(validTimestamp(options.now));
1242
- const row = this.state.connection
1243
- .prepare(`SELECT 1 AS present
1244
- FROM turn_intents t
1245
- JOIN effects e ON e.effect_id = t.effect_id
1246
- JOIN leases l ON l.resource_id = e.resource_id
1247
- WHERE t.target_session_id = ? AND t.resolved_at IS NULL
1248
- AND t.eligible_at IS NOT NULL AND t.eligible_at <= ?
1249
- AND e.status = 'pending'
1250
- AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
1251
- LIMIT 1`)
1252
- .get(options.targetSessionId, now, now);
1253
- return row !== undefined;
1254
- }
1255
- makeWorkflowTurnIntentEligible(options) {
1256
- const eligibleAt = epoch(validTimestamp(options.eligibleAt ?? options.now));
1257
- const factsHash = this.state.putJson(options.fallbackFacts, eligibleAt);
1258
- return (this.state.connection
1259
- .prepare("UPDATE turn_intents SET eligible_at = ?, facts_hash = ? WHERE turn_intent_id = ? AND resolved_at IS NULL")
1260
- .run(eligibleAt, factsHash, options.intentId).changes === 1);
1261
- }
1262
- resolveWorkflowTurnIntent(options) {
1263
- const now = epoch(validTimestamp(options.now));
1264
- return this.state.transaction(() => {
1265
- const row = this.turnIntentRow(options.intentId);
1266
- if (row === undefined ||
1267
- row.targetSessionId !== options.targetSessionId ||
1268
- !this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
1269
- return false;
1270
- }
1271
- const result = this.state.connection
1272
- .prepare(`UPDATE turn_intents SET resolved_at = ?, resolution_type = ?, resolution_message_id = ?
1273
- WHERE turn_intent_id = ? AND resolved_at IS NULL`)
1274
- .run(now, options.resolution, options.messageId ?? null, options.intentId);
1275
- if (result.changes === 1)
1276
- this.completeEffect(row.effectId, "applied", now);
1277
- return result.changes === 1;
1278
- });
1279
- }
1280
- releaseWorkflowTurnIntentClaim(options) {
1281
- const row = this.turnIntentRow(options.intentId);
1282
- return (row !== undefined &&
1283
- row.targetSessionId === options.targetSessionId &&
1284
- this.releaseEffectLease(row.effectId, options.claimToken));
1285
- }
1286
- listWorkflowTurnIntents(options = {}) {
1287
- const clauses = ["1 = 1"];
1288
- const params = [];
1289
- if (options.runId !== undefined) {
1290
- clauses.push("t.run_id = ?");
1291
- params.push(options.runId);
1292
- }
1293
- if (options.targetSessionId !== undefined) {
1294
- clauses.push("t.target_session_id = ?");
1295
- params.push(options.targetSessionId);
1296
- }
1297
- if (options.unresolvedOnly === true)
1298
- clauses.push("t.resolved_at IS NULL");
1299
- params.push(options.limit ?? 100);
1300
- const rows = this.state.connection
1301
- .prepare(turnIntentSelect(`WHERE ${clauses.join(" AND ")} ORDER BY t.requested_at DESC LIMIT ?`))
1302
- .all(...params);
1303
- return rows.filter(isTurnIntentRow).map((row) => this.mapTurnIntent(row));
1304
- }
1305
- enqueueWorkflowNotification(options) {
1306
- const existing = this.notification(options.runId, options.attemptId, options.notificationIndex);
1307
- if (existing !== undefined)
1308
- return existing;
1309
- const run = this.requireWorkflowRunRow(options.runId);
1310
- const now = epoch(validTimestamp(options.now));
1311
- const notificationId = `notification-${randomUUID()}`;
1312
- const resourceId = resourceIdFor("notification", notificationId);
1313
- const effectId = `effect-${randomUUID()}`;
1314
- this.state.transaction(() => {
1315
- const contentHash = this.state.putText(options.content, now);
1316
- this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "notification.deliver", notificationId, contentHash, "run", now);
1317
- this.state.connection
1318
- .prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'notification', ?, 1, ?, ?)")
1319
- .run(resourceId, notificationId, now, now);
1320
- this.state.connection
1321
- .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
1322
- .run(resourceId);
1323
- this.state.connection
1324
- .prepare(`INSERT INTO notifications(
1325
- notification_id, effect_id, run_id, attempt_id, notification_index,
1326
- target_session_id, notification_type, content_hash, created_at
1327
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1328
- .run(notificationId, effectId, options.runId, options.attemptId, options.notificationIndex, options.targetSessionId, options.kind, contentHash, now);
1329
- this.insertEvent(resourceId, 1, "notification.queued", "session", options.targetSessionId, {}, now);
1330
- });
1331
- return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
1332
- }
1333
- hasClaimableWorkflowNotification(options) {
1334
- const now = epoch(validTimestamp(options.now));
1335
- const row = this.state.connection
1336
- .prepare(`SELECT 1 AS present
1337
- FROM notifications n
1338
- JOIN effects e ON e.effect_id = n.effect_id
1339
- JOIN leases l ON l.resource_id = e.resource_id
1340
- WHERE n.target_session_id = ? AND e.status = 'pending'
1341
- AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
1342
- LIMIT 1`)
1343
- .get(options.targetSessionId, now);
1344
- return row !== undefined;
1345
- }
1346
- listPendingWorkflowNotifications(options) {
1347
- const now = Date.now();
1348
- return this.notificationRows(options.targetSessionId, options.limit ?? 20)
1349
- .map((row) => this.mapNotification(row))
1350
- .filter((notification) => notification.deliveryClaimExpiresAt === null ||
1351
- Date.parse(notification.deliveryClaimExpiresAt) <= now);
1352
- }
1353
- claimPendingWorkflowNotifications(options) {
1354
- const rows = this.notificationRows(options.targetSessionId, options.limit ?? 20);
1355
- const result = [];
1356
- for (const row of rows) {
1357
- if (this.claimEffect(row.effectId, options.targetSessionId, options.claimToken, options.leaseMs, options.now)) {
1358
- result.push(this.mapNotification(row));
1359
- }
1360
- }
1361
- return result;
1362
- }
1363
- isWorkflowNotificationClaimLive(options) {
1364
- const row = this.notificationRowById(options.notificationId);
1365
- return (row !== undefined &&
1366
- row.targetSessionId === options.targetSessionId &&
1367
- this.verifyEffectToken(row.effectId, options.claimToken, options.now));
1368
- }
1369
- markWorkflowNotificationDelivered(options) {
1370
- const now = epoch(validTimestamp(options.now));
1371
- return this.state.transaction(() => {
1372
- const row = this.notificationRowById(options.notificationId);
1373
- if (row === undefined ||
1374
- row.targetSessionId !== options.targetSessionId ||
1375
- !this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
1376
- return false;
1377
- }
1378
- this.completeEffect(row.effectId, "applied", now);
1379
- return true;
1380
- });
1381
- }
1382
1217
  settleRunEffect(runId, effectType) {
1383
1218
  const rows = this.state.connection
1384
1219
  .prepare(`SELECT e.effect_id AS effectId, e.resource_id AS resourceId
@@ -1722,6 +1557,17 @@ export class SqliteControllerStore {
1722
1557
  return this.mapWorkflowRun(this.requireWorkflowRunRow(runId));
1723
1558
  }
1724
1559
  assertWorkflowRunPreparationCompatible(row, options, definitionDigest) {
1560
+ const expectedLineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
1561
+ const parent = options.parentRunId === undefined
1562
+ ? undefined
1563
+ : this.requireWorkflowRunRow(options.parentRunId);
1564
+ const expectedRootRunId = parent?.rootRunId ?? options.runId;
1565
+ const expectedRestartNumber = expectedLineageKind === "restart"
1566
+ ? (options.restartNumber ?? (parent?.restartNumber ?? -1) + 1)
1567
+ : (parent?.restartNumber ?? 0);
1568
+ const expectedParentTerminalFingerprint = options.parentTerminalFingerprint === undefined
1569
+ ? null
1570
+ : terminalFingerprintBuffer(options.parentTerminalFingerprint);
1725
1571
  const compatible = row.workflowName === options.workflowName &&
1726
1572
  row.workflowRef === options.workflowSourceRef &&
1727
1573
  row.definitionDigest.equals(definitionDigest) &&
@@ -1732,7 +1578,14 @@ export class SqliteControllerStore {
1732
1578
  canonicalJson(options.launchOptions ?? {}) &&
1733
1579
  row.originSessionId === options.originSessionId &&
1734
1580
  row.executionMode === (options.executionMode ?? "interactive") &&
1735
- row.parentRunId === (options.parentRunId ?? null);
1581
+ row.parentRunId === (options.parentRunId ?? null) &&
1582
+ row.rootRunId === expectedRootRunId &&
1583
+ row.lineageKind === expectedLineageKind &&
1584
+ row.restartNumber === expectedRestartNumber &&
1585
+ ((row.parentTerminalFingerprint === null && expectedParentTerminalFingerprint === null) ||
1586
+ (row.parentTerminalFingerprint !== null &&
1587
+ expectedParentTerminalFingerprint !== null &&
1588
+ row.parentTerminalFingerprint.equals(expectedParentTerminalFingerprint)));
1736
1589
  if (!compatible) {
1737
1590
  throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
1738
1591
  }
@@ -1763,6 +1616,10 @@ export class SqliteControllerStore {
1763
1616
  originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
1764
1617
  executionMode: row.executionMode,
1765
1618
  parentRunId: row.parentRunId,
1619
+ rootRunId: row.rootRunId,
1620
+ lineageKind: row.lineageKind,
1621
+ restartNumber: row.restartNumber,
1622
+ parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
1766
1623
  errorCode: row.errorCode,
1767
1624
  errorMessage: row.errorHash === null
1768
1625
  ? null
@@ -1783,7 +1640,7 @@ export class SqliteControllerStore {
1783
1640
  if (options.allowPendingInteraction !== true &&
1784
1641
  this.state.connection
1785
1642
  .prepare(`SELECT 1 FROM interactive_requests
1786
- WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
1643
+ WHERE run_id = ? AND status = 'pending' LIMIT 1`)
1787
1644
  .get(runId) !== undefined) {
1788
1645
  return undefined;
1789
1646
  }
@@ -1977,165 +1834,10 @@ export class SqliteControllerStore {
1977
1834
  .run(receiptHash, runId);
1978
1835
  this.state.connection
1979
1836
  .prepare(`UPDATE interactive_requests
1980
- SET status = 'cancelled', presenter_id = NULL,
1981
- presentation_claim_expires_at = NULL, revision = revision + 1, updated_at = ?
1982
- WHERE run_id = ? AND status IN ('pending', 'presenting')`)
1837
+ SET status = 'cancelled', revision = revision + 1, updated_at = ?
1838
+ WHERE run_id = ? AND status = 'pending'`)
1983
1839
  .run(now, runId);
1984
1840
  }
1985
- turnIntent(intentId) {
1986
- const row = this.turnIntentRow(intentId);
1987
- return row === undefined ? undefined : this.mapTurnIntent(row);
1988
- }
1989
- requireTurnIntent(intentId) {
1990
- const row = this.turnIntent(intentId);
1991
- if (row === undefined)
1992
- throw new Error(`Workflow turn intent not found: ${intentId}`);
1993
- return row;
1994
- }
1995
- turnIntentRow(intentId) {
1996
- const row = this.state.connection
1997
- .prepare(turnIntentSelect("WHERE t.turn_intent_id = ?"))
1998
- .get(intentId);
1999
- return isTurnIntentRow(row) ? row : undefined;
2000
- }
2001
- turnIntentBySource(sourceEventId) {
2002
- const row = this.state.connection
2003
- .prepare(turnIntentSelect("WHERE t.source_event_id = ?"))
2004
- .get(sourceEventId);
2005
- return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
2006
- }
2007
- /* istanbul ignore next -- pure projection covered by integration tests */
2008
- mapTurnIntent(row) {
2009
- return {
2010
- intentId: row.intentId,
2011
- sourceEventId: row.sourceEventId,
2012
- runId: row.runId,
2013
- workflowRef: row.workflowRef,
2014
- targetSessionId: row.targetSessionId,
2015
- cause: row.cause,
2016
- nodeId: row.nodeId,
2017
- attemptId: row.attemptId,
2018
- fallbackFacts: this.state.readJson(row.factsHash),
2019
- requestedAt: new Date(row.requestedAt).toISOString(),
2020
- eligibleAt: row.eligibleAt === null ? null : new Date(row.eligibleAt).toISOString(),
2021
- resolvedAt: row.resolvedAt === null ? null : new Date(row.resolvedAt).toISOString(),
2022
- resolution: row.resolution,
2023
- resolutionMessageId: row.resolutionMessageId,
2024
- deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
2025
- };
2026
- }
2027
- claimEffectForTurn(intentId, token, leaseMs, nowValue) {
2028
- const row = this.turnIntentRow(intentId);
2029
- return (row !== undefined &&
2030
- this.claimEffect(row.effectId, row.targetSessionId, token, leaseMs, nowValue));
2031
- }
2032
- claimEffect(effectId, ownerId, token, leaseMs, nowValue) {
2033
- const now = epoch(validTimestamp(nowValue));
2034
- return this.state.transaction(() => {
2035
- const row = this.state.connection
2036
- .prepare("SELECT resource_id AS resourceId FROM effects WHERE effect_id = ? AND status = 'pending'")
2037
- .get(effectId);
2038
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
2039
- if (!isResourceIdRow(row))
2040
- return false;
2041
- const lease = this.requireLease(row.resourceId);
2042
- if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
2043
- return false;
2044
- return (this.state.connection
2045
- .prepare(`UPDATE leases SET generation = ?, owner_type = 'session', owner_id = ?, token_hash = ?,
2046
- acquired_at = ?, heartbeat_at = ?, expires_at = ?
2047
- WHERE resource_id = ? AND generation = ?`)
2048
- .run(lease.generation + 1, ownerId, tokenHash(token), now, now, now + leaseMs, row.resourceId, lease.generation).changes === 1);
2049
- });
2050
- }
2051
- verifyEffectToken(effectId, token, nowValue) {
2052
- const now = epoch(validTimestamp(nowValue));
2053
- const row = this.state.connection
2054
- .prepare(`SELECT l.token_hash AS tokenHash, l.expires_at AS expiresAt
2055
- FROM effects e JOIN leases l ON l.resource_id = e.resource_id
2056
- WHERE e.effect_id = ?`)
2057
- .get(effectId);
2058
- return (isEffectTokenRow(row) &&
2059
- row.tokenHash !== null &&
2060
- row.tokenHash.equals(tokenHash(token)) &&
2061
- row.expiresAt !== null &&
2062
- row.expiresAt > now);
2063
- }
2064
- releaseEffectLease(effectId, token) {
2065
- return (this.state.connection
2066
- .prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
2067
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
2068
- WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)
2069
- AND token_hash = ?`)
2070
- .run(effectId, tokenHash(token)).changes === 1);
2071
- }
2072
- completeEffect(effectId, status, now) {
2073
- this.state.connection
2074
- .prepare("UPDATE effects SET status = ?, updated_at = ?, settled_at = ? WHERE effect_id = ?")
2075
- .run(status, now, now, effectId);
2076
- this.state.connection
2077
- .prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
2078
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
2079
- WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)`)
2080
- .run(effectId);
2081
- }
2082
- insertEffectResource(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now) {
2083
- this.state.connection
2084
- .prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'effect', ?, 1, ?, ?)")
2085
- .run(resourceId, effectId, now, now);
2086
- this.state.connection
2087
- .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
2088
- .run(resourceId);
2089
- this.state.connection
2090
- .prepare(`INSERT INTO effects(
2091
- effect_id, resource_id, source_resource_id, source_revision, effect_type,
2092
- idempotency_key, payload_hash, owner_scope, status, attempt_count,
2093
- created_at, updated_at
2094
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)`)
2095
- .run(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now, now);
2096
- this.insertEvent(resourceId, 1, "effect.created", "system", null, {}, now);
2097
- }
2098
- notification(runId, attemptId, index) {
2099
- const row = this.state.connection
2100
- .prepare(notificationSelect("WHERE n.run_id = ? AND n.attempt_id = ? AND n.notification_index = ?"))
2101
- .get(runId, attemptId, index);
2102
- return isNotificationRow(row) ? this.mapNotification(row) : undefined;
2103
- }
2104
- requireNotification(runId, attemptId, index) {
2105
- const record = this.notification(runId, attemptId, index);
2106
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
2107
- if (record === undefined)
2108
- throw new Error("Workflow notification was not stored");
2109
- return record;
2110
- }
2111
- notificationRows(sessionId, limit) {
2112
- return this.state.connection
2113
- .prepare(notificationSelect("WHERE n.target_session_id = ? AND e.status = 'pending' ORDER BY n.created_at LIMIT ?"))
2114
- .all(sessionId, limit)
2115
- .filter(isNotificationRow);
2116
- }
2117
- notificationRowById(id) {
2118
- const row = this.state.connection
2119
- .prepare(notificationSelect("WHERE n.notification_id = ?"))
2120
- .get(id);
2121
- return isNotificationRow(row) ? row : undefined;
2122
- }
2123
- /* istanbul ignore next -- pure projection covered by integration tests */
2124
- mapNotification(row) {
2125
- return {
2126
- notificationId: row.notificationId,
2127
- runId: row.runId,
2128
- nodeId: row.nodeId,
2129
- attemptId: row.attemptId,
2130
- notificationIndex: row.notificationIndex,
2131
- targetSessionId: row.targetSessionId,
2132
- kind: row.kind,
2133
- content: this.state.readBlob(row.contentHash)?.content.toString("utf8") ?? "",
2134
- createdAt: new Date(row.createdAt).toISOString(),
2135
- deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
2136
- deliveredAt: row.deliveredAt === null ? null : new Date(row.deliveredAt).toISOString(),
2137
- };
2138
- }
2139
1841
  assertWritable() {
2140
1842
  if (this.state.mode === "read-only")
2141
1843
  throw new Error("Controller store is read-only");
@@ -2177,6 +1879,9 @@ function workflowRunSelect(clause) {
2177
1879
  q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
2178
1880
  q.error_hash AS errorHash, b.origin_session_id AS originSessionId,
2179
1881
  b.execution_mode AS executionMode, r.parent_run_id AS parentRunId,
1882
+ r.root_run_id AS rootRunId, r.lineage_kind AS lineageKind,
1883
+ r.restart_number AS restartNumber,
1884
+ r.parent_terminal_fingerprint AS parentTerminalFingerprint,
2180
1885
  q.created_at AS createdAt, q.updated_at AS updatedAt,
2181
1886
  q.started_at AS startedAt, q.finished_at AS finishedAt,
2182
1887
  l.generation AS leaseGeneration, l.owner_id AS ownerId, l.expires_at AS claimExpiresAt
@@ -2190,8 +1895,10 @@ function workflowRunViewSelect(clause) {
2190
1895
  r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
2191
1896
  r.definition_digest AS definitionDigest, q.status,
2192
1897
  b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
2193
- r.parent_run_id AS parentRunId, q.error_code AS errorCode,
2194
- CAST(error.content AS TEXT) AS errorMessage,
1898
+ r.parent_run_id AS parentRunId, r.root_run_id AS rootRunId,
1899
+ r.lineage_kind AS lineageKind, r.restart_number AS restartNumber,
1900
+ r.parent_terminal_fingerprint AS parentTerminalFingerprint,
1901
+ q.error_code AS errorCode, CAST(error.content AS TEXT) AS errorMessage,
2195
1902
  q.created_at AS createdAt, q.updated_at AS updatedAt,
2196
1903
  q.started_at AS startedAt, q.finished_at AS finishedAt,
2197
1904
  source.source_type AS sourceType, source.source_ref AS sourceRef,
@@ -2202,29 +1909,6 @@ function workflowRunViewSelect(clause) {
2202
1909
  LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
2203
1910
  LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
2204
1911
  }
2205
- function turnIntentSelect(clause) {
2206
- return `SELECT t.turn_intent_id AS intentId, t.source_event_id AS sourceEventId,
2207
- t.run_id AS runId, t.workflow_ref AS workflowRef,
2208
- t.target_session_id AS targetSessionId, t.cause,
2209
- t.node_id AS nodeId, t.attempt_id AS attemptId, t.facts_hash AS factsHash,
2210
- t.requested_at AS requestedAt, t.eligible_at AS eligibleAt,
2211
- t.resolved_at AS resolvedAt, t.resolution_type AS resolution,
2212
- t.resolution_message_id AS resolutionMessageId, l.expires_at AS claimExpiresAt,
2213
- t.effect_id AS effectId
2214
- FROM turn_intents t JOIN effects e ON e.effect_id = t.effect_id
2215
- JOIN leases l ON l.resource_id = e.resource_id ${clause}`;
2216
- }
2217
- function notificationSelect(clause) {
2218
- return `SELECT n.notification_id AS notificationId, n.effect_id AS effectId,
2219
- n.run_id AS runId, a.node_id AS nodeId, n.attempt_id AS attemptId,
2220
- n.notification_index AS notificationIndex, n.target_session_id AS targetSessionId,
2221
- n.notification_type AS kind, n.content_hash AS contentHash,
2222
- n.created_at AS createdAt, l.expires_at AS claimExpiresAt,
2223
- e.settled_at AS deliveredAt
2224
- FROM notifications n JOIN effects e ON e.effect_id = n.effect_id
2225
- JOIN leases l ON l.resource_id = e.resource_id
2226
- JOIN node_attempts a ON a.attempt_id = n.attempt_id ${clause}`;
2227
- }
2228
1912
  function isRecord(value) {
2229
1913
  return typeof value === "object" && value !== null && !Array.isArray(value);
2230
1914
  }
@@ -2251,6 +1935,10 @@ function workflowRunViewRecord(row) {
2251
1935
  originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
2252
1936
  executionMode: row.executionMode,
2253
1937
  parentRunId: row.parentRunId,
1938
+ rootRunId: row.rootRunId,
1939
+ lineageKind: row.lineageKind,
1940
+ restartNumber: row.restartNumber,
1941
+ parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
2254
1942
  errorCode: row.errorCode,
2255
1943
  errorMessage: row.errorMessage,
2256
1944
  createdAt: new Date(row.createdAt).toISOString(),
@@ -2271,6 +1959,13 @@ function isWorkflowRunViewRow(value) {
2271
1959
  (value.originSessionId === null || typeof value.originSessionId === "string") &&
2272
1960
  (value.executionMode === "interactive" || value.executionMode === "headless") &&
2273
1961
  (value.parentRunId === null || typeof value.parentRunId === "string") &&
1962
+ typeof value.rootRunId === "string" &&
1963
+ (value.lineageKind === null ||
1964
+ value.lineageKind === "continuation" ||
1965
+ value.lineageKind === "restart") &&
1966
+ typeof value.restartNumber === "number" &&
1967
+ (value.parentTerminalFingerprint === null ||
1968
+ Buffer.isBuffer(value.parentTerminalFingerprint)) &&
2274
1969
  (value.errorCode === null || typeof value.errorCode === "string") &&
2275
1970
  (value.errorMessage === null || typeof value.errorMessage === "string") &&
2276
1971
  typeof value.createdAt === "number" &&
@@ -2318,12 +2013,6 @@ function isControllerEventRow(value) {
2318
2013
  function isRunEventRow(value) {
2319
2014
  return isRecord(value);
2320
2015
  }
2321
- function isTurnIntentRow(value) {
2322
- return isRecord(value);
2323
- }
2324
- function isNotificationRow(value) {
2325
- return isRecord(value);
2326
- }
2327
2016
  function isProjectRow(value) {
2328
2017
  return isRecord(value);
2329
2018
  }
@@ -2348,12 +2037,6 @@ function isSequenceRow(value) {
2348
2037
  function isKeyRow(value) {
2349
2038
  return isRecord(value);
2350
2039
  }
2351
- function isResourceIdRow(value) {
2352
- return isRecord(value);
2353
- }
2354
- function isEffectTokenRow(value) {
2355
- return isRecord(value);
2356
- }
2357
2040
  function isEffectIdentityRow(value) {
2358
2041
  return isRecord(value);
2359
2042
  }
@@ -2377,6 +2060,12 @@ function digestBuffer(value) {
2377
2060
  throw new Error("Expected a SHA-256 digest");
2378
2061
  return Buffer.from(hex, "hex");
2379
2062
  }
2063
+ function terminalFingerprintBuffer(value) {
2064
+ const hex = value.startsWith("sha256:") ? value.slice(7) : value;
2065
+ if (!/^[a-f0-9]{64}$/i.test(hex))
2066
+ throw new Error("Expected a terminal fingerprint");
2067
+ return Buffer.from(hex, "hex");
2068
+ }
2380
2069
  function queuedWorkflowSource(value) {
2381
2070
  if (isWorkflowSource(value)) {
2382
2071
  return { root: value, mounted: [] };