@osolmaz/pi-workflows 0.16.5 → 0.16.6

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 (171) hide show
  1. package/README.md +5 -5
  2. package/dist/builtins/autoimplement.workflow.d.ts +5 -1
  3. package/dist/builtins/autoimplement.workflow.js +29 -10
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/client/client.js +3 -1
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/client/protocol.d.ts +1 -1
  8. package/dist/client/protocol.js +18 -1
  9. package/dist/client/protocol.js.map +1 -1
  10. package/dist/client/view.d.ts +7 -5
  11. package/dist/extension/index.d.ts +2 -1
  12. package/dist/extension/index.js +115 -165
  13. package/dist/extension/index.js.map +1 -1
  14. package/dist/extension/recorder.d.ts +1 -1
  15. package/dist/extension/recorder.js +2 -2
  16. package/dist/extension/recorder.js.map +1 -1
  17. package/dist/extension/step-message.d.ts +1 -1
  18. package/dist/extension/step-message.js +4 -4
  19. package/dist/extension/step-message.js.map +1 -1
  20. package/dist/extension/workflow-message-coordinator.d.ts +12 -1
  21. package/dist/extension/workflow-message-coordinator.js +65 -54
  22. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  23. package/dist/resource-managers/index.d.ts +1 -2
  24. package/dist/resource-managers/index.js +1 -2
  25. package/dist/resource-managers/index.js.map +1 -1
  26. package/dist/resource-managers/sqlite.d.ts +2 -295
  27. package/dist/resource-managers/sqlite.js +71 -1528
  28. package/dist/resource-managers/sqlite.js.map +1 -1
  29. package/dist/resource-managers/types.d.ts +2 -7
  30. package/dist/server/resource-runner-entry.js +2 -6
  31. package/dist/server/resource-runner-entry.js.map +1 -1
  32. package/dist/server/rpc-bridge.js +1 -1
  33. package/dist/server/rpc-bridge.js.map +1 -1
  34. package/dist/server/rpc-executor.js +2 -4
  35. package/dist/server/rpc-executor.js.map +1 -1
  36. package/dist/server/server-entry.js +3 -0
  37. package/dist/server/server-entry.js.map +1 -1
  38. package/dist/server/server.d.ts +9 -8
  39. package/dist/server/server.js +364 -541
  40. package/dist/server/server.js.map +1 -1
  41. package/dist/server/state.d.ts +16 -29
  42. package/dist/server/state.js +105 -319
  43. package/dist/server/state.js.map +1 -1
  44. package/dist/server/view.d.ts +4 -5
  45. package/dist/server/view.js +51 -51
  46. package/dist/server/view.js.map +1 -1
  47. package/dist/server/workflow-runner-entry.d.ts +1 -1
  48. package/dist/server/workflow-runner-entry.js +1 -49
  49. package/dist/server/workflow-runner-entry.js.map +1 -1
  50. package/dist/server/workflow-runner-protocol.d.ts +2 -7
  51. package/dist/server/workflow-runner-protocol.js +5 -10
  52. package/dist/server/workflow-runner-protocol.js.map +1 -1
  53. package/dist/server/workflow-runner-store.d.ts +5 -6
  54. package/dist/server/workflow-runner-store.js +7 -14
  55. package/dist/server/workflow-runner-store.js.map +1 -1
  56. package/dist/state/attempt-time.d.ts +21 -0
  57. package/dist/state/attempt-time.js +95 -0
  58. package/dist/state/attempt-time.js.map +1 -0
  59. package/dist/state/json.d.ts +2 -1
  60. package/dist/state/json.js.map +1 -1
  61. package/dist/state/project-store.d.ts +39 -0
  62. package/dist/state/project-store.js +154 -0
  63. package/dist/state/project-store.js.map +1 -0
  64. package/dist/state/prune.js +0 -7
  65. package/dist/state/prune.js.map +1 -1
  66. package/dist/state/schema.js +23 -23
  67. package/dist/state/workflow-messages.d.ts +1 -1
  68. package/dist/state/workflow-messages.js +3 -1
  69. package/dist/state/workflow-messages.js.map +1 -1
  70. package/dist/viewer/backup.js +5 -0
  71. package/dist/viewer/backup.js.map +1 -1
  72. package/dist/workflows/diagnostics.d.ts +8 -0
  73. package/dist/workflows/diagnostics.js +61 -0
  74. package/dist/workflows/diagnostics.js.map +1 -0
  75. package/dist/workflows/engine.d.ts +2 -15
  76. package/dist/workflows/engine.js +125 -245
  77. package/dist/workflows/engine.js.map +1 -1
  78. package/dist/workflows/human-decision.d.ts +3 -7
  79. package/dist/workflows/human-decision.js +2 -56
  80. package/dist/workflows/human-decision.js.map +1 -1
  81. package/dist/workflows/index.d.ts +2 -1
  82. package/dist/workflows/index.js +1 -0
  83. package/dist/workflows/index.js.map +1 -1
  84. package/dist/workflows/queue.d.ts +267 -0
  85. package/dist/workflows/queue.js +1248 -0
  86. package/dist/workflows/queue.js.map +1 -0
  87. package/dist/workflows/requests.d.ts +47 -0
  88. package/dist/workflows/requests.js +158 -0
  89. package/dist/workflows/requests.js.map +1 -0
  90. package/dist/workflows/schema.js +2 -4
  91. package/dist/workflows/schema.js.map +1 -1
  92. package/dist/workflows/store.d.ts +11 -16
  93. package/dist/workflows/store.js +174 -169
  94. package/dist/workflows/store.js.map +1 -1
  95. package/dist/workflows/tool-input.d.ts +6 -9
  96. package/dist/workflows/tool-input.js +15 -12
  97. package/dist/workflows/tool-input.js.map +1 -1
  98. package/dist/workflows/transitions.d.ts +38 -0
  99. package/dist/workflows/transitions.js +276 -0
  100. package/dist/workflows/transitions.js.map +1 -0
  101. package/dist/workflows/types.d.ts +9 -42
  102. package/dist/workflows/workflow-message-content.d.ts +7 -1
  103. package/dist/workflows/workflow-message-content.js +22 -4
  104. package/dist/workflows/workflow-message-content.js.map +1 -1
  105. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  106. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  107. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  108. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  109. package/docs/DEFERRED_TURNS.md +84 -105
  110. package/docs/HUMAN_DECISIONS.md +17 -19
  111. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  112. package/docs/SQLITE_STATE.md +68 -29
  113. package/docs/WORKFLOW_SERVER.md +21 -10
  114. package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
  115. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  116. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  117. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  118. package/docs/workflows.md +94 -45
  119. package/examples/workflows/autoresearch.workflow.ts +0 -2
  120. package/examples/workflows/branch.workflow.ts +0 -4
  121. package/examples/workflows/echo.workflow.ts +0 -2
  122. package/examples/workflows/live-settings.workflow.ts +0 -2
  123. package/examples/workflows/two-turn.workflow.ts +0 -2
  124. package/herdr-plugin.toml +1 -1
  125. package/package.json +1 -1
  126. package/protocol/fixtures/client-v1.json +1 -1
  127. package/skills/pi-workflows/SKILL.md +8 -7
  128. package/src/builtins/autoimplement.workflow.ts +31 -10
  129. package/src/client/client.ts +3 -1
  130. package/src/client/protocol.ts +21 -1
  131. package/src/client/view.ts +9 -5
  132. package/src/extension/index.ts +117 -189
  133. package/src/extension/recorder.ts +2 -2
  134. package/src/extension/step-message.ts +4 -4
  135. package/src/extension/workflow-message-coordinator.ts +78 -50
  136. package/src/resource-managers/index.ts +1 -9
  137. package/src/resource-managers/sqlite.ts +330 -2655
  138. package/src/resource-managers/types.ts +2 -3
  139. package/src/server/resource-runner-entry.ts +2 -7
  140. package/src/server/rpc-bridge.ts +1 -1
  141. package/src/server/rpc-executor.ts +4 -9
  142. package/src/server/server-entry.ts +3 -0
  143. package/src/server/server.ts +416 -641
  144. package/src/server/state.ts +122 -466
  145. package/src/server/view.ts +56 -60
  146. package/src/server/workflow-runner-entry.ts +2 -55
  147. package/src/server/workflow-runner-protocol.ts +5 -15
  148. package/src/server/workflow-runner-store.ts +12 -27
  149. package/src/state/attempt-time.ts +122 -0
  150. package/src/state/json.ts +2 -1
  151. package/src/state/project-store.ts +218 -0
  152. package/src/state/prune.ts +0 -13
  153. package/src/state/schema.ts +23 -23
  154. package/src/state/workflow-messages.ts +4 -2
  155. package/src/viewer/backup.ts +7 -0
  156. package/src/workflows/diagnostics.ts +78 -0
  157. package/src/workflows/engine.ts +133 -291
  158. package/src/workflows/human-decision.ts +3 -81
  159. package/src/workflows/index.ts +8 -2
  160. package/src/workflows/queue.ts +2091 -0
  161. package/src/workflows/requests.ts +283 -0
  162. package/src/workflows/schema.ts +2 -6
  163. package/src/workflows/store.ts +223 -231
  164. package/src/workflows/tool-input.ts +20 -12
  165. package/src/workflows/transitions.ts +330 -0
  166. package/src/workflows/types.ts +9 -46
  167. package/src/workflows/workflow-message-content.ts +28 -4
  168. package/dist/resource-managers/runtime.d.ts +0 -59
  169. package/dist/resource-managers/runtime.js +0 -423
  170. package/dist/resource-managers/runtime.js.map +0 -1
  171. package/src/resource-managers/runtime.ts +0 -576
@@ -1,46 +1,77 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { StateDatabase, workflowStatePath } from "../state/database.js";
5
- import { canonicalJson } from "../state/json.js";
6
2
  import { resourceIdFor, tokenHash } from "../state/mutation.js";
7
- import { initializeViewerRun, recordViewerDeltas } from "../state/viewer.js";
8
- import { WorkflowMessageStore } from "../state/workflow-messages.js";
3
+ import { ProjectStore, isRecord, validTimestamp, epoch, isSequenceRow, } from "../state/project-store.js";
9
4
  import { EffectRequestConflictError, ManagedResourceConflictError, ManagedResourceNotFoundError, WorkflowRequestConflictError, } from "./errors.js";
