@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
@@ -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
  }
@@ -640,6 +678,32 @@ export class SqliteControllerStore {
640
678
  const row = this.workflowRunRow(runId);
641
679
  return row === undefined ? undefined : this.mapWorkflowRun(row);
642
680
  }
681
+ getWorkflowRunView(runId) {
682
+ const row = this.workflowRunViewRows("WHERE r.run_id = ?", [runId])[0];
683
+ return row === undefined ? undefined : workflowRunViewRecord(row);
684
+ }
685
+ workflowRunListRevision() {
686
+ const row = this.state.connection
687
+ .prepare(`SELECT count(*) AS count, COALESCE(sum(res.revision), 0) AS revisionSum,
688
+ COALESCE(max(q.updated_at), 0) AS updatedAt
689
+ FROM runs r JOIN run_queue q ON q.run_id = r.run_id
690
+ JOIN resources res ON res.resource_id = r.resource_id`)
691
+ .get();
692
+ if (!isRunListRevisionRow(row))
693
+ throw new Error("Workflow run list revision is invalid");
694
+ return {
695
+ total: row.count,
696
+ revision: `${row.count}:${row.revisionSum}:${row.updatedAt}`,
697
+ };
698
+ }
699
+ listWorkflowRunViews(options) {
700
+ const current = this.workflowRunListRevision();
701
+ const rows = this.workflowRunViewRows("ORDER BY q.created_at DESC LIMIT ? OFFSET ?", [
702
+ options.limit,
703
+ options.offset,
704
+ ]);
705
+ return { ...current, runs: rows.map(workflowRunViewRecord) };
706
+ }
643
707
  workflowRunProjectPath(runId) {
644
708
  const row = this.state.connection
645
709
  .prepare(`SELECT p.canonical_path AS canonicalPath
@@ -662,9 +726,10 @@ export class SqliteControllerStore {
662
726
  clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
663
727
  params.push(...options.excludeRunIds);
664
728
  }
665
- params.push(options.limit ?? 100);
729
+ if (options.limit !== undefined)
730
+ params.push(options.limit);
666
731
  const rows = this.state.connection
667
- .prepare(workflowRunSelect(`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC LIMIT ?`))
732
+ .prepare(workflowRunSelect(`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC${options.limit === undefined ? "" : " LIMIT ?"}`))
668
733
  .all(...params);
669
734
  return rows.filter(isRunRow).map((row) => this.mapWorkflowRun(row));
670
735
  }
@@ -674,6 +739,10 @@ export class SqliteControllerStore {
674
739
  .get(sessionId);
675
740
  return isRunRow(row) ? this.mapWorkflowRun(row) : undefined;
676
741
  }
742
+ findSessionReservationView(sessionId) {
743
+ const row = this.workflowRunViewRows("WHERE b.origin_session_id = ? AND q.status NOT IN ('done', 'failed', 'cancelled') ORDER BY q.created_at DESC LIMIT 1", [sessionId])[0];
744
+ return row === undefined ? undefined : workflowRunViewRecord(row);
745
+ }
677
746
  claimWorkflowRun(options) {
678
747
  const now = epoch(validTimestamp(options.now));
679
748
  return this.claimRun(options.runId, options.runnerId, options.claimToken, options.leaseMs, now);
@@ -705,17 +774,23 @@ export class SqliteControllerStore {
705
774
  .prepare(`SELECT i.request_id AS requestId, i.attempt_id AS attemptId
706
775
  FROM interactive_requests i
707
776
  JOIN node_attempts a ON a.attempt_id = i.attempt_id
708
- 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
709
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
+ )
710
786
  ORDER BY a.deadline_at, i.request_id LIMIT 1`)
711
- .get(options.runId, now);
787
+ .get(options.runId, options.targetSessionId, now);
712
788
  if (!isExpiredInteractionRow(interaction))
713
789
  return false;
714
790
  const request = this.state.connection
715
791
  .prepare(`UPDATE interactive_requests
716
- SET status = 'cancelled', presenter_id = NULL,
717
- presentation_claim_expires_at = NULL, revision = revision + 1, updated_at = ?
718
- 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'`)
719
794
  .run(now, interaction.requestId, options.runId);
720
795
  const run = this.state.connection
721
796
  .prepare(`UPDATE runs
@@ -732,7 +807,7 @@ export class SqliteControllerStore {
732
807
  if (request.changes !== 1 || run.changes !== 1 || queue.changes !== 1) {
733
808
  throw new Error(`Workflow run ${options.runId} changed during interaction timeout`);
734
809
  }
735
- const error = "Workflow node deadline expired before origin-session input arrived";
810
+ const error = "Workflow node model-turn deadline expired";
736
811
  const receiptHash = this.state.putJson({ status: "rejected", error }, now);
737
812
  this.state.connection
738
813
  .prepare(`UPDATE interactive_submissions SET outcome = 'rejected', receipt_hash = ?
@@ -757,7 +832,7 @@ export class SqliteControllerStore {
757
832
  "(l.owner_id IS NULL OR l.expires_at <= ? OR l.owner_id = ?)",
758
833
  "(q.affinity_runner_id IS NULL OR q.affinity_runner_id = ? OR q.status IN ('parked', 'starting', 'running'))",
759
834
  "NOT (q.status = 'parked' AND (r.status = 'waiting' OR r.paused = 1))",
760
- "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')",
761
836
  "(q.error_code IS NULL OR q.error_code <> 'workflowSourceChanged')",
762
837
  ];
763
838
  const params = [now, now, options.runnerId, options.runnerId];
@@ -843,7 +918,7 @@ export class SqliteControllerStore {
843
918
  return false;
844
919
  const pending = this.state.connection
845
920
  .prepare(`SELECT 1 FROM interactive_requests
846
- WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
921
+ WHERE run_id = ? AND status = 'pending' LIMIT 1`)
847
922
  .get(options.runId);
848
923
  if (pending === undefined)
849
924
  return false;
@@ -879,7 +954,7 @@ export class SqliteControllerStore {
879
954
  return false;
880
955
  const pending = this.state.connection
881
956
  .prepare(`SELECT 1 FROM interactive_requests
882
- WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
957
+ WHERE run_id = ? AND status = 'pending' LIMIT 1`)
883
958
  .get(options.runId);
884
959
  if (pending === undefined)
885
960
  return false;
@@ -1139,187 +1214,6 @@ export class SqliteControllerStore {
1139
1214
  payload: row.payloadHash === null ? {} : this.state.readJson(row.payloadHash),
1140
1215
  }));
1141
1216
  }
1142
- ensureWorkflowTurnIntent(options) {
1143
- const existing = this.turnIntentBySource(options.sourceEventId);
1144
- if (existing !== undefined)
1145
- return existing;
1146
- const now = epoch(validTimestamp(options.requestedAt));
1147
- const run = this.requireWorkflowRunRow(options.runId);
1148
- const intentId = options.intentId;
1149
- const resourceId = resourceIdFor("turn_intent", intentId);
1150
- const effectId = `effect-${randomUUID()}`;
1151
- this.state.transaction(() => {
1152
- const factsHash = this.state.putJson(options.fallbackFacts, now);
1153
- this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "turn.deliver", intentId, factsHash, "run", now);
1154
- this.state.connection
1155
- .prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'turn_intent', ?, 1, ?, ?)")
1156
- .run(resourceId, intentId, now, now);
1157
- this.state.connection
1158
- .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
1159
- .run(resourceId);
1160
- this.state.connection
1161
- .prepare(`INSERT INTO turn_intents(
1162
- turn_intent_id, resource_id, effect_id, source_event_id, run_id,
1163
- workflow_ref, target_session_id, cause, node_id, attempt_id,
1164
- facts_hash, requested_at, eligible_at, created_at
1165
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1166
- .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);
1167
- this.insertEvent(resourceId, 1, "turn.requested", "session", options.targetSessionId, {}, now);
1168
- });
1169
- return this.requireTurnIntent(intentId);
1170
- }
1171
- getWorkflowTurnIntent(intentId) {
1172
- return this.turnIntent(intentId);
1173
- }
1174
- findPendingWorkflowTurnIntent(options) {
1175
- const row = this.state.connection
1176
- .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"))
1177
- .get(options.runId, options.targetSessionId);
1178
- return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
1179
- }
1180
- isWorkflowTurnIntentClaimLive(options) {
1181
- const row = this.turnIntentRow(options.intentId);
1182
- return (row !== undefined &&
1183
- row.targetSessionId === options.targetSessionId &&
1184
- row.resolvedAt === null &&
1185
- this.verifyEffectToken(row.effectId, options.claimToken, options.now));
1186
- }
1187
- claimWorkflowTurnIntent(options) {
1188
- const intent = this.turnIntent(options.intentId);
1189
- if (intent?.targetSessionId !== options.targetSessionId)
1190
- return undefined;
1191
- return this.claimEffectForTurn(options.intentId, options.claimToken, options.leaseMs, options.now)
1192
- ? this.requireTurnIntent(options.intentId)
1193
- : undefined;
1194
- }
1195
- claimEligibleWorkflowTurnIntents(options) {
1196
- const now = epoch(validTimestamp(options.now));
1197
- const rows = this.state.connection
1198
- .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 ?"))
1199
- .all(options.targetSessionId, now, options.limit ?? 10);
1200
- const claimed = [];
1201
- for (const row of rows) {
1202
- if (isTurnIntentRow(row) &&
1203
- this.claimEffectForTurn(row.intentId, options.claimToken, options.leaseMs, options.now)) {
1204
- claimed.push(this.requireTurnIntent(row.intentId));
1205
- }
1206
- }
1207
- return claimed;
1208
- }
1209
- makeWorkflowTurnIntentEligible(options) {
1210
- const eligibleAt = epoch(validTimestamp(options.eligibleAt ?? options.now));
1211
- const factsHash = this.state.putJson(options.fallbackFacts, eligibleAt);
1212
- return (this.state.connection
1213
- .prepare("UPDATE turn_intents SET eligible_at = ?, facts_hash = ? WHERE turn_intent_id = ? AND resolved_at IS NULL")
1214
- .run(eligibleAt, factsHash, options.intentId).changes === 1);
1215
- }
1216
- resolveWorkflowTurnIntent(options) {
1217
- const now = epoch(validTimestamp(options.now));
1218
- return this.state.transaction(() => {
1219
- const row = this.turnIntentRow(options.intentId);
1220
- if (row === undefined ||
1221
- row.targetSessionId !== options.targetSessionId ||
1222
- !this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
1223
- return false;
1224
- }
1225
- const result = this.state.connection
1226
- .prepare(`UPDATE turn_intents SET resolved_at = ?, resolution_type = ?, resolution_message_id = ?
1227
- WHERE turn_intent_id = ? AND resolved_at IS NULL`)
1228
- .run(now, options.resolution, options.messageId ?? null, options.intentId);
1229
- if (result.changes === 1)
1230
- this.completeEffect(row.effectId, "applied", now);
1231
- return result.changes === 1;
1232
- });
1233
- }
1234
- releaseWorkflowTurnIntentClaim(options) {
1235
- const row = this.turnIntentRow(options.intentId);
1236
- return (row !== undefined &&
1237
- row.targetSessionId === options.targetSessionId &&
1238
- this.releaseEffectLease(row.effectId, options.claimToken));
1239
- }
1240
- listWorkflowTurnIntents(options = {}) {
1241
- const clauses = ["1 = 1"];
1242
- const params = [];
1243
- if (options.runId !== undefined) {
1244
- clauses.push("t.run_id = ?");
1245
- params.push(options.runId);
1246
- }
1247
- if (options.targetSessionId !== undefined) {
1248
- clauses.push("t.target_session_id = ?");
1249
- params.push(options.targetSessionId);
1250
- }
1251
- if (options.unresolvedOnly === true)
1252
- clauses.push("t.resolved_at IS NULL");
1253
- params.push(options.limit ?? 100);
1254
- const rows = this.state.connection
1255
- .prepare(turnIntentSelect(`WHERE ${clauses.join(" AND ")} ORDER BY t.requested_at DESC LIMIT ?`))
1256
- .all(...params);
1257
- return rows.filter(isTurnIntentRow).map((row) => this.mapTurnIntent(row));
1258
- }
1259
- enqueueWorkflowNotification(options) {
1260
- const existing = this.notification(options.runId, options.attemptId, options.notificationIndex);
1261
- if (existing !== undefined)
1262
- return existing;
1263
- const run = this.requireWorkflowRunRow(options.runId);
1264
- const now = epoch(validTimestamp(options.now));
1265
- const notificationId = `notification-${randomUUID()}`;
1266
- const resourceId = resourceIdFor("notification", notificationId);
1267
- const effectId = `effect-${randomUUID()}`;
1268
- this.state.transaction(() => {
1269
- const contentHash = this.state.putText(options.content, now);
1270
- this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "notification.deliver", notificationId, contentHash, "run", now);
1271
- this.state.connection
1272
- .prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'notification', ?, 1, ?, ?)")
1273
- .run(resourceId, notificationId, now, now);
1274
- this.state.connection
1275
- .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
1276
- .run(resourceId);
1277
- this.state.connection
1278
- .prepare(`INSERT INTO notifications(
1279
- notification_id, effect_id, run_id, attempt_id, notification_index,
1280
- target_session_id, notification_type, content_hash, created_at
1281
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1282
- .run(notificationId, effectId, options.runId, options.attemptId, options.notificationIndex, options.targetSessionId, options.kind, contentHash, now);
1283
- this.insertEvent(resourceId, 1, "notification.queued", "session", options.targetSessionId, {}, now);
1284
- });
1285
- return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
1286
- }
1287
- listPendingWorkflowNotifications(options) {
1288
- const now = Date.now();
1289
- return this.notificationRows(options.targetSessionId, options.limit ?? 20)
1290
- .map((row) => this.mapNotification(row))
1291
- .filter((notification) => notification.deliveryClaimExpiresAt === null ||
1292
- Date.parse(notification.deliveryClaimExpiresAt) <= now);
1293
- }
1294
- claimPendingWorkflowNotifications(options) {
1295
- const rows = this.notificationRows(options.targetSessionId, options.limit ?? 20);
1296
- const result = [];
1297
- for (const row of rows) {
1298
- if (this.claimEffect(row.effectId, options.targetSessionId, options.claimToken, options.leaseMs, options.now)) {
1299
- result.push(this.mapNotification(row));
1300
- }
1301
- }
1302
- return result;
1303
- }
1304
- isWorkflowNotificationClaimLive(options) {
1305
- const row = this.notificationRowById(options.notificationId);
1306
- return (row !== undefined &&
1307
- row.targetSessionId === options.targetSessionId &&
1308
- this.verifyEffectToken(row.effectId, options.claimToken, options.now));
1309
- }
1310
- markWorkflowNotificationDelivered(options) {
1311
- const now = epoch(validTimestamp(options.now));
1312
- return this.state.transaction(() => {
1313
- const row = this.notificationRowById(options.notificationId);
1314
- if (row === undefined ||
1315
- row.targetSessionId !== options.targetSessionId ||
1316
- !this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
1317
- return false;
1318
- }
1319
- this.completeEffect(row.effectId, "applied", now);
1320
- return true;
1321
- });
1322
- }
1323
1217
  settleRunEffect(runId, effectType) {
1324
1218
  const rows = this.state.connection
1325
1219
  .prepare(`SELECT e.effect_id AS effectId, e.resource_id AS resourceId
@@ -1663,6 +1557,17 @@ export class SqliteControllerStore {
1663
1557
  return this.mapWorkflowRun(this.requireWorkflowRunRow(runId));
1664
1558
  }
1665
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);
1666
1571
  const compatible = row.workflowName === options.workflowName &&
1667
1572
  row.workflowRef === options.workflowSourceRef &&
1668
1573
  row.definitionDigest.equals(definitionDigest) &&
@@ -1673,11 +1578,24 @@ export class SqliteControllerStore {
1673
1578
  canonicalJson(options.launchOptions ?? {}) &&
1674
1579
  row.originSessionId === options.originSessionId &&
1675
1580
  row.executionMode === (options.executionMode ?? "interactive") &&
1676
- 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)));
1677
1589
  if (!compatible) {
1678
1590
  throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
1679
1591
  }
1680
1592
  }
1593
+ workflowRunViewRows(clause, params) {
1594
+ return this.state.connection
1595
+ .prepare(workflowRunViewSelect(clause))
1596
+ .all(...params)
1597
+ .filter(isWorkflowRunViewRow);
1598
+ }
1681
1599
  /* istanbul ignore next -- pure projection covered by integration tests */
1682
1600
  mapWorkflowRun(row) {
1683
1601
  return {
@@ -1698,6 +1616,10 @@ export class SqliteControllerStore {
1698
1616
  originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
1699
1617
  executionMode: row.executionMode,
1700
1618
  parentRunId: row.parentRunId,
1619
+ rootRunId: row.rootRunId,
1620
+ lineageKind: row.lineageKind,
1621
+ restartNumber: row.restartNumber,
1622
+ parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
1701
1623
  errorCode: row.errorCode,
1702
1624
  errorMessage: row.errorHash === null
1703
1625
  ? null
@@ -1718,7 +1640,7 @@ export class SqliteControllerStore {
1718
1640
  if (options.allowPendingInteraction !== true &&
1719
1641
  this.state.connection
1720
1642
  .prepare(`SELECT 1 FROM interactive_requests
1721
- WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
1643
+ WHERE run_id = ? AND status = 'pending' LIMIT 1`)
1722
1644
  .get(runId) !== undefined) {
1723
1645
  return undefined;
1724
1646
  }
@@ -1912,165 +1834,10 @@ export class SqliteControllerStore {
1912
1834
  .run(receiptHash, runId);
1913
1835
  this.state.connection
1914
1836
  .prepare(`UPDATE interactive_requests
1915
- SET status = 'cancelled', presenter_id = NULL,
1916
- presentation_claim_expires_at = NULL, revision = revision + 1, updated_at = ?
1917
- WHERE run_id = ? AND status IN ('pending', 'presenting')`)
1837
+ SET status = 'cancelled', revision = revision + 1, updated_at = ?
1838
+ WHERE run_id = ? AND status = 'pending'`)
1918
1839
  .run(now, runId);
1919
1840
  }
1920
- turnIntent(intentId) {
1921
- const row = this.turnIntentRow(intentId);
1922
- return row === undefined ? undefined : this.mapTurnIntent(row);
1923
- }
1924
- requireTurnIntent(intentId) {
1925
- const row = this.turnIntent(intentId);
1926
- if (row === undefined)
1927
- throw new Error(`Workflow turn intent not found: ${intentId}`);
1928
- return row;
1929
- }
1930
- turnIntentRow(intentId) {
1931
- const row = this.state.connection
1932
- .prepare(turnIntentSelect("WHERE t.turn_intent_id = ?"))
1933
- .get(intentId);
1934
- return isTurnIntentRow(row) ? row : undefined;
1935
- }
1936
- turnIntentBySource(sourceEventId) {
1937
- const row = this.state.connection
1938
- .prepare(turnIntentSelect("WHERE t.source_event_id = ?"))
1939
- .get(sourceEventId);
1940
- return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
1941
- }
1942
- /* istanbul ignore next -- pure projection covered by integration tests */
1943
- mapTurnIntent(row) {
1944
- return {
1945
- intentId: row.intentId,
1946
- sourceEventId: row.sourceEventId,
1947
- runId: row.runId,
1948
- workflowRef: row.workflowRef,
1949
- targetSessionId: row.targetSessionId,
1950
- cause: row.cause,
1951
- nodeId: row.nodeId,
1952
- attemptId: row.attemptId,
1953
- fallbackFacts: this.state.readJson(row.factsHash),
1954
- requestedAt: new Date(row.requestedAt).toISOString(),
1955
- eligibleAt: row.eligibleAt === null ? null : new Date(row.eligibleAt).toISOString(),
1956
- resolvedAt: row.resolvedAt === null ? null : new Date(row.resolvedAt).toISOString(),
1957
- resolution: row.resolution,
1958
- resolutionMessageId: row.resolutionMessageId,
1959
- deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
1960
- };
1961
- }
1962
- claimEffectForTurn(intentId, token, leaseMs, nowValue) {
1963
- const row = this.turnIntentRow(intentId);
1964
- return (row !== undefined &&
1965
- this.claimEffect(row.effectId, row.targetSessionId, token, leaseMs, nowValue));
1966
- }
1967
- claimEffect(effectId, ownerId, token, leaseMs, nowValue) {
1968
- const now = epoch(validTimestamp(nowValue));
1969
- return this.state.transaction(() => {
1970
- const row = this.state.connection
1971
- .prepare("SELECT resource_id AS resourceId FROM effects WHERE effect_id = ? AND status = 'pending'")
1972
- .get(effectId);
1973
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
1974
- if (!isResourceIdRow(row))
1975
- return false;
1976
- const lease = this.requireLease(row.resourceId);
1977
- if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
1978
- return false;
1979
- return (this.state.connection
1980
- .prepare(`UPDATE leases SET generation = ?, owner_type = 'session', owner_id = ?, token_hash = ?,
1981
- acquired_at = ?, heartbeat_at = ?, expires_at = ?
1982
- WHERE resource_id = ? AND generation = ?`)
1983
- .run(lease.generation + 1, ownerId, tokenHash(token), now, now, now + leaseMs, row.resourceId, lease.generation).changes === 1);
1984
- });
1985
- }
1986
- verifyEffectToken(effectId, token, nowValue) {
1987
- const now = epoch(validTimestamp(nowValue));
1988
- const row = this.state.connection
1989
- .prepare(`SELECT l.token_hash AS tokenHash, l.expires_at AS expiresAt
1990
- FROM effects e JOIN leases l ON l.resource_id = e.resource_id
1991
- WHERE e.effect_id = ?`)
1992
- .get(effectId);
1993
- return (isEffectTokenRow(row) &&
1994
- row.tokenHash !== null &&
1995
- row.tokenHash.equals(tokenHash(token)) &&
1996
- row.expiresAt !== null &&
1997
- row.expiresAt > now);
1998
- }
1999
- releaseEffectLease(effectId, token) {
2000
- return (this.state.connection
2001
- .prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
2002
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
2003
- WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)
2004
- AND token_hash = ?`)
2005
- .run(effectId, tokenHash(token)).changes === 1);
2006
- }
2007
- completeEffect(effectId, status, now) {
2008
- this.state.connection
2009
- .prepare("UPDATE effects SET status = ?, updated_at = ?, settled_at = ? WHERE effect_id = ?")
2010
- .run(status, now, now, effectId);
2011
- this.state.connection
2012
- .prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
2013
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
2014
- WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)`)
2015
- .run(effectId);
2016
- }
2017
- insertEffectResource(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now) {
2018
- this.state.connection
2019
- .prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'effect', ?, 1, ?, ?)")
2020
- .run(resourceId, effectId, now, now);
2021
- this.state.connection
2022
- .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
2023
- .run(resourceId);
2024
- this.state.connection
2025
- .prepare(`INSERT INTO effects(
2026
- effect_id, resource_id, source_resource_id, source_revision, effect_type,
2027
- idempotency_key, payload_hash, owner_scope, status, attempt_count,
2028
- created_at, updated_at
2029
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)`)
2030
- .run(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now, now);
2031
- this.insertEvent(resourceId, 1, "effect.created", "system", null, {}, now);
2032
- }
2033
- notification(runId, attemptId, index) {
2034
- const row = this.state.connection
2035
- .prepare(notificationSelect("WHERE n.run_id = ? AND n.attempt_id = ? AND n.notification_index = ?"))
2036
- .get(runId, attemptId, index);
2037
- return isNotificationRow(row) ? this.mapNotification(row) : undefined;
2038
- }
2039
- requireNotification(runId, attemptId, index) {
2040
- const record = this.notification(runId, attemptId, index);
2041
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
2042
- if (record === undefined)
2043
- throw new Error("Workflow notification was not stored");
2044
- return record;
2045
- }
2046
- notificationRows(sessionId, limit) {
2047
- return this.state.connection
2048
- .prepare(notificationSelect("WHERE n.target_session_id = ? AND e.status = 'pending' ORDER BY n.created_at LIMIT ?"))
2049
- .all(sessionId, limit)
2050
- .filter(isNotificationRow);
2051
- }
2052
- notificationRowById(id) {
2053
- const row = this.state.connection
2054
- .prepare(notificationSelect("WHERE n.notification_id = ?"))
2055
- .get(id);
2056
- return isNotificationRow(row) ? row : undefined;
2057
- }
2058
- /* istanbul ignore next -- pure projection covered by integration tests */
2059
- mapNotification(row) {
2060
- return {
2061
- notificationId: row.notificationId,
2062
- runId: row.runId,
2063
- nodeId: row.nodeId,
2064
- attemptId: row.attemptId,
2065
- notificationIndex: row.notificationIndex,
2066
- targetSessionId: row.targetSessionId,
2067
- kind: row.kind,
2068
- content: this.state.readBlob(row.contentHash)?.content.toString("utf8") ?? "",
2069
- createdAt: new Date(row.createdAt).toISOString(),
2070
- deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
2071
- deliveredAt: row.deliveredAt === null ? null : new Date(row.deliveredAt).toISOString(),
2072
- };
2073
- }
2074
1841
  assertWritable() {
2075
1842
  if (this.state.mode === "read-only")
2076
1843
  throw new Error("Controller store is read-only");
@@ -2112,6 +1879,9 @@ function workflowRunSelect(clause) {
2112
1879
  q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
2113
1880
  q.error_hash AS errorHash, b.origin_session_id AS originSessionId,
2114
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,
2115
1885
  q.created_at AS createdAt, q.updated_at AS updatedAt,
2116
1886
  q.started_at AS startedAt, q.finished_at AS finishedAt,
2117
1887
  l.generation AS leaseGeneration, l.owner_id AS ownerId, l.expires_at AS claimExpiresAt
@@ -2120,28 +1890,24 @@ function workflowRunSelect(clause) {
2120
1890
  LEFT JOIN run_bindings b ON b.run_id = r.run_id
2121
1891
  JOIN leases l ON l.resource_id = r.resource_id ${clause}`;
2122
1892
  }
2123
- function turnIntentSelect(clause) {
2124
- return `SELECT t.turn_intent_id AS intentId, t.source_event_id AS sourceEventId,
2125
- t.run_id AS runId, t.workflow_ref AS workflowRef,
2126
- t.target_session_id AS targetSessionId, t.cause,
2127
- t.node_id AS nodeId, t.attempt_id AS attemptId, t.facts_hash AS factsHash,
2128
- t.requested_at AS requestedAt, t.eligible_at AS eligibleAt,
2129
- t.resolved_at AS resolvedAt, t.resolution_type AS resolution,
2130
- t.resolution_message_id AS resolutionMessageId, l.expires_at AS claimExpiresAt,
2131
- t.effect_id AS effectId
2132
- FROM turn_intents t JOIN effects e ON e.effect_id = t.effect_id
2133
- JOIN leases l ON l.resource_id = e.resource_id ${clause}`;
2134
- }
2135
- function notificationSelect(clause) {
2136
- return `SELECT n.notification_id AS notificationId, n.effect_id AS effectId,
2137
- n.run_id AS runId, a.node_id AS nodeId, n.attempt_id AS attemptId,
2138
- n.notification_index AS notificationIndex, n.target_session_id AS targetSessionId,
2139
- n.notification_type AS kind, n.content_hash AS contentHash,
2140
- n.created_at AS createdAt, l.expires_at AS claimExpiresAt,
2141
- e.settled_at AS deliveredAt
2142
- FROM notifications n JOIN effects e ON e.effect_id = n.effect_id
2143
- JOIN leases l ON l.resource_id = e.resource_id
2144
- JOIN node_attempts a ON a.attempt_id = n.attempt_id ${clause}`;
1893
+ function workflowRunViewSelect(clause) {
1894
+ return `SELECT r.run_id AS runId, d.workflow_name AS workflowName,
1895
+ r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
1896
+ r.definition_digest AS definitionDigest, q.status,
1897
+ b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
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,
1902
+ q.created_at AS createdAt, q.updated_at AS updatedAt,
1903
+ q.started_at AS startedAt, q.finished_at AS finishedAt,
1904
+ source.source_type AS sourceType, source.source_ref AS sourceRef,
1905
+ source.source_revision AS sourceRevision
1906
+ FROM runs r JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
1907
+ JOIN run_queue q ON q.run_id = r.run_id
1908
+ LEFT JOIN run_bindings b ON b.run_id = r.run_id
1909
+ LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
1910
+ LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
2145
1911
  }
2146
1912
  function isRecord(value) {
2147
1913
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2152,6 +1918,70 @@ function isControllerRow(value) {
2152
1918
  function isLeaseRow(value) {
2153
1919
  return isRecord(value);
2154
1920
  }
1921
+ function workflowRunViewRecord(row) {
1922
+ const root = row.sourceType === "builtin"
1923
+ ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
1924
+ : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
1925
+ return {
1926
+ runId: row.runId,
1927
+ workflowName: row.workflowName,
1928
+ workflowSourceRef: row.workflowRef,
1929
+ workflowSource: { root, mounted: [] },
1930
+ initialized: row.runStatus !== "queued",
1931
+ definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
1932
+ runStateStatus: row.runStatus,
1933
+ paused: row.paused === 1,
1934
+ status: row.status,
1935
+ originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
1936
+ executionMode: row.executionMode,
1937
+ parentRunId: row.parentRunId,
1938
+ rootRunId: row.rootRunId,
1939
+ lineageKind: row.lineageKind,
1940
+ restartNumber: row.restartNumber,
1941
+ parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
1942
+ errorCode: row.errorCode,
1943
+ errorMessage: row.errorMessage,
1944
+ createdAt: new Date(row.createdAt).toISOString(),
1945
+ updatedAt: new Date(row.updatedAt).toISOString(),
1946
+ startedAt: row.startedAt === null ? null : new Date(row.startedAt).toISOString(),
1947
+ finishedAt: row.finishedAt === null ? null : new Date(row.finishedAt).toISOString(),
1948
+ };
1949
+ }
1950
+ function isWorkflowRunViewRow(value) {
1951
+ return (isRecord(value) &&
1952
+ typeof value.runId === "string" &&
1953
+ typeof value.workflowName === "string" &&
1954
+ typeof value.workflowRef === "string" &&
1955
+ typeof value.runStatus === "string" &&
1956
+ typeof value.paused === "number" &&
1957
+ Buffer.isBuffer(value.definitionDigest) &&
1958
+ typeof value.status === "string" &&
1959
+ (value.originSessionId === null || typeof value.originSessionId === "string") &&
1960
+ (value.executionMode === "interactive" || value.executionMode === "headless") &&
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)) &&
1969
+ (value.errorCode === null || typeof value.errorCode === "string") &&
1970
+ (value.errorMessage === null || typeof value.errorMessage === "string") &&
1971
+ typeof value.createdAt === "number" &&
1972
+ typeof value.updatedAt === "number" &&
1973
+ (value.startedAt === null || typeof value.startedAt === "number") &&
1974
+ (value.finishedAt === null || typeof value.finishedAt === "number") &&
1975
+ (value.sourceType === "builtin" || value.sourceType === "file") &&
1976
+ typeof value.sourceRef === "string" &&
1977
+ typeof value.sourceRevision === "string");
1978
+ }
1979
+ function isRunListRevisionRow(value) {
1980
+ return (isRecord(value) &&
1981
+ typeof value.count === "number" &&
1982
+ typeof value.revisionSum === "number" &&
1983
+ typeof value.updatedAt === "number");
1984
+ }
2155
1985
  function isRunRow(value) {
2156
1986
  return isRecord(value);
2157
1987
  }
@@ -2183,12 +2013,6 @@ function isControllerEventRow(value) {
2183
2013
  function isRunEventRow(value) {
2184
2014
  return isRecord(value);
2185
2015
  }
2186
- function isTurnIntentRow(value) {
2187
- return isRecord(value);
2188
- }
2189
- function isNotificationRow(value) {
2190
- return isRecord(value);
2191
- }
2192
2016
  function isProjectRow(value) {
2193
2017
  return isRecord(value);
2194
2018
  }
@@ -2213,12 +2037,6 @@ function isSequenceRow(value) {
2213
2037
  function isKeyRow(value) {
2214
2038
  return isRecord(value);
2215
2039
  }
2216
- function isResourceIdRow(value) {
2217
- return isRecord(value);
2218
- }
2219
- function isEffectTokenRow(value) {
2220
- return isRecord(value);
2221
- }
2222
2040
  function isEffectIdentityRow(value) {
2223
2041
  return isRecord(value);
2224
2042
  }
@@ -2242,6 +2060,12 @@ function digestBuffer(value) {
2242
2060
  throw new Error("Expected a SHA-256 digest");
2243
2061
  return Buffer.from(hex, "hex");
2244
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
+ }
2245
2069
  function queuedWorkflowSource(value) {
2246
2070
  if (isWorkflowSource(value)) {
2247
2071
  return { root: value, mounted: [] };