10
- export class SqliteResourceManagerStore {
11
- filePath;
12
- state;
13
- workflowMessages;
14
- ownsState;
15
- projectId;
16
- closed = false;
17
- constructor(filePath = workflowStatePath(), options = {}) {
18
- this.ownsState = options.state === undefined;
19
- this.state =
20
- options.state ??
21
- new StateDatabase({
22
- filePath,
23
- mode: options.readOnly === true ? "read-only" : "read-write",
24
- checkLegacyState: filePath === workflowStatePath(),
25
- });
26
- this.filePath = this.state.filePath;
27
- this.workflowMessages = new WorkflowMessageStore(this.state);
28
- if (options.global === true) {
29
- this.projectId = null;
30
- }
31
- else {
32
- const projectPath = options.projectPath === undefined ? process.cwd() : options.projectPath;
33
- this.projectId =
34
- options.readOnly === true ? this.findProject(projectPath) : this.ensureProject(projectPath);
35
- }
36
- }
37
- close() {
38
- if (this.closed)
39
- return;
40
- this.closed = true;
41
- if (this.ownsState)
42
- this.state.close();
43
- }
5
+ function resourceManagerSelect(clause) {
6
+ return `SELECT c.controller_resource_id AS managedResourceId,
7
+ c.resource_id AS resourceId, r.revision AS resourceVersion,
8
+ c.controller_name AS resourceManagerName, c.resource_key AS resourceKey,
9
+ c.uid, c.generation, c.spec_hash AS specHash, c.status_hash AS statusHash,
10
+ c.deletion_requested_at AS deletionRequestedAt
11
+ FROM controller_resources c JOIN resources r ON r.resource_id = c.resource_id ${clause}`;
12
+ }
13
+ function effectSelect(clause) {
14
+ return `SELECT e.effect_id AS effectId, c.uid AS resourceUid, c.generation,
15
+ e.effect_type AS kind, e.status AS state,
16
+ json_extract(CAST(b.content AS TEXT), '$.requestFingerprint') AS requestFingerprint,
17
+ e.created_at AS startedAt, e.settled_at AS completedAt,
18
+ e.external_ref AS externalRef, e.error_hash AS errorHash
19
+ FROM effects e
20
+ JOIN controller_resources c ON c.resource_id = e.source_resource_id
21
+ JOIN blobs b ON b.blob_hash = e.payload_hash ${clause}`;
22
+ }
23
+ function workflowSelect(clause) {
24
+ return `SELECT w.request_id AS requestId, c.uid AS resourceUid,
25
+ w.request_key AS requestKey, w.input_fingerprint AS inputFingerprint,
26
+ w.workflow_name AS workflowName, COALESCE(w.run_id, w.reserved_run_id) AS runId, w.status,
27
+ w.attempt_count AS attemptCount, w.error_hash AS errorHash
28
+ FROM controller_workflows w
29
+ JOIN controller_resources c ON c.controller_resource_id = w.controller_resource_id ${clause}`;
30
+ }
31
+ function isResourceManagerRow(value) {
32
+ return isRecord(value);
33
+ }
34
+ function isEffectRow(value) {
35
+ return isRecord(value);
36
+ }
37
+ function isWorkflowRow(value) {
38
+ return isRecord(value);
39
+ }
40
+ function isQueueListRow(value) {
41
+ return isRecord(value);
42
+ }
43
+ function isResourceManagerEventRow(value) {
44
+ return isRecord(value);
45
+ }
46
+ function isFinalizerRow(value) {
47
+ return isRecord(value);
48
+ }
49
+ function isErrorCountRow(value) {
50
+ return isRecord(value);
51
+ }
52
+ function isQueueVersionRow(value) {
53
+ return isRecord(value);
54
+ }
55
+ function isKeyRow(value) {
56
+ return isRecord(value);
57
+ }
58
+ function effectIdFor(sourceResourceId, key) {
59
+ return `effect-${createHash("sha256").update(`${sourceResourceId}\0${key}`).digest("hex").slice(0, 40)}`;
60
+ }
61
+ function effectStatus(value) {
62
+ return value === "indeterminate" ? "ambiguous" : value;
63
+ }
64
+ function resourceManagerEffectState(value) {
65
+ return value === "ambiguous" ? "indeterminate" : value;
66
+ }
67
+ function validateName(value, label) {
68
+ validateKey(value, label);
69
+ }
70
+ function validateKey(value, label) {
71
+ if (value.length === 0 || value.length > 512)
72
+ throw new Error(`${label} is invalid`);
73
+ }
74
+ export class SqliteResourceManagerStore extends ProjectStore {
44
75
  putResource(options) {
45
76
  this.assertWritable();
46
77
  validateName(options.resourceManager, "resource manager");
@@ -518,806 +549,6 @@ export class SqliteResourceManagerStore {
518
549
  payload: row.payloadHash === null ? {} : this.state.readJson(row.payloadHash),
519
550
  }));
520
551
  }
521
- reserveWorkflowRun(options) {
522
- validateRunId(options.runId);
523
- const now = epoch(validTimestamp(options.now));
524
- const definitionDigest = digestBuffer(options.definitionDigest);
525
- return this.state.transaction(() => this.reserveWorkflowRunInTransaction(options, now, definitionDigest));
526
- }
527
- reserveWorkflowRunInTransaction(options, now, definitionDigest) {
528
- if (this.getWorkflowRun(options.runId) !== undefined) {
529
- throw new Error(`Workflow run already reserved: ${options.runId}`);
530
- }
531
- const lineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
532
- if (options.parentRunId === undefined && options.lineageKind !== undefined) {
533
- throw new Error("A root workflow run cannot declare lineage");
534
- }
535
- let rootRunId = options.runId;
536
- let restartNumber = 0;
537
- let parentTerminalFingerprint = null;
538
- if (options.parentRunId !== undefined) {
539
- const parent = this.requireWorkflowRunRow(options.parentRunId);
540
- if (parent.originSessionId !== options.originSessionId) {
541
- throw new Error("Workflow parent belongs to another Pi session");
542
- }
543
- rootRunId = parent.rootRunId;
544
- if (lineageKind === "restart") {
545
- if (!["completed", "failed", "timed_out", "cancelled"].includes(parent.runStatus) ||
546
- !["done", "failed", "cancelled"].includes(parent.status)) {
547
- throw new Error("Restart parent is not terminal");
548
- }
549
- if (options.parentTerminalFingerprint === undefined) {
550
- throw new Error("Restart requires the parent terminal fingerprint");
551
- }
552
- parentTerminalFingerprint = terminalFingerprintBuffer(options.parentTerminalFingerprint);
553
- restartNumber = options.restartNumber ?? parent.restartNumber + 1;
554
- if (restartNumber !== parent.restartNumber + 1) {
555
- throw new Error("Restart number does not follow its parent");
556
- }
557
- }
558
- else {
559
- if (options.parentTerminalFingerprint !== undefined ||
560
- (options.restartNumber !== undefined && options.restartNumber !== parent.restartNumber)) {
561
- throw new Error("Continuation lineage conflicts with restart metadata");
562
- }
563
- restartNumber = parent.restartNumber;
564
- if (parent.status === "parked") {
565
- const parentLease = this.requireLease(parent.resourceId);
566
- if (parentLease.ownerId !== null) {
567
- throw new Error("Continuation parent still has an active owner");
568
- }
569
- this.state.connection
570
- .prepare(`UPDATE run_queue
571
- SET status = 'done', updated_at = ?, finished_at = ?
572
- WHERE run_id = ? AND status = 'parked'`)
573
- .run(now, now, parent.runId);
574
- const parentRevision = this.resourceRevision(parent.resourceId);
575
- this.bumpResource(parent.resourceId, parentRevision, now);
576
- this.insertEvent(parent.resourceId, parentRevision + 1, "run.queue_done_for_continuation", "session", options.originSessionId, { continuationRunId: options.runId }, now);
577
- }
578
- else if (parent.status === "done") {
579
- throw new Error("Continuation parent already has a reserved continuation");
580
- }
581
- else {
582
- throw new Error(`Continuation parent queue is ${parent.status}`);
583
- }
584
- }
585
- }
586
- else if (options.restartNumber !== undefined ||
587
- options.parentTerminalFingerprint !== undefined) {
588
- throw new Error("A root workflow run cannot declare restart metadata");
589
- }
590
- const resourceId = resourceIdFor("run", options.runId);
591
- const definitionHash = this.state.putJson(options.definitionSnapshot, now);
592
- const queuedSource = queuedWorkflowSource(options.workflowSource);
593
- const inputHash = this.state.putJson(options.input ?? null, now);
594
- const launchHash = this.state.putJson(options.launchOptions ?? {}, now);
595
- this.state.connection
596
- .prepare(`INSERT INTO workflow_definitions(
597
- definition_digest, workflow_name, definition_hash, created_at
598
- ) VALUES (?, ?, ?, ?)
599
- ON CONFLICT(definition_digest) DO NOTHING`)
600
- .run(definitionDigest, options.workflowName, definitionHash, now);
601
- this.state.connection
602
- .prepare(`INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at)
603
- VALUES (?, 'run', ?, 1, ?, ?)`)
604
- .run(resourceId, options.runId, now, now);
605
- this.state.connection
606
- .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
607
- .run(resourceId);
608
- this.state.connection
609
- .prepare(`INSERT INTO runs(
610
- run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
611
- restart_number, parent_terminal_fingerprint, definition_digest,
612
- workflow_ref, launch_options_hash, status, paused,
613
- input_hash, created_at, updated_at
614
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`)
615
- .run(options.runId, resourceId, this.requireProjectId(), options.parentRunId ?? null, rootRunId, lineageKind, restartNumber, parentTerminalFingerprint, definitionDigest, options.workflowSourceRef, launchHash, inputHash, now, now);
616
- initializeViewerRun(this.state, options.runId, now);
617
- insertQueuedRunSources(this.state, options.runId, queuedSource);
618
- this.state.connection
619
- .prepare(`INSERT INTO run_bindings(run_id, origin_session_id, execution_mode, created_at)
620
- VALUES (?, ?, ?, ?)`)
621
- .run(options.runId, options.originSessionId, options.executionMode ?? "interactive", now);
622
- this.state.connection
623
- .prepare(`INSERT INTO run_queue(
624
- run_id, status, available_at, affinity_runner_id, origin_session_id,
625
- consecutive_errors, created_at, updated_at
626
- ) VALUES (?, 'queued', ?, ?, ?, 0, ?, ?)`)
627
- .run(options.runId, now, options.runnerId, options.originSessionId, now, now);
628
- this.insertEvent(resourceId, 1, "run.queued", "session", options.originSessionId, {}, now);
629
- return this.requireWorkflowRun(options.runId);
630
- }
631
- prepareOrAdoptWorkflowRun(options) {
632
- validateRunId(options.runId);
633
- const now = epoch(validTimestamp(options.now));
634
- const definitionDigest = digestBuffer(options.definitionDigest);
635
- return this.state.transaction(() => {
636
- const existing = this.workflowRunRow(options.runId);
637
- if (existing !== undefined) {
638
- this.assertWorkflowRunPreparationCompatible(existing, options, definitionDigest);
639
- return { state: "adopted", run: this.mapWorkflowRun(existing) };
640
- }
641
- this.reserveWorkflowRunInTransaction(options, now, definitionDigest);
642
- const claimed = this.claimRunInTransaction(options.runId, options.runnerId, options.claimToken, options.leaseMs, now);
643
- if (claimed === undefined) {
644
- throw new Error(`Workflow run could not be claimed: ${options.runId}`);
645
- }
646
- return {
647
- state: "claimed",
648
- run: claimed,
649
- };
650
- });
651
- }
652
- enqueueWorkflowRun(options) {
653
- if (this.getWorkflowRun(options.runId) === undefined) {
654
- this.reserveWorkflowRun({
655
- runId: options.runId,
656
- workflowName: options.workflowName,
657
- workflowSourceRef: options.workflowSourceRef,
658
- workflowSource: options.workflowSource,
659
- definitionDigest: options.definitionDigest,
660
- definitionSnapshot: options.definitionSnapshot,
661
- input: options.input,
662
- launchOptions: options.launchOptions ?? {},
663
- runnerId: options.runnerId,
664
- originSessionId: options.originSessionId,
665
- ...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
666
- ...(options.lineageKind === undefined ? {} : { lineageKind: options.lineageKind }),
667
- ...(options.restartNumber === undefined ? {} : { restartNumber: options.restartNumber }),
668
- ...(options.parentTerminalFingerprint === undefined
669
- ? {}
670
- : { parentTerminalFingerprint: options.parentTerminalFingerprint }),
671
- ...(options.now === undefined ? {} : { now: options.now }),
672
- });
673
- }
674
- const claimed = this.claimWorkflowRun({
675
- runId: options.runId,
676
- runnerId: options.runnerId,
677
- claimToken: options.claimToken,
678
- leaseMs: options.leaseMs,
679
- ...(options.now === undefined ? {} : { now: options.now }),
680
- });
681
- if (claimed === undefined) {
682
- throw new Error(`Workflow run could not be claimed: ${options.runId}`);
683
- }
684
- return claimed;
685
- }
686
- getWorkflowRun(runId) {
687
- const row = this.workflowRunRow(runId);
688
- return row === undefined ? undefined : this.mapWorkflowRun(row);
689
- }
690
- getWorkflowRunView(runId) {
691
- const row = this.workflowRunViewRows("WHERE r.run_id = ?", [runId])[0];
692
- return row === undefined ? undefined : workflowRunViewRecord(row);
693
- }
694
- workflowRunListRevision() {
695
- const row = this.state.connection
696
- .prepare(`SELECT count(*) AS count, COALESCE(sum(res.revision), 0) AS revisionSum,
697
- COALESCE(max(q.updated_at), 0) AS updatedAt
698
- FROM runs r JOIN run_queue q ON q.run_id = r.run_id
699
- JOIN resources res ON res.resource_id = r.resource_id`)
700
- .get();
701
- if (!isRunListRevisionRow(row))
702
- throw new Error("Workflow run list revision is invalid");
703
- return {
704
- total: row.count,
705
- revision: `${row.count}:${row.revisionSum}:${row.updatedAt}`,
706
- };
707
- }
708
- listWorkflowRunViews(options) {
709
- const current = this.workflowRunListRevision();
710
- const rows = this.workflowRunViewRows("ORDER BY q.created_at DESC LIMIT ? OFFSET ?", [
711
- options.limit,
712
- options.offset,
713
- ]);
714
- return { ...current, runs: rows.map(workflowRunViewRecord) };
715
- }
716
- workflowRunProjectPath(runId) {
717
- const row = this.state.connection
718
- .prepare(`SELECT p.canonical_path AS canonicalPath
719
- FROM runs r JOIN projects p ON p.project_id = r.project_id WHERE r.run_id = ?`)
720
- .get(runId);
721
- return isCanonicalPathRow(row) ? row.canonicalPath : undefined;
722
- }
723
- listWorkflowRuns(options = {}) {
724
- const clauses = [];
725
- const params = [];
726
- if (this.projectId !== null) {
727
- clauses.push("r.project_id = ?");
728
- params.push(this.projectId);
729
- }
730
- if (options.statuses !== undefined && options.statuses.length > 0) {
731
- clauses.push(`q.status IN (${options.statuses.map(() => "?").join(", ")})`);
732
- params.push(...options.statuses);
733
- }
734
- if (options.excludeRunIds !== undefined && options.excludeRunIds.length > 0) {
735
- clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
736
- params.push(...options.excludeRunIds);
737
- }
738
- if (options.limit !== undefined)
739
- params.push(options.limit);
740
- const rows = this.state.connection
741
- .prepare(workflowRunSelect(`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC${options.limit === undefined ? "" : " LIMIT ?"}`))
742
- .all(...params);
743
- return rows.filter(isRunRow).map((row) => this.mapWorkflowRun(row));
744
- }
745
- findSessionReservation(sessionId) {
746
- const row = this.state.connection
747
- .prepare(workflowRunSelect("WHERE b.origin_session_id = ? AND q.status NOT IN ('done', 'failed', 'cancelled') ORDER BY q.created_at DESC LIMIT 1"))
748
- .get(sessionId);
749
- return isRunRow(row) ? this.mapWorkflowRun(row) : undefined;
750
- }
751
- findSessionReservationView(sessionId) {
752
- 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];
753
- return row === undefined ? undefined : workflowRunViewRecord(row);
754
- }
755
- claimWorkflowRun(options) {
756
- const now = epoch(validTimestamp(options.now));
757
- return this.claimRun(options.runId, options.runnerId, options.claimToken, options.leaseMs, now);
758
- }
759
- /** Schedule a runner that validates one pending interactive submission. */
760
- claimWorkflowRunForInteractionValidation(options) {
761
- const now = epoch(validTimestamp(options.now));
762
- return this.claimRun(options.runId, options.runnerId, options.claimToken, options.leaseMs, now, {
763
- allowPendingInteraction: true,
764
- });
765
- }
766
- /** Take a short server claim without scheduling a parked interactive run. */
767
- claimWorkflowRunForControl(options) {
768
- const now = epoch(validTimestamp(options.now));
769
- return this.claimRun(options.runId, options.runnerId, options.claimToken, options.leaseMs, now, { allowPendingInteraction: true, preserveQueueStatus: true });
770
- }
771
- beginWorkflowRunInteractionTimeout(options) {
772
- const now = epoch(validTimestamp(options.now));
773
- return this.state.transaction(() => {
774
- if (!this.verifyWorkflowRunClaim({
775
- runId: options.runId,
776
- claimToken: options.claimToken,
777
- now: new Date(now).toISOString(),
778
- })) {
779
- return false;
780
- }
781
- const row = this.requireWorkflowRunRow(options.runId);
782
- const interaction = this.state.connection
783
- .prepare(`SELECT i.request_id AS requestId, i.attempt_id AS attemptId
784
- FROM interactive_requests i
785
- JOIN node_attempts a ON a.attempt_id = i.attempt_id
786
- JOIN runs r ON r.run_id = i.run_id
787
- WHERE i.run_id = ? AND i.target_session_id = ?
788
- AND i.status = 'pending' AND r.paused = 0
789
- AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
790
- AND EXISTS (
791
- SELECT 1 FROM workflow_messages m
792
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
793
- WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
794
- )
795
- ORDER BY a.deadline_at, i.request_id LIMIT 1`)
796
- .get(options.runId, options.targetSessionId, now);
797
- if (!isExpiredInteractionRow(interaction))
798
- return false;
799
- const request = this.state.connection
800
- .prepare(`UPDATE interactive_requests
801
- SET status = 'cancelled', revision = revision + 1, updated_at = ?
802
- WHERE request_id = ? AND run_id = ? AND status = 'pending'`)
803
- .run(now, interaction.requestId, options.runId);
804
- const run = this.state.connection
805
- .prepare(`UPDATE runs
806
- SET status = 'running', status_detail = 'finishing expired interaction deadline',
807
- updated_at = ?, finished_at = NULL
808
- WHERE run_id = ? AND status = 'waiting'`)
809
- .run(now, options.runId);
810
- const queue = this.state.connection
811
- .prepare(`UPDATE run_queue SET status = 'starting', error_code = NULL, error_hash = NULL,
812
- updated_at = ?, finished_at = NULL
813
- WHERE run_id = ? AND status = 'parked'`)
814
- .run(now, options.runId);
815
- /* istanbul ignore if -- the expired parked interaction was selected above */
816
- if (request.changes !== 1 || run.changes !== 1 || queue.changes !== 1) {
817
- throw new Error(`Workflow run ${options.runId} changed during interaction timeout`);
818
- }
819
- const error = "Workflow node model-turn deadline expired";
820
- const receiptHash = this.state.putJson({ status: "rejected", error }, now);
821
- this.state.connection
822
- .prepare(`UPDATE interactive_submissions SET outcome = 'rejected', receipt_hash = ?
823
- WHERE request_id = ? AND outcome = 'validating'`)
824
- .run(receiptHash, interaction.requestId);
825
- const revision = this.resourceRevision(row.resourceId);
826
- const lease = this.requireLease(row.resourceId);
827
- this.bumpResource(row.resourceId, revision, now);
828
- this.insertEvent(row.resourceId, revision + 1, "run.interaction_timeout_started", lease.ownerType ?? "system", lease.ownerId, { requestId: interaction.requestId, attemptId: interaction.attemptId }, now, lease.generation || undefined);
829
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }, { targetType: "conversation" }], now);
830
- return true;
831
- });
832
- }
833
- markWorkflowRunRunning(options) {
834
- return this.updateClaimedRunStatus(options.runId, options.claimToken, "running", options.now);
835
- }
836
- claimNextWorkflowRun(options) {
837
- const now = epoch(validTimestamp(options.now));
838
- const clauses = [
839
- "q.status IN ('queued', 'parked', 'starting', 'running')",
840
- "q.available_at <= ?",
841
- "(l.owner_id IS NULL OR l.expires_at <= ? OR l.owner_id = ?)",
842
- "(q.affinity_runner_id IS NULL OR q.affinity_runner_id = ? OR q.status IN ('parked', 'starting', 'running'))",
843
- "NOT (q.status = 'parked' AND (r.status = 'waiting' OR r.paused = 1))",
844
- "NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status = 'pending')",
845
- "(q.error_code IS NULL OR q.error_code NOT IN ('workflowSourceChanged', 'runnerNoProgress'))",
846
- ];
847
- const params = [now, now, options.runnerId, options.runnerId];
848
- if (this.projectId !== null) {
849
- clauses.unshift("r.project_id = ?");
850
- params.unshift(this.projectId);
851
- }
852
- if (options.sessionId !== undefined) {
853
- clauses.push("b.origin_session_id = ?");
854
- params.push(options.sessionId);
855
- }
856
- else {
857
- clauses.push("(b.execution_mode = 'headless' OR q.status <> 'queued')");
858
- }
859
- if (options.excludeRunIds !== undefined && options.excludeRunIds.length > 0) {
860
- clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
861
- params.push(...options.excludeRunIds);
862
- }
863
- const row = this.state.connection
864
- .prepare(workflowRunSelect(`WHERE ${clauses.join(" AND ")} ORDER BY q.available_at, q.created_at LIMIT 1`))
865
- .get(...params);
866
- if (!isRunRow(row))
867
- return undefined;
868
- return this.claimRun(row.runId, options.runnerId, options.claimToken, options.leaseMs, now);
869
- }
870
- renewWorkflowRunClaim(options) {
871
- const now = epoch(validTimestamp(options.now));
872
- return this.state.transaction(() => {
873
- const row = this.workflowRunRow(options.runId);
874
- if (row === undefined || row.ownerId === null)
875
- return false;
876
- const lease = this.requireLease(row.resourceId);
877
- const result = this.state.connection
878
- .prepare(`UPDATE leases SET heartbeat_at = ?, expires_at = ?
879
- WHERE resource_id = ? AND owner_type = ? AND owner_id = ?
880
- AND token_hash = ? AND generation = ? AND expires_at > ?`)
881
- .run(now, now + options.leaseMs, row.resourceId, lease.ownerType, row.ownerId, tokenHash(options.claimToken), lease.generation, now);
882
- if (result.changes === 1) {
883
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
884
- return true;
885
- }
886
- return false;
887
- });
888
- }
889
- verifyWorkflowRunClaim(options) {
890
- const now = epoch(validTimestamp(options.now));
891
- const row = this.workflowRunRow(options.runId);
892
- if (row === undefined || row.ownerId === null || row.claimExpiresAt === null)
893
- return false;
894
- const lease = this.requireLease(row.resourceId);
895
- return (lease.tokenHash !== null &&
896
- lease.tokenHash.equals(tokenHash(options.claimToken)) &&
897
- row.claimExpiresAt > now);
898
- }
899
- workflowRunAuthority(runId, claimToken) {
900
- const row = this.workflowRunRow(runId);
901
- if (row === undefined || row.ownerId === null || row.claimExpiresAt === null)
902
- return undefined;
903
- const lease = this.requireLease(row.resourceId);
904
- if (lease.tokenHash === null || !lease.tokenHash.equals(tokenHash(claimToken)))
905
- return undefined;
906
- const ownerType = row.ownerId.startsWith("host-") ? "host" : "session";
907
- return {
908
- actor: { type: ownerType, id: row.ownerId },
909
- ownerType,
910
- ownerId: row.ownerId,
911
- token: claimToken,
912
- generation: row.leaseGeneration,
913
- leaseMs: 30_000,
914
- };
915
- }
916
- parkWorkflowRun(options) {
917
- return this.releaseRunClaim(options.runId, options.claimToken, "parked", options.now);
918
- }
919
- parkWorkflowRunForRunnerNoProgress(options) {
920
- const now = epoch(validTimestamp(options.now));
921
- return this.state.transaction(() => {
922
- if (!this.verifyWorkflowRunClaim({
923
- runId: options.runId,
924
- claimToken: options.claimToken,
925
- now: new Date(now).toISOString(),
926
- })) {
927
- return false;
928
- }
929
- const row = this.requireWorkflowRunRow(options.runId);
930
- const lease = this.requireLease(row.resourceId);
931
- const detail = options.detail.slice(0, 8_192);
932
- const errorHash = this.state.putText(detail, now);
933
- const run = this.state.connection
934
- .prepare(`UPDATE runs SET status_detail = ?, updated_at = ?, finished_at = NULL
935
- WHERE run_id = ? AND status NOT IN ('completed', 'failed', 'timed_out', 'cancelled')`)
936
- .run(detail, now, options.runId);
937
- const queue = this.state.connection
938
- .prepare(`UPDATE run_queue
939
- SET status = 'parked', error_code = 'runnerNoProgress', error_hash = ?,
940
- available_at = ?, updated_at = ?, finished_at = NULL
941
- WHERE run_id = ? AND status IN ('queued', 'starting', 'running', 'parked')`)
942
- .run(errorHash, now, now, options.runId);
943
- if (run.changes !== 1 || queue.changes !== 1) {
944
- throw new Error(`Workflow run ${options.runId} changed during runner recovery`);
945
- }
946
- const released = this.state.connection
947
- .prepare(`UPDATE leases
948
- SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
949
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
950
- WHERE resource_id = ? AND token_hash = ? AND generation = ? AND expires_at > ?`)
951
- .run(row.resourceId, tokenHash(options.claimToken), lease.generation, now);
952
- /* istanbul ignore if -- the exact live claim is stable in this transaction */
953
- if (released.changes !== 1) {
954
- throw new Error(`Workflow run ${options.runId} claim changed during runner recovery`);
955
- }
956
- const revision = this.resourceRevision(row.resourceId);
957
- this.bumpResource(row.resourceId, revision, now);
958
- this.insertEvent(row.resourceId, revision + 1, "run.worker_no_progress", lease.ownerType ?? "system", lease.ownerId, { status: "parked", code: "runnerNoProgress" }, now, lease.generation || undefined);
959
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
960
- return true;
961
- });
962
- }
963
- pauseParkedWorkflowRun(options) {
964
- const now = epoch(validTimestamp(options.now));
965
- return this.state.transaction(() => {
966
- const row = this.workflowRunRow(options.runId);
967
- if (row === undefined || row.status !== "parked")
968
- return false;
969
- const lease = this.requireLease(row.resourceId);
970
- if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
971
- return false;
972
- const pending = this.state.connection
973
- .prepare(`SELECT 1 FROM interactive_requests
974
- WHERE run_id = ? AND status = 'pending' LIMIT 1`)
975
- .get(options.runId);
976
- if (pending === undefined)
977
- return false;
978
- if (row.paused === 1)
979
- return true;
980
- if (!["running", "waiting"].includes(row.runStatus))
981
- return false;
982
- const changed = this.state.connection
983
- .prepare(`UPDATE runs SET paused = 1, status_detail = 'paused', updated_at = ?
984
- WHERE run_id = ? AND paused = 0 AND status IN ('running', 'waiting')`)
985
- .run(now, options.runId);
986
- if (changed.changes !== 1)
987
- return false;
988
- const revision = this.resourceRevision(row.resourceId);
989
- this.bumpResource(row.resourceId, revision, now);
990
- this.insertEvent(row.resourceId, revision + 1, "run.paused", "control", null, { status: "parked" }, now);
991
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
992
- return true;
993
- });
994
- }
995
- resumePausedInteraction(options) {
996
- const now = epoch(validTimestamp(options.now));
997
- return this.state.transaction(() => {
998
- const row = this.workflowRunRow(options.runId);
999
- if (row === undefined ||
1000
- row.status !== "parked" ||
1001
- row.runStatus !== "waiting" ||
1002
- row.paused !== 1) {
1003
- return false;
1004
- }
1005
- const lease = this.requireLease(row.resourceId);
1006
- if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
1007
- return false;
1008
- const pending = this.state.connection
1009
- .prepare(`SELECT 1 FROM interactive_requests
1010
- WHERE run_id = ? AND status = 'pending' LIMIT 1`)
1011
- .get(options.runId);
1012
- if (pending === undefined)
1013
- return false;
1014
- const changed = this.state.connection
1015
- .prepare(`UPDATE runs
1016
- SET paused = 0, status_detail = 'waiting for origin-session input', updated_at = ?
1017
- WHERE run_id = ? AND status = 'waiting' AND paused = 1`)
1018
- .run(now, options.runId);
1019
- if (changed.changes !== 1)
1020
- return false;
1021
- const revision = this.resourceRevision(row.resourceId);
1022
- this.bumpResource(row.resourceId, revision, now);
1023
- this.insertEvent(row.resourceId, revision + 1, "run.resumed", "control", null, { status: "waiting" }, now);
1024
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
1025
- return true;
1026
- });
1027
- }
1028
- isWorkflowRunPaused(runId) {
1029
- return this.workflowRunRow(runId)?.paused === 1;
1030
- }
1031
- parkWorkflowRunForSourceChange(options) {
1032
- const now = epoch(validTimestamp(options.now));
1033
- return this.state.transaction(() => {
1034
- const row = this.workflowRunRow(options.runId);
1035
- if (row === undefined ||
1036
- !this.verifyWorkflowRunClaim({
1037
- runId: options.runId,
1038
- claimToken: options.claimToken,
1039
- now: new Date(now).toISOString(),
1040
- })) {
1041
- return false;
1042
- }
1043
- const lease = this.requireLease(row.resourceId);
1044
- const detail = options.detail.slice(0, 8_192);
1045
- const errorHash = this.state.putText(detail, now);
1046
- const run = this.state.connection
1047
- .prepare(`UPDATE runs SET status_detail = ?, updated_at = ?, finished_at = NULL
1048
- WHERE run_id = ? AND status NOT IN ('completed', 'failed', 'timed_out', 'cancelled')`)
1049
- .run(detail, now, options.runId);
1050
- const queue = this.state.connection
1051
- .prepare(`UPDATE run_queue
1052
- SET status = 'parked', error_code = 'workflowSourceChanged', error_hash = ?,
1053
- available_at = ?, updated_at = ?, finished_at = NULL
1054
- WHERE run_id = ? AND status IN ('queued', 'starting', 'running', 'parked')`)
1055
- .run(errorHash, now, now, options.runId);
1056
- /* istanbul ignore if -- exact live claim and nonterminal checks make both updates mandatory */
1057
- if (run.changes !== 1 || queue.changes !== 1) {
1058
- throw new Error(`Workflow run ${options.runId} has inconsistent source-change state`);
1059
- }
1060
- const released = this.state.connection
1061
- .prepare(`UPDATE leases
1062
- SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
1063
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
1064
- WHERE resource_id = ? AND token_hash = ? AND generation = ? AND expires_at > ?`)
1065
- .run(row.resourceId, tokenHash(options.claimToken), lease.generation, now);
1066
- /* istanbul ignore if -- the exact live claim is stable in this transaction */
1067
- if (released.changes !== 1) {
1068
- throw new Error(`Workflow run ${options.runId} claim changed during source recovery`);
1069
- }
1070
- const revision = this.resourceRevision(row.resourceId);
1071
- this.bumpResource(row.resourceId, revision, now);
1072
- this.insertEvent(row.resourceId, revision + 1, "run.source_changed", lease.ownerType ?? "system", lease.ownerId, { status: "parked", code: "workflowSourceChanged" }, now, lease.generation || undefined);
1073
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
1074
- return true;
1075
- });
1076
- }
1077
- parkWorkflowRunForAmbiguousEffect(options) {
1078
- const now = epoch(validTimestamp(options.now));
1079
- return this.state.transaction(() => {
1080
- const row = this.workflowRunRow(options.runId);
1081
- if (row === undefined ||
1082
- !this.verifyWorkflowRunClaim({
1083
- runId: options.runId,
1084
- claimToken: options.claimToken,
1085
- now: new Date(now).toISOString(),
1086
- })) {
1087
- return false;
1088
- }
1089
- const lease = this.requireLease(row.resourceId);
1090
- const detail = "effect outcome is ambiguous; explicit recovery is required";
1091
- const errorHash = this.state.putText(detail, now);
1092
- this.state.connection
1093
- .prepare(`UPDATE node_attempts SET status = 'waiting', updated_at = ?
1094
- WHERE run_id = ? AND status IN ('pending', 'running')`)
1095
- .run(now, options.runId);
1096
- const run = this.state.connection
1097
- .prepare(`UPDATE runs SET status = 'waiting', status_detail = ?, updated_at = ?, finished_at = ?
1098
- WHERE run_id = ? AND status = 'running'`)
1099
- .run(detail, now, now, options.runId);
1100
- const queue = this.state.connection
1101
- .prepare(`UPDATE run_queue
1102
- SET status = 'parked', error_code = 'effectAmbiguous', error_hash = ?, updated_at = ?
1103
- WHERE run_id = ? AND status IN ('starting', 'running', 'parked')`)
1104
- .run(errorHash, now, options.runId);
1105
- /* istanbul ignore if -- exact live claim and run checks make both updates mandatory */
1106
- if (run.changes !== 1 || queue.changes !== 1) {
1107
- throw new Error(`Workflow run ${options.runId} has inconsistent ambiguous-effect state`);
1108
- }
1109
- const released = this.state.connection
1110
- .prepare(`UPDATE leases
1111
- SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
1112
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
1113
- WHERE resource_id = ? AND token_hash = ? AND generation = ? AND expires_at > ?`)
1114
- .run(row.resourceId, tokenHash(options.claimToken), lease.generation, now);
1115
- /* istanbul ignore if -- the exact live claim is stable in this transaction */
1116
- if (released.changes !== 1) {
1117
- throw new Error(`Workflow run ${options.runId} claim changed during effect recovery`);
1118
- }
1119
- const revision = this.resourceRevision(row.resourceId);
1120
- this.bumpResource(row.resourceId, revision, now);
1121
- this.insertEvent(row.resourceId, revision + 1, "run.effect_ambiguous", lease.ownerType ?? "system", lease.ownerId, { status: "waiting", code: "effectAmbiguous" }, now, lease.generation || undefined);
1122
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
1123
- return true;
1124
- });
1125
- }
1126
- failWorkflowRun(options) {
1127
- return this.terminalRun(options.runId, options.claimToken, "failed", options.errorCode, options.errorMessage, options.now);
1128
- }
1129
- cancelWorkflowRun(options) {
1130
- const cancelled = this.terminalRun(options.runId, options.claimToken, "cancelled", "cancelled", "Workflow run cancelled", options.now);
1131
- if (cancelled || options.claimToken !== undefined)
1132
- return cancelled;
1133
- return this.cancelStaleWorkflowRun({
1134
- runId: options.runId,
1135
- ...(options.now === undefined ? {} : { now: options.now }),
1136
- });
1137
- }
1138
- /** Cancel a nonterminal run only when its claim is absent or expired. */
1139
- cancelStaleWorkflowRun(options) {
1140
- const now = epoch(validTimestamp(options.now));
1141
- const controlId = options.controlId ?? "workflow-control";
1142
- const claimToken = options.claimToken ?? randomUUID();
1143
- return this.state.transaction(() => {
1144
- const row = this.workflowRunRow(options.runId);
1145
- if (row === undefined || ["done", "failed", "cancelled"].includes(row.status))
1146
- return false;
1147
- const lease = this.requireLease(row.resourceId);
1148
- if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
1149
- return false;
1150
- const generation = lease.generation + 1;
1151
- const claimed = this.state.connection
1152
- .prepare(`UPDATE leases
1153
- SET generation = ?, owner_type = 'system', owner_id = ?, token_hash = ?,
1154
- acquired_at = ?, heartbeat_at = ?, expires_at = ?
1155
- WHERE resource_id = ? AND generation = ?
1156
- AND (owner_id IS NULL OR expires_at IS NULL OR expires_at <= ?)`)
1157
- .run(generation, controlId, tokenHash(claimToken), now, now, now + 30_000, row.resourceId, lease.generation, now);
1158
- if (claimed.changes !== 1)
1159
- return false;
1160
- const errorHash = this.state.putText("Workflow run cancelled", now);
1161
- const queue = this.state.connection
1162
- .prepare(`UPDATE run_queue
1163
- SET status = 'cancelled', error_code = 'cancelled', error_hash = ?,
1164
- updated_at = ?, finished_at = ?
1165
- WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`)
1166
- .run(errorHash, now, now, options.runId);
1167
- /* istanbul ignore if -- the control claim selects one nonterminal queue row */
1168
- if (queue.changes !== 1) {
1169
- throw new Error(`Workflow run ${options.runId} has inconsistent queue state`);
1170
- }
1171
- const run = this.state.connection
1172
- .prepare(`UPDATE runs
1173
- SET status = 'cancelled', paused = 0, status_detail = NULL, error_hash = ?,
1174
- updated_at = ?, finished_at = ?
1175
- WHERE run_id = ? AND status NOT IN ('completed', 'failed', 'timed_out', 'cancelled')`)
1176
- .run(errorHash, now, now, options.runId);
1177
- /* istanbul ignore if -- the control claim selects one nonterminal durable run */
1178
- if (run.changes !== 1) {
1179
- throw new Error(`Workflow run ${options.runId} has inconsistent durable state`);
1180
- }
1181
- this.settleWorkflowRunMessages(options.runId, now);
1182
- this.cancelWorkflowRunDependents(options.runId, controlId, errorHash, now);
1183
- const released = this.state.connection
1184
- .prepare(`UPDATE leases
1185
- SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
1186
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
1187
- WHERE resource_id = ? AND owner_type = 'system' AND owner_id = ?
1188
- AND token_hash = ? AND generation = ? AND expires_at > ?`)
1189
- .run(row.resourceId, controlId, tokenHash(claimToken), generation, now);
1190
- /* istanbul ignore if -- the exact control claim was written in this transaction */
1191
- if (released.changes !== 1) {
1192
- throw new Error(`Workflow run ${options.runId} control claim changed during cancellation`);
1193
- }
1194
- const revision = this.resourceRevision(row.resourceId);
1195
- this.bumpResource(row.resourceId, revision, now);
1196
- this.insertEvent(row.resourceId, revision + 1, "run.queue_cancelled", "control", controlId, { status: "cancelled", code: "cancelled", staleControl: true }, now, generation);
1197
- recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
1198
- return true;
1199
- });
1200
- }
1201
- deleteWorkflowRun(options) {
1202
- const now = Date.now();
1203
- return this.state.transaction(() => {
1204
- const row = this.workflowRunRow(options.runId);
1205
- if (row === undefined)
1206
- return false;
1207
- const lease = this.requireLease(row.resourceId);
1208
- if (lease.ownerId === null ||
1209
- lease.tokenHash === null ||
1210
- lease.expiresAt === null ||
1211
- lease.expiresAt <= now ||
1212
- !lease.tokenHash.equals(tokenHash(options.claimToken))) {
1213
- return false;
1214
- }
1215
- return (this.state.connection
1216
- .prepare("DELETE FROM resources WHERE resource_id = ?")
1217
- .run(row.resourceId).changes === 1);
1218
- });
1219
- }
1220
- completeWorkflowRun(options) {
1221
- return this.releaseRunClaim(options.runId, options.claimToken, "done", options.now);
1222
- }
1223
- repairCanonicalWorkflowSourceRun() {
1224
- throw new Error("Legacy workflow source repair is not available after the SQLite cutover");
1225
- }
1226
- claimLegacyWorkflowSourceRun() {
1227
- throw new Error("Legacy workflow source claims are not available after the SQLite cutover");
1228
- }
1229
- recordRunEvent(options) {
1230
- return this.state.transaction(() => {
1231
- const row = this.requireWorkflowRunRow(options.runId);
1232
- const now = epoch(validTimestamp(options.now));
1233
- const revision = this.resourceRevision(row.resourceId) + 1;
1234
- this.bumpResource(row.resourceId, revision - 1, now);
1235
- const eventId = this.insertEvent(row.resourceId, revision, options.type, options.runnerId?.startsWith("host-") ? "host" : "session", options.runnerId ?? null, options.payload ?? {}, now, row.leaseGeneration || undefined);
1236
- const seq = this.state.connection
1237
- .prepare("SELECT event_seq AS seq FROM events WHERE event_id = ?")
1238
- .get(eventId);
1239
- /* istanbul ignore if -- exact schema and internal query shape */
1240
- if (!isSequenceRow(seq))
1241
- throw new Error("Run event was not recorded");
1242
- return {
1243
- seq: seq.seq,
1244
- recordedAt: new Date(now).toISOString(),
1245
- runId: options.runId,
1246
- workflowRef: options.workflowRef,
1247
- type: options.type,
1248
- runnerId: options.runnerId ?? null,
1249
- payload: options.payload ?? {},
1250
- };
1251
- });
1252
- }
1253
- listRunEventsAfter(seq, options = {}) {
1254
- const rows = this.state.connection
1255
- .prepare(`SELECT e.event_seq AS seq, e.recorded_at AS recordedAt, r.run_id AS runId,
1256
- r.workflow_ref AS workflowRef, e.event_type AS eventType,
1257
- e.actor_id AS runnerId, e.payload_hash AS payloadHash
1258
- FROM events e JOIN runs r ON r.resource_id = e.resource_id
1259
- WHERE e.event_seq > ? ORDER BY e.event_seq LIMIT ?`)
1260
- .all(seq, options.limit ?? 100);
1261
- return rows.filter(isRunEventRow).map((row) => ({
1262
- seq: row.seq,
1263
- recordedAt: new Date(row.recordedAt).toISOString(),
1264
- runId: row.runId,
1265
- workflowRef: row.workflowRef,
1266
- type: row.eventType,
1267
- runnerId: row.runnerId,
1268
- payload: row.payloadHash === null ? {} : this.state.readJson(row.payloadHash),
1269
- }));
1270
- }
1271
- settleRunEffect(runId, effectType) {
1272
- const rows = this.state.connection
1273
- .prepare(`SELECT e.effect_id AS effectId, e.resource_id AS resourceId
1274
- FROM effects e JOIN runs r ON r.resource_id = e.source_resource_id
1275
- WHERE r.run_id = ? AND e.effect_type = ? AND e.status = 'pending'`)
1276
- .all(runId, effectType);
1277
- for (const row of rows) {
1278
- /* istanbul ignore if -- exact schema and internal query shape */
1279
- if (!isEffectIdentityRow(row))
1280
- continue;
1281
- this.state.transaction(() => {
1282
- const now = Date.now();
1283
- const revision = this.resourceRevision(row.resourceId);
1284
- this.state.connection
1285
- .prepare(`UPDATE effects SET status = 'applied', updated_at = ?, settled_at = ?
1286
- WHERE effect_id = ? AND status = 'pending'`)
1287
- .run(now, now, row.effectId);
1288
- this.bumpResource(row.resourceId, revision, now);
1289
- this.insertEvent(row.resourceId, revision + 1, "effect.applied", "system", null, { runId, effectType }, now);
1290
- });
1291
- }
1292
- }
1293
- setWorkflowRunOriginSession(runId, originSessionId) {
1294
- const now = Date.now();
1295
- return (this.state.connection
1296
- .prepare(`INSERT INTO run_bindings(run_id, origin_session_id, execution_mode, created_at)
1297
- VALUES (?, ?, 'interactive', ?)
1298
- ON CONFLICT(run_id) DO UPDATE SET origin_session_id = excluded.origin_session_id`)
1299
- .run(runId, originSessionId, now).changes === 1);
1300
- }
1301
- ensureProject(projectPath) {
1302
- const canonicalPath = canonicalProjectPath(projectPath);
1303
- const projectId = projectIdFor(canonicalPath);
1304
- this.state.connection
1305
- .prepare(`INSERT INTO projects(project_id, canonical_path, created_at)
1306
- VALUES (?, ?, ?) ON CONFLICT(project_id) DO NOTHING`)
1307
- .run(projectId, canonicalPath, Date.now());
1308
- return projectId;
1309
- }
1310
- findProject(projectPath) {
1311
- const row = this.state.connection
1312
- .prepare("SELECT project_id AS projectId FROM projects WHERE canonical_path = ?")
1313
- .get(canonicalProjectPath(projectPath));
1314
- return isProjectRow(row) ? row.projectId : null;
1315
- }
1316
- requireProjectId() {
1317
- if (this.projectId === null)
1318
- throw new Error("ResourceManager project is not registered in the state database");
1319
- return this.projectId;
1320
- }
1321
552
  resourceManagerRow(ref) {
1322
553
  if (this.projectId === null)
1323
554
  return undefined;
@@ -1458,18 +689,6 @@ export class SqliteResourceManagerStore {
1458
689
  throw new Error("ResourceManager claim is stale");
1459
690
  }
1460
691
  }
1461
- requireLease(resourceId) {
1462
- const row = this.state.connection
1463
- .prepare(`SELECT generation, owner_type AS ownerType, owner_id AS ownerId,
1464
- token_hash AS tokenHash, expires_at AS expiresAt
1465
- FROM leases WHERE resource_id = ?`)
1466
- .get(resourceId);
1467
- /* istanbul ignore if -- exact schema and internal query shape */
1468
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
1469
- if (!isLeaseRow(row))
1470
- throw new Error(`Lease is missing: ${resourceId}`);
1471
- return row;
1472
- }
1473
692
  releaseLease(resourceId, claim, now) {
1474
693
  const row = this.resourceManagerRow({ resourceManager: claim.resourceManager, key: claim.key });
1475
694
  if (row === undefined)
@@ -1486,47 +705,6 @@ export class SqliteResourceManagerStore {
1486
705
  claim.resourceVersion = revision + 1;
1487
706
  }
1488
707
  }
1489
- resourceRevision(resourceId) {
1490
- const row = this.state.connection
1491
- .prepare("SELECT revision FROM resources WHERE resource_id = ?")
1492
- .get(resourceId);
1493
- /* istanbul ignore if -- exact schema and internal query shape */
1494
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
1495
- if (!isRevisionRow(row))
1496
- throw new Error(`Resource is missing: ${resourceId}`);
1497
- return row.revision;
1498
- }
1499
- bumpResource(resourceId, expectedRevision, now) {
1500
- const result = this.state.connection
1501
- .prepare("UPDATE resources SET revision = revision + 1, updated_at = ? WHERE resource_id = ? AND revision = ?")
1502
- .run(now, resourceId, expectedRevision);
1503
- if (result.changes !== 1)
1504
- throw new Error("Resource revision conflict");
1505
- const run = this.state.connection
1506
- .prepare(`SELECT r.run_id AS runId
1507
- FROM runs r JOIN viewer_runs v ON v.run_id = r.run_id
1508
- WHERE r.resource_id = ?`)
1509
- .get(resourceId);
1510
- if (isRecord(run) && typeof run.runId === "string") {
1511
- recordViewerDeltas(this.state, run.runId, [
1512
- { targetType: "summary" },
1513
- { targetType: "graph" },
1514
- { targetType: "replay" },
1515
- { targetType: "inspector", targetKey: "run" },
1516
- ], now);
1517
- }
1518
- }
1519
- insertEvent(resourceId, revision, type, actorType, actorId, payload, now, leaseGeneration) {
1520
- const eventId = `event-${randomUUID()}`;
1521
- const payloadHash = this.state.putJson(payload, now);
1522
- this.state.connection
1523
- .prepare(`INSERT INTO events(
1524
- event_id, resource_id, resource_revision, event_type, actor_type,
1525
- actor_id, lease_generation, payload_hash, recorded_at
1526
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1527
- .run(eventId, resourceId, revision, type, actorType, actorId, leaseGeneration ?? null, payloadHash, now);
1528
- return eventId;
1529
- }
1530
708
  effectRow(resourceUid, key) {
1531
709
  const row = this.state.connection
1532
710
  .prepare(effectSelect("WHERE c.uid = ? AND e.idempotency_key = ?"))
@@ -1600,640 +778,5 @@ export class SqliteResourceManagerStore {
1600
778
  : { error: this.state.readBlob(row.errorHash)?.content.toString("utf8") ?? "" }),
1601
779
  };
1602
780
  }
1603
- workflowRunRow(runId) {
1604
- const row = this.state.connection.prepare(workflowRunSelect("WHERE r.run_id = ?")).get(runId);
1605
- return isRunRow(row) ? row : undefined;
1606
- }
1607
- requireWorkflowRunRow(runId) {
1608
- const row = this.workflowRunRow(runId);
1609
- if (row === undefined)
1610
- throw new Error(`Workflow run not found: ${runId}`);
1611
- return row;
1612
- }
1613
- requireWorkflowRun(runId) {
1614
- return this.mapWorkflowRun(this.requireWorkflowRunRow(runId));
1615
- }
1616
- assertWorkflowRunPreparationCompatible(row, options, definitionDigest) {
1617
- const expectedLineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
1618
- const parent = options.parentRunId === undefined
1619
- ? undefined
1620
- : this.requireWorkflowRunRow(options.parentRunId);
1621
- const expectedRootRunId = parent?.rootRunId ?? options.runId;
1622
- const expectedRestartNumber = expectedLineageKind === "restart"
1623
- ? (options.restartNumber ?? (parent?.restartNumber ?? -1) + 1)
1624
- : (parent?.restartNumber ?? 0);
1625
- const expectedParentTerminalFingerprint = options.parentTerminalFingerprint === undefined
1626
- ? null
1627
- : terminalFingerprintBuffer(options.parentTerminalFingerprint);
1628
- const compatible = row.workflowName === options.workflowName &&
1629
- row.workflowRef === options.workflowSourceRef &&
1630
- row.definitionDigest.equals(definitionDigest) &&
1631
- canonicalJson(readQueuedRunSources(this.state, row)) ===
1632
- canonicalJson(queuedWorkflowSource(options.workflowSource)) &&
1633
- canonicalJson(this.state.readJson(row.inputHash)) === canonicalJson(options.input ?? null) &&
1634
- canonicalJson(this.state.readJson(row.launchOptionsHash)) ===
1635
- canonicalJson(options.launchOptions ?? {}) &&
1636
- row.originSessionId === options.originSessionId &&
1637
- row.executionMode === (options.executionMode ?? "interactive") &&
1638
- row.parentRunId === (options.parentRunId ?? null) &&
1639
- row.rootRunId === expectedRootRunId &&
1640
- row.lineageKind === expectedLineageKind &&
1641
- row.restartNumber === expectedRestartNumber &&
1642
- ((row.parentTerminalFingerprint === null && expectedParentTerminalFingerprint === null) ||
1643
- (row.parentTerminalFingerprint !== null &&
1644
- expectedParentTerminalFingerprint !== null &&
1645
- row.parentTerminalFingerprint.equals(expectedParentTerminalFingerprint)));
1646
- if (!compatible) {
1647
- throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
1648
- }
1649
- }
1650
- workflowRunViewRows(clause, params) {
1651
- return this.state.connection
1652
- .prepare(workflowRunViewSelect(clause))
1653
- .all(...params)
1654
- .filter(isWorkflowRunViewRow);
1655
- }
1656
- /* istanbul ignore next -- pure projection covered by integration tests */
1657
- mapWorkflowRun(row) {
1658
- return {
1659
- runId: row.runId,
1660
- workflowName: row.workflowName,
1661
- workflowSourceRef: row.workflowRef,
1662
- workflowSource: readQueuedRunSources(this.state, row),
1663
- initialized: row.runStatus !== "queued",
1664
- definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
1665
- input: this.state.readJson(row.inputHash),
1666
- launchOptions: this.state.readJson(row.launchOptionsHash),
1667
- status: row.status,
1668
- runnerId: row.ownerId,
1669
- claimToken: null,
1670
- claimGeneration: row.ownerId === null ? null : row.leaseGeneration,
1671
- claimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
1672
- affinityRunnerId: row.affinityRunnerId,
1673
- originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
1674
- executionMode: row.executionMode,
1675
- parentRunId: row.parentRunId,
1676
- rootRunId: row.rootRunId,
1677
- lineageKind: row.lineageKind,
1678
- restartNumber: row.restartNumber,
1679
- parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
1680
- errorCode: row.errorCode,
1681
- errorMessage: row.errorHash === null
1682
- ? null
1683
- : (this.state.readBlob(row.errorHash)?.content.toString("utf8") ?? null),
1684
- createdAt: new Date(row.createdAt).toISOString(),
1685
- updatedAt: new Date(row.updatedAt).toISOString(),
1686
- startedAt: row.startedAt === null ? null : new Date(row.startedAt).toISOString(),
1687
- finishedAt: row.finishedAt === null ? null : new Date(row.finishedAt).toISOString(),
1688
- };
1689
- }
1690
- claimRun(runId, runnerId, claimToken, leaseMs, now, options = {}) {
1691
- return this.state.transaction(() => this.claimRunInTransaction(runId, runnerId, claimToken, leaseMs, now, options));
1692
- }
1693
- claimRunInTransaction(runId, runnerId, claimToken, leaseMs, now, options = {}) {
1694
- const row = this.workflowRunRow(runId);
1695
- if (row === undefined || ["done", "failed", "cancelled"].includes(row.status))
1696
- return undefined;
1697
- if (options.allowPendingInteraction !== true &&
1698
- this.state.connection
1699
- .prepare(`SELECT 1 FROM interactive_requests
1700
- WHERE run_id = ? AND status = 'pending' LIMIT 1`)
1701
- .get(runId) !== undefined) {
1702
- return undefined;
1703
- }
1704
- const lease = this.requireLease(row.resourceId);
1705
- if (lease.ownerId !== null &&
1706
- lease.expiresAt !== null &&
1707
- lease.expiresAt > now &&
1708
- lease.ownerId !== runnerId)
1709
- return undefined;
1710
- const generation = lease.generation + 1;
1711
- const expiresAt = now + leaseMs;
1712
- const result = this.state.connection
1713
- .prepare(`UPDATE leases SET generation = ?, owner_type = ?, owner_id = ?, token_hash = ?,
1714
- acquired_at = ?, heartbeat_at = ?, expires_at = ?
1715
- WHERE resource_id = ? AND generation = ?`)
1716
- .run(generation, runnerId.startsWith("host-") ? "host" : "session", runnerId, tokenHash(claimToken), now, now, expiresAt, row.resourceId, lease.generation);
1717
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
1718
- if (result.changes !== 1)
1719
- return undefined;
1720
- if (options.preserveQueueStatus !== true) {
1721
- this.state.connection
1722
- .prepare(`UPDATE run_queue
1723
- SET status = 'starting', error_code = NULL, error_hash = NULL, updated_at = ?
1724
- WHERE run_id = ?`)
1725
- .run(now, runId);
1726
- }
1727
- const revision = this.resourceRevision(row.resourceId);
1728
- this.bumpResource(row.resourceId, revision, now);
1729
- this.insertEvent(row.resourceId, revision + 1, "lease.claimed", runnerId.startsWith("host-") ? "host" : "session", runnerId, { expiresAt }, now, generation);
1730
- return { ...this.requireWorkflowRun(runId), claimToken };
1731
- }
1732
- updateClaimedRunStatus(runId, claimToken, status, nowValue) {
1733
- const now = epoch(validTimestamp(nowValue));
1734
- return this.state.transaction(() => {
1735
- if (!this.verifyWorkflowRunClaim({
1736
- runId,
1737
- claimToken,
1738
- now: new Date(now).toISOString(),
1739
- })) {
1740
- return false;
1741
- }
1742
- const row = this.requireWorkflowRunRow(runId);
1743
- const changed = this.state.connection
1744
- .prepare("UPDATE run_queue SET status = ?, started_at = COALESCE(started_at, ?), updated_at = ? WHERE run_id = ?")
1745
- .run(status, now, now, runId).changes === 1;
1746
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
1747
- if (!changed)
1748
- return false;
1749
- const revision = this.resourceRevision(row.resourceId);
1750
- const lease = this.requireLease(row.resourceId);
1751
- this.bumpResource(row.resourceId, revision, now);
1752
- this.insertEvent(row.resourceId, revision + 1, `run.queue_${status}`, lease.ownerType ?? "system", lease.ownerId, { status }, now, lease.generation || undefined);
1753
- return true;
1754
- });
1755
- }
1756
- releaseRunClaim(runId, claimToken, status, nowValue) {
1757
- const now = epoch(validTimestamp(nowValue));
1758
- return this.state.transaction(() => {
1759
- const row = this.workflowRunRow(runId);
1760
- if (row === undefined ||
1761
- !this.verifyWorkflowRunClaim({ runId, claimToken, now: new Date(now).toISOString() }))
1762
- return false;
1763
- this.state.connection
1764
- .prepare("UPDATE run_queue SET status = ?, updated_at = ?, finished_at = ? WHERE run_id = ?")
1765
- .run(status, now, status === "done" ? now : null, runId);
1766
- const lease = this.requireLease(row.resourceId);
1767
- const update = this.state.connection
1768
- .prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
1769
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
1770
- WHERE resource_id = ? AND token_hash = ? AND generation = ?`)
1771
- .run(row.resourceId, tokenHash(claimToken), lease.generation);
1772
- /* istanbul ignore if -- impossible after exact schema and transaction checks */
1773
- if (update.changes !== 1)
1774
- return false;
1775
- const revision = this.resourceRevision(row.resourceId);
1776
- this.bumpResource(row.resourceId, revision, now);
1777
- this.insertEvent(row.resourceId, revision + 1, `run.queue_${status}`, lease.ownerType ?? "system", lease.ownerId, { status }, now, lease.generation || undefined);
1778
- recordViewerDeltas(this.state, runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
1779
- return true;
1780
- });
1781
- }
1782
- terminalRun(runId, claimToken, status, code, error, nowValue) {
1783
- const now = epoch(validTimestamp(nowValue));
1784
- return this.state.transaction(() => {
1785
- const row = this.workflowRunRow(runId);
1786
- if (row === undefined || ["done", "failed", "cancelled"].includes(row.status))
1787
- return false;
1788
- const lease = this.requireLease(row.resourceId);
1789
- if (claimToken === undefined) {
1790
- if (!["queued", "starting"].includes(row.status)) {
1791
- return false;
1792
- }
1793
- if (lease.ownerId !== null)
1794
- return false;
1795
- }
1796
- else if (lease.ownerId === null ||
1797
- lease.tokenHash === null ||
1798
- lease.expiresAt === null ||
1799
- lease.expiresAt <= now ||
1800
- !lease.tokenHash.equals(tokenHash(claimToken))) {
1801
- return false;
1802
- }
1803
- const revision = this.resourceRevision(row.resourceId);
1804
- const errorHash = this.state.putText(error, now);
1805
- const queueUpdate = this.state.connection
1806
- .prepare(`UPDATE run_queue
1807
- SET status = ?, error_code = ?, error_hash = ?, updated_at = ?, finished_at = ?
1808
- WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`)
1809
- .run(status, code, errorHash, now, now, runId);
1810
- if (queueUpdate.changes !== 1)
1811
- return false;
1812
- if (claimToken !== undefined) {
1813
- const leaseUpdate = this.state.connection
1814
- .prepare(`UPDATE leases
1815
- SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
1816
- acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
1817
- WHERE resource_id = ? AND token_hash = ? AND generation = ? AND expires_at > ?`)
1818
- .run(row.resourceId, tokenHash(claimToken), lease.generation, now);
1819
- if (leaseUpdate.changes !== 1) {
1820
- throw new Error(`Workflow run ${runId} claim changed during terminal transition`);
1821
- }
1822
- }
1823
- this.state.connection
1824
- .prepare(`UPDATE runs
1825
- SET status = ?, paused = 0, status_detail = NULL, error_hash = ?,
1826
- updated_at = ?, finished_at = ?
1827
- WHERE run_id = ?`)
1828
- .run(status, errorHash, now, now, runId);
1829
- this.settleWorkflowRunMessages(runId, now);
1830
- if (status === "cancelled") {
1831
- this.cancelWorkflowRunDependents(runId, lease.ownerId ?? "workflow-control", errorHash, now);
1832
- }
1833
- this.bumpResource(row.resourceId, revision, now);
1834
- this.insertEvent(row.resourceId, revision + 1, `run.queue_${status}`, lease.ownerType ?? "system", lease.ownerId, { status, code, error }, now, lease.generation || undefined);
1835
- recordViewerDeltas(this.state, runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
1836
- return true;
1837
- });
1838
- }
1839
- settleWorkflowRunMessages(runId, now) {
1840
- this.workflowMessages.settleOpenTurnsForRun(runId, "lost", now);
1841
- this.workflowMessages.cancelPendingForRun(runId, now);
1842
- }
1843
- cancelWorkflowRunDependents(runId, actorId, errorHash, now) {
1844
- this.state.connection
1845
- .prepare(`UPDATE node_attempts
1846
- SET status = 'cancelled', error_hash = COALESCE(error_hash, ?),
1847
- updated_at = ?, finished_at = COALESCE(finished_at, ?)
1848
- WHERE run_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
1849
- .run(errorHash, now, now, runId);
1850
- const effects = this.state.connection
1851
- .prepare(`SELECT e.effect_id AS effectId, e.resource_id AS resourceId,
1852
- e.status, e.attempt_count AS attemptCount
1853
- FROM effects e JOIN runs r ON r.resource_id = e.source_resource_id
1854
- WHERE r.run_id = ? AND e.owner_scope = 'run' AND e.status IN ('pending', 'applying')
1855
- ORDER BY e.effect_id`)
1856
- .all(runId)
1857
- .filter(isCancelledRunEffectRow);
1858
- for (const effect of effects) {
1859
- const status = effect.status === "applying" ? "ambiguous" : "cancelled";
1860
- const revision = this.resourceRevision(effect.resourceId);
1861
- const changed = this.state.connection
1862
- .prepare(`UPDATE effects
1863
- SET status = ?, next_attempt_at = NULL, error_hash = ?, updated_at = ?, settled_at = ?
1864
- WHERE effect_id = ? AND status = ? AND attempt_count = ?`)
1865
- .run(status, errorHash, now, now, effect.effectId, effect.status, effect.attemptCount);
1866
- /* istanbul ignore if -- cancellation serializes the selected effect transition */
1867
- if (changed.changes !== 1) {
1868
- throw new Error(`Workflow effect ${effect.effectId} changed during cancellation`);
1869
- }
1870
- if (effect.status === "applying") {
1871
- this.state.connection
1872
- .prepare(`UPDATE effect_attempts
1873
- SET finished_at = ?, outcome = 'interrupted', error_hash = ?
1874
- WHERE effect_id = ? AND attempt_number = ? AND finished_at IS NULL`)
1875
- .run(now, errorHash, effect.effectId, effect.attemptCount);
1876
- }
1877
- this.bumpResource(effect.resourceId, revision, now);
1878
- this.insertEvent(effect.resourceId, revision + 1, `effect.${status}`, "control", actorId, { runId, reason: "workflowCancelled" }, now);
1879
- }
1880
- this.state.connection
1881
- .prepare(`INSERT INTO human_decision_resolutions(
1882
- decision_id, outcome, provenance, response_hash, reason, channel,
1883
- actor_id, request_digest, resolved_at
1884
- )
1885
- SELECT d.decision_id, 'cancelled', 'explicit_cancel', NULL,
1886
- 'Workflow run cancelled', NULL, ?, d.request_digest, ?
1887
- FROM human_decisions d
1888
- LEFT JOIN human_decision_resolutions r ON r.decision_id = d.decision_id
1889
- WHERE d.run_id = ? AND r.decision_id IS NULL`)
1890
- .run(actorId, now, runId);
1891
- const receiptHash = this.state.putJson({ status: "rejected", error: "Workflow run cancelled" }, now);
1892
- this.state.connection
1893
- .prepare(`UPDATE interactive_submissions SET outcome = 'rejected', receipt_hash = ?
1894
- WHERE outcome = 'validating'
1895
- AND request_id IN (SELECT request_id FROM interactive_requests WHERE run_id = ?)`)
1896
- .run(receiptHash, runId);
1897
- this.state.connection
1898
- .prepare(`UPDATE interactive_requests
1899
- SET status = 'cancelled', revision = revision + 1, updated_at = ?
1900
- WHERE run_id = ? AND status = 'pending'`)
1901
- .run(now, runId);
1902
- }
1903
- assertWritable() {
1904
- if (this.state.mode === "read-only")
1905
- throw new Error("ResourceManager store is read-only");
1906
- }
1907
- }
1908
- function resourceManagerSelect(clause) {
1909
- return `SELECT c.controller_resource_id AS managedResourceId,
1910
- c.resource_id AS resourceId, r.revision AS resourceVersion,
1911
- c.controller_name AS resourceManagerName, c.resource_key AS resourceKey,
1912
- c.uid, c.generation, c.spec_hash AS specHash, c.status_hash AS statusHash,
1913
- c.deletion_requested_at AS deletionRequestedAt
1914
- FROM controller_resources c JOIN resources r ON r.resource_id = c.resource_id ${clause}`;
1915
- }
1916
- function effectSelect(clause) {
1917
- return `SELECT e.effect_id AS effectId, c.uid AS resourceUid, c.generation,
1918
- e.effect_type AS kind, e.status AS state,
1919
- json_extract(CAST(b.content AS TEXT), '$.requestFingerprint') AS requestFingerprint,
1920
- e.created_at AS startedAt, e.settled_at AS completedAt,
1921
- e.external_ref AS externalRef, e.error_hash AS errorHash
1922
- FROM effects e
1923
- JOIN controller_resources c ON c.resource_id = e.source_resource_id
1924
- JOIN blobs b ON b.blob_hash = e.payload_hash ${clause}`;
1925
- }
1926
- function workflowSelect(clause) {
1927
- return `SELECT w.request_id AS requestId, c.uid AS resourceUid,
1928
- w.request_key AS requestKey, w.input_fingerprint AS inputFingerprint,
1929
- w.workflow_name AS workflowName, COALESCE(w.run_id, w.reserved_run_id) AS runId, w.status,
1930
- w.attempt_count AS attemptCount, w.error_hash AS errorHash
1931
- FROM controller_workflows w
1932
- JOIN controller_resources c ON c.controller_resource_id = w.controller_resource_id ${clause}`;
1933
- }
1934
- function workflowRunSelect(clause) {
1935
- return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
1936
- d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
1937
- r.paused, r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
1938
- r.input_hash AS inputHash,
1939
- r.launch_options_hash AS launchOptionsHash,
1940
- q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,
1941
- q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
1942
- q.error_hash AS errorHash, b.origin_session_id AS originSessionId,
1943
- b.execution_mode AS executionMode, r.parent_run_id AS parentRunId,
1944
- r.root_run_id AS rootRunId, r.lineage_kind AS lineageKind,
1945
- r.restart_number AS restartNumber,
1946
- r.parent_terminal_fingerprint AS parentTerminalFingerprint,
1947
- q.created_at AS createdAt, q.updated_at AS updatedAt,
1948
- q.started_at AS startedAt, q.finished_at AS finishedAt,
1949
- l.generation AS leaseGeneration, l.owner_id AS ownerId, l.expires_at AS claimExpiresAt
1950
- FROM runs r JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
1951
- JOIN run_queue q ON q.run_id = r.run_id
1952
- LEFT JOIN run_bindings b ON b.run_id = r.run_id
1953
- JOIN leases l ON l.resource_id = r.resource_id ${clause}`;
1954
- }
1955
- function workflowRunViewSelect(clause) {
1956
- return `SELECT r.run_id AS runId, d.workflow_name AS workflowName,
1957
- r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
1958
- r.definition_digest AS definitionDigest, q.status,
1959
- b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
1960
- r.parent_run_id AS parentRunId, r.root_run_id AS rootRunId,
1961
- r.lineage_kind AS lineageKind, r.restart_number AS restartNumber,
1962
- r.parent_terminal_fingerprint AS parentTerminalFingerprint,
1963
- q.error_code AS errorCode, CAST(error.content AS TEXT) AS errorMessage,
1964
- q.created_at AS createdAt, q.updated_at AS updatedAt,
1965
- q.started_at AS startedAt, q.finished_at AS finishedAt,
1966
- source.source_type AS sourceType, source.source_ref AS sourceRef,
1967
- source.source_revision AS sourceRevision
1968
- FROM runs r JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
1969
- JOIN run_queue q ON q.run_id = r.run_id
1970
- LEFT JOIN run_bindings b ON b.run_id = r.run_id
1971
- LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
1972
- LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
1973
- }
1974
- function isRecord(value) {
1975
- return typeof value === "object" && value !== null && !Array.isArray(value);
1976
- }
1977
- function isResourceManagerRow(value) {
1978
- return isRecord(value);
1979
- }
1980
- function isLeaseRow(value) {
1981
- return isRecord(value);
1982
- }
1983
- function workflowRunViewRecord(row) {
1984
- const root = row.sourceType === "builtin"
1985
- ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
1986
- : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
1987
- return {
1988
- runId: row.runId,
1989
- workflowName: row.workflowName,
1990
- workflowSourceRef: row.workflowRef,
1991
- workflowSource: { root, mounted: [] },
1992
- initialized: row.runStatus !== "queued",
1993
- definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
1994
- runStateStatus: row.runStatus,
1995
- paused: row.paused === 1,
1996
- status: row.status,
1997
- originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
1998
- executionMode: row.executionMode,
1999
- parentRunId: row.parentRunId,
2000
- rootRunId: row.rootRunId,
2001
- lineageKind: row.lineageKind,
2002
- restartNumber: row.restartNumber,
2003
- parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
2004
- errorCode: row.errorCode,
2005
- errorMessage: row.errorMessage,
2006
- createdAt: new Date(row.createdAt).toISOString(),
2007
- updatedAt: new Date(row.updatedAt).toISOString(),
2008
- startedAt: row.startedAt === null ? null : new Date(row.startedAt).toISOString(),
2009
- finishedAt: row.finishedAt === null ? null : new Date(row.finishedAt).toISOString(),
2010
- };
2011
- }
2012
- function isWorkflowRunViewRow(value) {
2013
- return (isRecord(value) &&
2014
- typeof value.runId === "string" &&
2015
- typeof value.workflowName === "string" &&
2016
- typeof value.workflowRef === "string" &&
2017
- typeof value.runStatus === "string" &&
2018
- typeof value.paused === "number" &&
2019
- Buffer.isBuffer(value.definitionDigest) &&
2020
- typeof value.status === "string" &&
2021
- (value.originSessionId === null || typeof value.originSessionId === "string") &&
2022
- (value.executionMode === "interactive" || value.executionMode === "headless") &&
2023
- (value.parentRunId === null || typeof value.parentRunId === "string") &&
2024
- typeof value.rootRunId === "string" &&
2025
- (value.lineageKind === null ||
2026
- value.lineageKind === "continuation" ||
2027
- value.lineageKind === "restart") &&
2028
- typeof value.restartNumber === "number" &&
2029
- (value.parentTerminalFingerprint === null ||
2030
- Buffer.isBuffer(value.parentTerminalFingerprint)) &&
2031
- (value.errorCode === null || typeof value.errorCode === "string") &&
2032
- (value.errorMessage === null || typeof value.errorMessage === "string") &&
2033
- typeof value.createdAt === "number" &&
2034
- typeof value.updatedAt === "number" &&
2035
- (value.startedAt === null || typeof value.startedAt === "number") &&
2036
- (value.finishedAt === null || typeof value.finishedAt === "number") &&
2037
- (value.sourceType === "builtin" || value.sourceType === "file") &&
2038
- typeof value.sourceRef === "string" &&
2039
- typeof value.sourceRevision === "string");
2040
- }
2041
- function isRunListRevisionRow(value) {
2042
- return (isRecord(value) &&
2043
- typeof value.count === "number" &&
2044
- typeof value.revisionSum === "number" &&
2045
- typeof value.updatedAt === "number");
2046
- }
2047
- function isRunRow(value) {
2048
- return isRecord(value);
2049
- }
2050
- function isRunSourceIdentityRow(value) {
2051
- return isRecord(value);
2052
- }
2053
- function isEffectRow(value) {
2054
- return isRecord(value);
2055
- }
2056
- function isCancelledRunEffectRow(value) {
2057
- return (isRecord(value) &&
2058
- typeof value.effectId === "string" &&
2059
- typeof value.resourceId === "string" &&
2060
- (value.status === "pending" || value.status === "applying") &&
2061
- typeof value.attemptCount === "number");
2062
- }
2063
- function isExpiredInteractionRow(value) {
2064
- return (isRecord(value) && typeof value.requestId === "string" && typeof value.attemptId === "string");
2065
- }
2066
- function isWorkflowRow(value) {
2067
- return isRecord(value);
2068
- }
2069
- function isQueueListRow(value) {
2070
- return isRecord(value);
2071
- }
2072
- function isResourceManagerEventRow(value) {
2073
- return isRecord(value);
2074
- }
2075
- function isRunEventRow(value) {
2076
- return isRecord(value);
2077
- }
2078
- function isProjectRow(value) {
2079
- return isRecord(value);
2080
- }
2081
- function isCanonicalPathRow(value) {
2082
- return isRecord(value) && typeof value.canonicalPath === "string";
2083
- }
2084
- function isFinalizerRow(value) {
2085
- return isRecord(value);
2086
- }
2087
- function isErrorCountRow(value) {
2088
- return isRecord(value);
2089
- }
2090
- function isQueueVersionRow(value) {
2091
- return isRecord(value);
2092
- }
2093
- function isRevisionRow(value) {
2094
- return isRecord(value);
2095
- }
2096
- function isSequenceRow(value) {
2097
- return isRecord(value);
2098
- }
2099
- function isKeyRow(value) {
2100
- return isRecord(value);
2101
- }
2102
- function isEffectIdentityRow(value) {
2103
- return isRecord(value);
2104
- }
2105
- function effectIdFor(sourceResourceId, key) {
2106
- return `effect-${createHash("sha256").update(`${sourceResourceId}\0${key}`).digest("hex").slice(0, 40)}`;
2107
- }
2108
- function projectIdFor(canonicalPath) {
2109
- return `project-${createHash("sha256").update(canonicalPath).digest("hex").slice(0, 40)}`;
2110
- }
2111
- function canonicalProjectPath(value) {
2112
- try {
2113
- return fs.realpathSync.native(value);
2114
- }
2115
- catch {
2116
- return path.resolve(value);
2117
- }
2118
- }
2119
- function digestBuffer(value) {
2120
- const hex = value.startsWith("sha256:") ? value.slice(7) : value;
2121
- if (!/^[a-f0-9]{64}$/i.test(hex))
2122
- throw new Error("Expected a SHA-256 digest");
2123
- return Buffer.from(hex, "hex");
2124
- }
2125
- function terminalFingerprintBuffer(value) {
2126
- const hex = value.startsWith("sha256:") ? value.slice(7) : value;
2127
- if (!/^[a-f0-9]{64}$/i.test(hex))
2128
- throw new Error("Expected a terminal fingerprint");
2129
- return Buffer.from(hex, "hex");
2130
- }
2131
- function queuedWorkflowSource(value) {
2132
- if (isWorkflowSource(value)) {
2133
- return { root: value, mounted: [] };
2134
- }
2135
- if (isRecord(value) &&
2136
- isWorkflowSource(value.root) &&
2137
- Array.isArray(value.mounted) &&
2138
- value.mounted.every(isMountedWorkflowSource)) {
2139
- return { root: value.root, mounted: value.mounted };
2140
- }
2141
- throw new Error("Stored workflow source identity is invalid");
2142
- }
2143
- function isWorkflowSource(value) {
2144
- return (isRecord(value) &&
2145
- ((value.kind === "builtin" &&
2146
- typeof value.id === "string" &&
2147
- typeof value.revision === "string") ||
2148
- (value.kind === "file" && typeof value.path === "string" && typeof value.hash === "string")));
2149
- }
2150
- function isMountedWorkflowSource(value) {
2151
- return (isRecord(value) &&
2152
- Array.isArray(value.mountPath) &&
2153
- value.mountPath.every((part) => typeof part === "string") &&
2154
- typeof value.workflowName === "string" &&
2155
- isWorkflowSource(value.source));
2156
- }
2157
- function insertQueuedRunSources(state, runId, value) {
2158
- const insert = state.connection.prepare(`INSERT INTO run_sources(run_id, mount_path, source_type, source_ref, source_revision)
2159
- VALUES (?, ?, ?, ?, ?)`);
2160
- const root = queuedSourceParts(value.root);
2161
- insert.run(runId, "", root.type, root.ref, root.revision);
2162
- for (const mounted of value.mounted) {
2163
- const source = queuedSourceParts(mounted.source);
2164
- insert.run(runId, mounted.mountPath.join("/"), source.type, source.ref, source.revision);
2165
- }
2166
- }
2167
- function queuedSourceParts(source) {
2168
- return source.kind === "builtin"
2169
- ? { type: "builtin", ref: source.id, revision: source.revision }
2170
- : { type: "file", ref: source.path, revision: source.hash };
2171
- }
2172
- function readQueuedRunSources(state, run) {
2173
- const rows = state.connection
2174
- .prepare(`SELECT mount_path AS mountPath, source_type AS sourceType,
2175
- source_ref AS sourceRef, source_revision AS sourceRevision
2176
- FROM run_sources WHERE run_id = ? ORDER BY mount_path`)
2177
- .all(run.runId)
2178
- .filter(isRunSourceIdentityRow);
2179
- const rootRow = rows.find((row) => row.mountPath === "");
2180
- if (rootRow === undefined)
2181
- throw new Error(`Workflow run source is missing: ${run.runId}`);
2182
- const snapshot = state.readJson(run.definitionHash);
2183
- const mounts = isRecord(snapshot) &&
2184
- isRecord(snapshot.composition) &&
2185
- Array.isArray(snapshot.composition.mounts)
2186
- ? snapshot.composition.mounts
2187
- : [];
2188
- const names = new Map(mounts.flatMap((mount) => {
2189
- if (!isRecord(mount) ||
2190
- !Array.isArray(mount.mountPath) ||
2191
- !mount.mountPath.every((part) => typeof part === "string") ||
2192
- typeof mount.workflowName !== "string") {
2193
- return [];
2194
- }
2195
- return [[mount.mountPath.join("/"), mount.workflowName]];
2196
- }));
2197
- return {
2198
- root: rowToSource(rootRow),
2199
- mounted: rows
2200
- .filter((row) => row.mountPath !== "")
2201
- .map((row) => ({
2202
- mountPath: row.mountPath.split("/"),
2203
- workflowName: names.get(row.mountPath) ?? row.mountPath,
2204
- source: rowToSource(row),
2205
- })),
2206
- };
2207
- }
2208
- function rowToSource(row) {
2209
- return row.sourceType === "builtin"
2210
- ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
2211
- : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
2212
- }
2213
- function effectStatus(value) {
2214
- return value === "indeterminate" ? "ambiguous" : value;
2215
- }
2216
- function resourceManagerEffectState(value) {
2217
- return value === "ambiguous" ? "indeterminate" : value;
2218
- }
2219
- function validTimestamp(value) {
2220
- const date = value === undefined ? new Date() : new Date(value);
2221
- if (!Number.isFinite(date.getTime()))
2222
- throw new Error("Invalid timestamp");
2223
- return date.toISOString();
2224
- }
2225
- function epoch(value) {
2226
- return Date.parse(value);
2227
- }
2228
- function validateName(value, label) {
2229
- validateKey(value, label);
2230
- }
2231
- function validateKey(value, label) {
2232
- if (value.length === 0 || value.length > 512)
2233
- throw new Error(`${label} is invalid`);
2234
- }
2235
- function validateRunId(value) {
2236
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(value))
2237
- throw new Error(`Invalid workflow run id: ${value}`);
2238
781
  }
2239
782
  //# sourceMappingURL=sqlite.js.map