@acpus/runtime 0.7.1 → 0.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/admission/input.d.ts +5 -0
  3. package/dist/admission/input.d.ts.map +1 -0
  4. package/dist/admission/input.js +17 -0
  5. package/dist/admission/input.js.map +1 -0
  6. package/dist/artifacts/rewrite.d.ts +7 -0
  7. package/dist/artifacts/rewrite.d.ts.map +1 -0
  8. package/dist/artifacts/rewrite.js +28 -0
  9. package/dist/artifacts/rewrite.js.map +1 -0
  10. package/dist/control/agent-overrides.d.ts +4 -0
  11. package/dist/control/agent-overrides.d.ts.map +1 -0
  12. package/dist/control/agent-overrides.js +40 -0
  13. package/dist/control/agent-overrides.js.map +1 -0
  14. package/dist/daemon/loop.d.ts +16 -0
  15. package/dist/daemon/loop.d.ts.map +1 -0
  16. package/dist/daemon/loop.js +191 -0
  17. package/dist/daemon/loop.js.map +1 -0
  18. package/dist/daemon/sessions.d.ts +23 -0
  19. package/dist/daemon/sessions.d.ts.map +1 -0
  20. package/dist/daemon/sessions.js +190 -0
  21. package/dist/daemon/sessions.js.map +1 -0
  22. package/dist/daemon/socket.d.ts +82 -0
  23. package/dist/daemon/socket.d.ts.map +1 -0
  24. package/dist/daemon/socket.js +341 -0
  25. package/dist/daemon/socket.js.map +1 -0
  26. package/dist/daemon/tick.d.ts +9 -0
  27. package/dist/daemon/tick.d.ts.map +1 -0
  28. package/dist/daemon/tick.js +18 -0
  29. package/dist/daemon/tick.js.map +1 -0
  30. package/dist/evaluation/admissible.d.ts +2 -0
  31. package/dist/evaluation/admissible.d.ts.map +1 -0
  32. package/dist/evaluation/admissible.js +50 -0
  33. package/dist/evaluation/admissible.js.map +1 -0
  34. package/dist/evaluation/evaluator.d.ts +14 -0
  35. package/dist/evaluation/evaluator.d.ts.map +1 -0
  36. package/dist/evaluation/evaluator.js +34 -0
  37. package/dist/evaluation/evaluator.js.map +1 -0
  38. package/dist/evaluation/loop-limit.d.ts +4 -0
  39. package/dist/evaluation/loop-limit.d.ts.map +1 -0
  40. package/dist/evaluation/loop-limit.js +9 -0
  41. package/dist/evaluation/loop-limit.js.map +1 -0
  42. package/dist/evaluation/schema.d.ts +4 -0
  43. package/dist/evaluation/schema.d.ts.map +1 -0
  44. package/dist/evaluation/schema.js +92 -0
  45. package/dist/evaluation/schema.js.map +1 -0
  46. package/dist/execution/advance.d.ts +18 -0
  47. package/dist/execution/advance.d.ts.map +1 -0
  48. package/dist/execution/advance.js +57 -0
  49. package/dist/execution/advance.js.map +1 -0
  50. package/dist/execution/agent-node.d.ts +23 -0
  51. package/dist/execution/agent-node.d.ts.map +1 -0
  52. package/dist/execution/agent-node.js +512 -0
  53. package/dist/execution/agent-node.js.map +1 -0
  54. package/dist/execution/duration.d.ts +2 -0
  55. package/dist/execution/duration.d.ts.map +1 -0
  56. package/dist/execution/duration.js +15 -0
  57. package/dist/execution/duration.js.map +1 -0
  58. package/dist/execution/ir.d.ts +3 -0
  59. package/dist/execution/ir.d.ts.map +1 -0
  60. package/dist/execution/ir.js +46 -0
  61. package/dist/execution/ir.js.map +1 -0
  62. package/dist/execution/scheduler.d.ts +59 -0
  63. package/dist/execution/scheduler.d.ts.map +1 -0
  64. package/dist/execution/scheduler.js +306 -0
  65. package/dist/execution/scheduler.js.map +1 -0
  66. package/dist/execution/task-executor.d.ts +14 -0
  67. package/dist/execution/task-executor.d.ts.map +1 -0
  68. package/dist/execution/task-executor.js +186 -0
  69. package/dist/execution/task-executor.js.map +1 -0
  70. package/dist/hooks/config.d.ts +32 -0
  71. package/dist/hooks/config.d.ts.map +1 -0
  72. package/dist/hooks/config.js +112 -0
  73. package/dist/hooks/config.js.map +1 -0
  74. package/dist/hooks/context.d.ts +50 -0
  75. package/dist/hooks/context.d.ts.map +1 -0
  76. package/dist/hooks/context.js +116 -0
  77. package/dist/hooks/context.js.map +1 -0
  78. package/dist/hooks/events.d.ts +22 -0
  79. package/dist/hooks/events.d.ts.map +1 -0
  80. package/dist/hooks/events.js +41 -0
  81. package/dist/hooks/events.js.map +1 -0
  82. package/dist/hooks/journal.d.ts +21 -19
  83. package/dist/hooks/journal.d.ts.map +1 -1
  84. package/dist/hooks/journal.js +1 -48
  85. package/dist/hooks/journal.js.map +1 -1
  86. package/dist/hooks/loader.d.ts +33 -39
  87. package/dist/hooks/loader.d.ts.map +1 -1
  88. package/dist/hooks/loader.js +79 -95
  89. package/dist/hooks/loader.js.map +1 -1
  90. package/dist/hooks/runner.d.ts +14 -41
  91. package/dist/hooks/runner.d.ts.map +1 -1
  92. package/dist/hooks/runner.js +177 -145
  93. package/dist/hooks/runner.js.map +1 -1
  94. package/dist/index.d.ts +13 -28
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +13 -33
  97. package/dist/index.js.map +1 -1
  98. package/dist/runs/advance-runtime.d.ts +50 -0
  99. package/dist/runs/advance-runtime.d.ts.map +1 -0
  100. package/dist/runs/advance-runtime.js +82 -0
  101. package/dist/runs/advance-runtime.js.map +1 -0
  102. package/dist/runs/use-cases.d.ts +111 -0
  103. package/dist/runs/use-cases.d.ts.map +1 -0
  104. package/dist/runs/use-cases.js +539 -0
  105. package/dist/runs/use-cases.js.map +1 -0
  106. package/dist/scheduler/advance.d.ts +65 -0
  107. package/dist/scheduler/advance.d.ts.map +1 -0
  108. package/dist/scheduler/advance.js +435 -0
  109. package/dist/scheduler/advance.js.map +1 -0
  110. package/dist/scheduler/control.d.ts +49 -0
  111. package/dist/scheduler/control.d.ts.map +1 -0
  112. package/dist/scheduler/control.js +212 -0
  113. package/dist/scheduler/control.js.map +1 -0
  114. package/dist/scheduler/event-codec.d.ts +4 -0
  115. package/dist/scheduler/event-codec.d.ts.map +1 -0
  116. package/dist/scheduler/event-codec.js +57 -0
  117. package/dist/scheduler/event-codec.js.map +1 -0
  118. package/dist/scheduler/events.d.ts +193 -0
  119. package/dist/scheduler/events.d.ts.map +1 -0
  120. package/dist/scheduler/events.js +13 -0
  121. package/dist/scheduler/events.js.map +1 -0
  122. package/dist/scheduler/fork-seed.d.ts +38 -0
  123. package/dist/scheduler/fork-seed.d.ts.map +1 -0
  124. package/dist/scheduler/fork-seed.js +590 -0
  125. package/dist/scheduler/fork-seed.js.map +1 -0
  126. package/dist/scheduler/identity.d.ts +8 -0
  127. package/dist/scheduler/identity.d.ts.map +1 -0
  128. package/dist/scheduler/identity.js +49 -0
  129. package/dist/scheduler/identity.js.map +1 -0
  130. package/dist/scheduler/ir-walk.d.ts +3 -0
  131. package/dist/scheduler/ir-walk.d.ts.map +1 -0
  132. package/dist/scheduler/ir-walk.js +25 -0
  133. package/dist/scheduler/ir-walk.js.map +1 -0
  134. package/dist/scheduler/limiter.d.ts +7 -0
  135. package/dist/scheduler/limiter.d.ts.map +1 -0
  136. package/dist/scheduler/limiter.js +10 -0
  137. package/dist/scheduler/limiter.js.map +1 -0
  138. package/dist/scheduler/materialize.d.ts +7 -0
  139. package/dist/scheduler/materialize.d.ts.map +1 -0
  140. package/dist/scheduler/materialize.js +756 -0
  141. package/dist/scheduler/materialize.js.map +1 -0
  142. package/dist/scheduler/membership.d.ts +12 -0
  143. package/dist/scheduler/membership.d.ts.map +1 -0
  144. package/dist/scheduler/membership.js +74 -0
  145. package/dist/scheduler/membership.js.map +1 -0
  146. package/dist/scheduler/node-executor.d.ts +15 -0
  147. package/dist/scheduler/node-executor.d.ts.map +1 -0
  148. package/dist/scheduler/node-executor.js +69 -0
  149. package/dist/scheduler/node-executor.js.map +1 -0
  150. package/dist/scheduler/runtime-runner.d.ts +44 -0
  151. package/dist/scheduler/runtime-runner.d.ts.map +1 -0
  152. package/dist/scheduler/runtime-runner.js +155 -0
  153. package/dist/scheduler/runtime-runner.js.map +1 -0
  154. package/dist/scheduler/scope.d.ts +6 -0
  155. package/dist/scheduler/scope.d.ts.map +1 -0
  156. package/dist/scheduler/scope.js +56 -0
  157. package/dist/scheduler/scope.js.map +1 -0
  158. package/dist/scheduler/store-port.d.ts +211 -0
  159. package/dist/scheduler/store-port.d.ts.map +1 -0
  160. package/dist/scheduler/store-port.js +28 -0
  161. package/dist/scheduler/store-port.js.map +1 -0
  162. package/dist/scheduler/transitions.d.ts +89 -0
  163. package/dist/scheduler/transitions.d.ts.map +1 -0
  164. package/dist/scheduler/transitions.js +967 -0
  165. package/dist/scheduler/transitions.js.map +1 -0
  166. package/dist/scheduler/types.d.ts +146 -0
  167. package/dist/scheduler/types.d.ts.map +1 -0
  168. package/dist/{executors → scheduler}/types.js.map +1 -1
  169. package/dist/store/store.d.ts +390 -0
  170. package/dist/store/store.d.ts.map +1 -0
  171. package/dist/store/store.js +3053 -0
  172. package/dist/store/store.js.map +1 -0
  173. package/dist/visualization/overlay.d.ts +86 -0
  174. package/dist/visualization/overlay.d.ts.map +1 -0
  175. package/dist/visualization/overlay.js +161 -0
  176. package/dist/visualization/overlay.js.map +1 -0
  177. package/package.json +19 -37
  178. package/README.md +0 -7
  179. package/dist/agent-telemetry.d.ts +0 -56
  180. package/dist/agent-telemetry.d.ts.map +0 -1
  181. package/dist/agent-telemetry.js +0 -304
  182. package/dist/agent-telemetry.js.map +0 -1
  183. package/dist/artifacts.d.ts +0 -45
  184. package/dist/artifacts.d.ts.map +0 -1
  185. package/dist/artifacts.js +0 -163
  186. package/dist/artifacts.js.map +0 -1
  187. package/dist/attempt-artifacts.d.ts +0 -35
  188. package/dist/attempt-artifacts.d.ts.map +0 -1
  189. package/dist/attempt-artifacts.js +0 -90
  190. package/dist/attempt-artifacts.js.map +0 -1
  191. package/dist/client.d.ts +0 -82
  192. package/dist/client.d.ts.map +0 -1
  193. package/dist/client.js +0 -200
  194. package/dist/client.js.map +0 -1
  195. package/dist/evaluator.d.ts +0 -36
  196. package/dist/evaluator.d.ts.map +0 -1
  197. package/dist/evaluator.js +0 -101
  198. package/dist/evaluator.js.map +0 -1
  199. package/dist/executors/agent.d.ts +0 -101
  200. package/dist/executors/agent.d.ts.map +0 -1
  201. package/dist/executors/agent.js +0 -508
  202. package/dist/executors/agent.js.map +0 -1
  203. package/dist/executors/mock-program.d.ts +0 -26
  204. package/dist/executors/mock-program.d.ts.map +0 -1
  205. package/dist/executors/mock-program.js +0 -100
  206. package/dist/executors/mock-program.js.map +0 -1
  207. package/dist/executors/output-preview.d.ts +0 -2
  208. package/dist/executors/output-preview.d.ts.map +0 -1
  209. package/dist/executors/output-preview.js +0 -20
  210. package/dist/executors/output-preview.js.map +0 -1
  211. package/dist/executors/program.d.ts +0 -30
  212. package/dist/executors/program.d.ts.map +0 -1
  213. package/dist/executors/program.js +0 -230
  214. package/dist/executors/program.js.map +0 -1
  215. package/dist/executors/types.d.ts +0 -32
  216. package/dist/executors/types.d.ts.map +0 -1
  217. package/dist/fork.d.ts +0 -74
  218. package/dist/fork.d.ts.map +0 -1
  219. package/dist/fork.js +0 -278
  220. package/dist/fork.js.map +0 -1
  221. package/dist/hooks/payload.d.ts +0 -17
  222. package/dist/hooks/payload.d.ts.map +0 -1
  223. package/dist/hooks/payload.js +0 -45
  224. package/dist/hooks/payload.js.map +0 -1
  225. package/dist/interpreter.d.ts +0 -168
  226. package/dist/interpreter.d.ts.map +0 -1
  227. package/dist/interpreter.js +0 -1747
  228. package/dist/interpreter.js.map +0 -1
  229. package/dist/keys.d.ts +0 -77
  230. package/dist/keys.d.ts.map +0 -1
  231. package/dist/keys.js +0 -375
  232. package/dist/keys.js.map +0 -1
  233. package/dist/run-control.d.ts +0 -45
  234. package/dist/run-control.d.ts.map +0 -1
  235. package/dist/run-control.js +0 -255
  236. package/dist/run-control.js.map +0 -1
  237. package/dist/run-id.d.ts +0 -3
  238. package/dist/run-id.d.ts.map +0 -1
  239. package/dist/run-id.js +0 -10
  240. package/dist/run-id.js.map +0 -1
  241. package/dist/state-machine.d.ts +0 -31
  242. package/dist/state-machine.d.ts.map +0 -1
  243. package/dist/state-machine.js +0 -102
  244. package/dist/state-machine.js.map +0 -1
  245. package/dist/store.d.ts +0 -92
  246. package/dist/store.d.ts.map +0 -1
  247. package/dist/store.js +0 -420
  248. package/dist/store.js.map +0 -1
  249. package/dist/supervisor-app.d.ts +0 -27
  250. package/dist/supervisor-app.d.ts.map +0 -1
  251. package/dist/supervisor-app.js +0 -694
  252. package/dist/supervisor-app.js.map +0 -1
  253. package/dist/supervisor-discovery.d.ts +0 -18
  254. package/dist/supervisor-discovery.d.ts.map +0 -1
  255. package/dist/supervisor-discovery.js +0 -180
  256. package/dist/supervisor-discovery.js.map +0 -1
  257. package/dist/supervisor-entry.d.ts +0 -11
  258. package/dist/supervisor-entry.d.ts.map +0 -1
  259. package/dist/supervisor-entry.js +0 -50
  260. package/dist/supervisor-entry.js.map +0 -1
  261. package/dist/supervisor-lock.d.ts +0 -26
  262. package/dist/supervisor-lock.d.ts.map +0 -1
  263. package/dist/supervisor-lock.js +0 -49
  264. package/dist/supervisor-lock.js.map +0 -1
  265. package/dist/supervisor-runner.d.ts +0 -11
  266. package/dist/supervisor-runner.d.ts.map +0 -1
  267. package/dist/supervisor-runner.js +0 -128
  268. package/dist/supervisor-runner.js.map +0 -1
  269. package/dist/types.d.ts +0 -375
  270. package/dist/types.d.ts.map +0 -1
  271. package/dist/types.js +0 -7
  272. package/dist/types.js.map +0 -1
  273. package/dist/validate-input.d.ts +0 -27
  274. package/dist/validate-input.d.ts.map +0 -1
  275. package/dist/validate-input.js +0 -56
  276. package/dist/validate-input.js.map +0 -1
  277. package/dist/validate-signal.d.ts +0 -14
  278. package/dist/validate-signal.d.ts.map +0 -1
  279. package/dist/validate-signal.js +0 -31
  280. package/dist/validate-signal.js.map +0 -1
  281. package/dist/workflow-context.d.ts +0 -4
  282. package/dist/workflow-context.d.ts.map +0 -1
  283. package/dist/workflow-context.js +0 -11
  284. package/dist/workflow-context.js.map +0 -1
  285. package/dist/workflow-outputs.d.ts +0 -9
  286. package/dist/workflow-outputs.d.ts.map +0 -1
  287. package/dist/workflow-outputs.js +0 -60
  288. package/dist/workflow-outputs.js.map +0 -1
  289. /package/dist/{executors → scheduler}/types.js +0 -0
@@ -0,0 +1,3053 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { access, cp, lstat, mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { valueToExprIR } from "@acpus/expression/ir";
6
+ import { ArtifactRewriteError, rewriteArtifactValue } from "../artifacts/rewrite.js";
7
+ import { compactUndefined, parseAgentOverrideMap } from "../control/agent-overrides.js";
8
+ import { evaluateExpr } from "../evaluation/evaluator.js";
9
+ import { applySchedulerEvents, applyTimestampedSchedulerEvents, cancellationEventsForFrame, cancellationEventsForNode, createSchedulerProjection } from "../scheduler/transitions.js";
10
+ import { ancestorGroupMembersForFrame, ancestorGroupMembersForNode } from "../scheduler/membership.js";
11
+ import { ForkSeedPlanError, planTargetedForkSeed } from "../scheduler/fork-seed.js";
12
+ import { SchedulerStoreException, schedulerStoreResult } from "../scheduler/store-port.js";
13
+ import { drainDerivedTransitions } from "../scheduler/advance.js";
14
+ import { continueRootEvents } from "../scheduler/materialize.js";
15
+ import { decodeSchedulerPayload, isSchedulerEventType } from "../scheduler/event-codec.js";
16
+ import { decodeCommittedRuntimeEventRow } from "../hooks/events.js";
17
+ const RUNNABLE_RUNS_WHERE = `
18
+ status = 'pending'
19
+ AND NOT EXISTS (
20
+ SELECT 1
21
+ FROM node_states
22
+ WHERE node_states.run_id = runs.id
23
+ AND node_states.status = 'pending'
24
+ AND node_states.error_json IS NOT NULL
25
+ )
26
+ `;
27
+ const RECOVERABLE_RUNNING_RUNS_WHERE = `
28
+ status = 'running'
29
+ AND NOT EXISTS (
30
+ SELECT 1
31
+ FROM signal_waits
32
+ WHERE signal_waits.run_id = runs.id
33
+ AND signal_waits.status = 'awaiting'
34
+ )
35
+ AND NOT EXISTS (
36
+ SELECT 1
37
+ FROM run_leases
38
+ WHERE run_leases.run_id = runs.id
39
+ AND run_leases.released_at IS NULL
40
+ AND run_leases.lease_expires_at > ?
41
+ )
42
+ `;
43
+ const TIMED_SIGNAL_WAIT_WHERE = `
44
+ status NOT IN ('paused', 'failed', 'completed', 'canceled')
45
+ AND EXISTS (
46
+ SELECT 1
47
+ FROM signal_waits
48
+ WHERE signal_waits.run_id = runs.id
49
+ AND signal_waits.status = 'awaiting'
50
+ AND signal_waits.deadline_at IS NOT NULL
51
+ )
52
+ `;
53
+ const DUE_SIGNAL_WAIT_WHERE = `
54
+ ${TIMED_SIGNAL_WAIT_WHERE}
55
+ AND EXISTS (
56
+ SELECT 1
57
+ FROM signal_waits
58
+ WHERE signal_waits.run_id = runs.id
59
+ AND signal_waits.status = 'awaiting'
60
+ AND signal_waits.deadline_at IS NOT NULL
61
+ AND signal_waits.deadline_at <= ?
62
+ )
63
+ `;
64
+ const localStateRoot = ".acpus/.local";
65
+ const runIdPattern = /^\d{14}[A-F0-9]{20}$/;
66
+ export async function openRuntimeStore(cwd) {
67
+ const stateDir = join(cwd, localStateRoot, "state");
68
+ await mkdir(stateDir, { recursive: true });
69
+ const db = openDatabase(join(stateDir, "runtime.db"));
70
+ db.exec("PRAGMA journal_mode = WAL;");
71
+ migrate(db);
72
+ return new SqliteRuntimeStore(db, cwd);
73
+ }
74
+ export async function openExistingRuntimeStore(cwd) {
75
+ return openExistingStore(cwd, true);
76
+ }
77
+ export async function openExistingWritableRuntimeStore(cwd) {
78
+ return openExistingStore(cwd, false);
79
+ }
80
+ async function openExistingStore(cwd, readOnly) {
81
+ const path = join(cwd, localStateRoot, "state", "runtime.db");
82
+ try {
83
+ await access(path);
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ if (readOnly) {
89
+ const db = openDatabase(path, true);
90
+ if (!storeNeedsMigration(db))
91
+ return new SqliteRuntimeStore(db, cwd);
92
+ db.close();
93
+ throw new Error("Runtime store requires migration; run a writable Acpus command before read-only inspection.");
94
+ }
95
+ const db = openDatabase(path);
96
+ migrate(db);
97
+ return new SqliteRuntimeStore(db, cwd);
98
+ }
99
+ class SqliteRuntimeStore {
100
+ db;
101
+ cwd;
102
+ schedulerPort;
103
+ constructor(db, cwd) {
104
+ this.db = db;
105
+ this.cwd = cwd;
106
+ }
107
+ get scheduler() {
108
+ return this.schedulerStore();
109
+ }
110
+ schedulerStore() {
111
+ this.schedulerPort ??= new SqliteSchedulerStorePort(this.db, this.cwd);
112
+ return this.schedulerPort;
113
+ }
114
+ runRecordColumns() {
115
+ return "id, name, status, workflow_entry, ir_digest, source_graph_digest, created_at, updated_at, progress_version, progress_updated_at";
116
+ }
117
+ close() {
118
+ this.db.close();
119
+ }
120
+ async admitRun(input) {
121
+ const runId = newRunId();
122
+ const now = new Date().toISOString();
123
+ const workflowEntry = relative(input.cwd, input.prepared.workflowPath);
124
+ const runDir = join(input.cwd, localStateRoot, "runs", runId);
125
+ try {
126
+ await mkdir(runDir, { recursive: true });
127
+ await writeFile(join(runDir, "workflow.ir.json"), input.prepared.irJson);
128
+ await writeFile(join(runDir, "lock.json"), `${JSON.stringify(input.prepared.lock, null, 2)}\n`);
129
+ const agentOverrides = normalizeAgentOverrides(input.prepared.ir, input.agentOverrides);
130
+ const eventPayload = {
131
+ workflow: summarizeWorkflowForEvent(input.prepared.ir),
132
+ input: input.input,
133
+ lock: input.prepared.lock,
134
+ ...(Object.keys(agentOverrides).length > 0 ? { agentOverrides } : {}),
135
+ };
136
+ this.db.exec("BEGIN IMMEDIATE");
137
+ try {
138
+ this.db.prepare(`
139
+ INSERT INTO runs (id, name, status, workflow_entry, ir_digest, source_graph_digest, created_at, updated_at)
140
+ VALUES (?, ?, 'pending', ?, ?, ?, ?, ?)
141
+ `).run(runId, input.prepared.ir.name, workflowEntry, input.prepared.irDigest, input.prepared.sourceGraphDigest, now, now);
142
+ this.db.prepare(`
143
+ INSERT INTO run_inputs (
144
+ run_id, workflow_ir_json, input_json, agent_overrides_json, lock_json, package_lock_digest, run_dir, created_at
145
+ )
146
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
147
+ `).run(runId, input.prepared.irJson, stableJson(input.input), stableJson(agentOverrides), stableJson(input.prepared.lock), input.prepared.packageLockDigest ?? null, relative(input.cwd, runDir), now);
148
+ this.db.prepare(`
149
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
150
+ VALUES (?, 1, 'run.admitted', NULL, ?, ?, ?)
151
+ `).run(runId, stableJson(eventPayload), now, `admit:${runId}:${input.prepared.irDigest}`);
152
+ for (const nodeId of collectNodeIds(input.prepared.ir.root)) {
153
+ this.db.prepare(`
154
+ INSERT INTO node_states (run_id, node_key, node_id, status, created_at, updated_at)
155
+ VALUES (?, ?, ?, 'pending', ?, ?)
156
+ `).run(runId, nodeId, nodeId, now, now);
157
+ }
158
+ this.db.exec("COMMIT");
159
+ }
160
+ catch (error) {
161
+ this.db.exec("ROLLBACK");
162
+ throw error;
163
+ }
164
+ }
165
+ catch (error) {
166
+ await rm(runDir, { recursive: true, force: true });
167
+ throw error;
168
+ }
169
+ const record = this.getRunRecord(runId);
170
+ if (!record)
171
+ throw new Error(`Admitted run ${runId} was not persisted.`);
172
+ return record;
173
+ }
174
+ completeRun(input) {
175
+ const now = new Date().toISOString();
176
+ const output = assertJsonValue(input.output, "run output");
177
+ const nodeOutputs = Object.fromEntries(Object.entries(input.nodes).map(([nodeKey, node]) => [
178
+ nodeKey,
179
+ assertJsonValue(node.output, `node '${nodeKey}' output`),
180
+ ]));
181
+ this.db.exec("BEGIN IMMEDIATE");
182
+ try {
183
+ const nextSequence = this.nextSequence(input.runId);
184
+ const transition = this.db.prepare(`
185
+ UPDATE runs
186
+ SET status = 'completed', updated_at = ?
187
+ WHERE id = ? AND status IN ('pending', 'running')
188
+ `).run(now, input.runId);
189
+ if (transition.changes !== 1)
190
+ throw new Error(`Run '${input.runId}' cannot transition to completed.`);
191
+ for (const [nodeKey, output] of Object.entries(nodeOutputs)) {
192
+ const update = this.db.prepare(`
193
+ UPDATE node_states
194
+ SET status = 'completed', output_json = ?, error_json = NULL, updated_at = ?
195
+ WHERE run_id = ? AND node_key = ?
196
+ `).run(stableJson(output), now, input.runId, nodeKey);
197
+ if (update.changes !== 1)
198
+ throw new Error(`Node '${nodeKey}' was not found for run '${input.runId}'.`);
199
+ }
200
+ this.db.prepare(`
201
+ UPDATE run_inputs
202
+ SET output_json = ?
203
+ WHERE run_id = ?
204
+ `).run(stableJson(output), input.runId);
205
+ this.db.prepare(`
206
+ UPDATE node_states
207
+ SET status = 'skipped', updated_at = ?
208
+ WHERE run_id = ? AND status = 'pending'
209
+ `).run(now, input.runId);
210
+ this.db.prepare(`
211
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
212
+ VALUES (?, ?, 'run.completed', NULL, ?, ?, ?)
213
+ `).run(input.runId, nextSequence, stableJson({ output }), now, `complete:${input.runId}`);
214
+ this.db.exec("COMMIT");
215
+ }
216
+ catch (error) {
217
+ this.db.exec("ROLLBACK");
218
+ throw error;
219
+ }
220
+ const record = this.getRunRecord(input.runId);
221
+ if (!record)
222
+ throw new Error(`Completed run ${input.runId} was not persisted.`);
223
+ return record;
224
+ }
225
+ persistCompletedNodes(input) {
226
+ const now = new Date().toISOString();
227
+ const nodeOutputs = Object.fromEntries(Object.entries(input.nodes).map(([nodeKey, node]) => [
228
+ nodeKey,
229
+ assertJsonValue(node.output, `node '${nodeKey}' output`),
230
+ ]));
231
+ this.db.exec("BEGIN IMMEDIATE");
232
+ try {
233
+ for (const [nodeKey, output] of Object.entries(nodeOutputs)) {
234
+ const update = this.db.prepare(`
235
+ UPDATE node_states
236
+ SET status = 'completed', output_json = ?, error_json = NULL, updated_at = ?
237
+ WHERE run_id = ? AND node_key = ?
238
+ `).run(stableJson(output), now, input.runId, nodeKey);
239
+ if (update.changes !== 1)
240
+ throw new Error(`Node '${nodeKey}' was not found for run '${input.runId}'.`);
241
+ }
242
+ this.db.exec("COMMIT");
243
+ }
244
+ catch (error) {
245
+ this.db.exec("ROLLBACK");
246
+ throw error;
247
+ }
248
+ return this.requireRun(input.runId);
249
+ }
250
+ blockRun(input) {
251
+ const now = new Date().toISOString();
252
+ const payload = { message: input.message, nodeKey: input.nodeKey };
253
+ this.db.prepare(`
254
+ UPDATE node_states
255
+ SET error_json = ?, updated_at = ?
256
+ WHERE run_id = ? AND node_key = ? AND status = 'pending'
257
+ `).run(stableJson(payload), now, input.runId, input.nodeKey);
258
+ return this.requireRun(input.runId);
259
+ }
260
+ failRun(input) {
261
+ const now = new Date().toISOString();
262
+ const payload = {
263
+ message: input.message,
264
+ ...(input.nodeKey ? { nodeKey: input.nodeKey } : {}),
265
+ };
266
+ this.db.exec("BEGIN IMMEDIATE");
267
+ try {
268
+ const nextSequence = this.nextSequence(input.runId);
269
+ const transition = this.db.prepare(`
270
+ UPDATE runs
271
+ SET status = 'failed', updated_at = ?
272
+ WHERE id = ? AND status IN ('pending', 'running')
273
+ `).run(now, input.runId);
274
+ if (transition.changes !== 1)
275
+ throw new Error(`Run '${input.runId}' cannot transition to failed.`);
276
+ if (input.nodeKey) {
277
+ this.db.prepare(`
278
+ UPDATE node_states
279
+ SET status = 'failed', error_json = ?, updated_at = ?
280
+ WHERE run_id = ? AND node_key = ?
281
+ `).run(stableJson(payload), now, input.runId, input.nodeKey);
282
+ }
283
+ this.db.prepare(`
284
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
285
+ VALUES (?, ?, 'run.failed', ?, ?, ?, ?)
286
+ `).run(input.runId, nextSequence, input.nodeKey ?? null, stableJson(payload), now, `fail:${input.runId}:${nextSequence}`);
287
+ this.db.exec("COMMIT");
288
+ }
289
+ catch (error) {
290
+ this.db.exec("ROLLBACK");
291
+ throw error;
292
+ }
293
+ const record = this.getRunRecord(input.runId);
294
+ if (!record)
295
+ throw new Error(`Failed run ${input.runId} was not persisted.`);
296
+ return record;
297
+ }
298
+ awaitSignal(input) {
299
+ const now = new Date().toISOString();
300
+ const nodeOutputs = Object.fromEntries(Object.entries(input.nodes).map(([nodeKey, node]) => [
301
+ nodeKey,
302
+ assertJsonValue(node.output, `node '${nodeKey}' output`),
303
+ ]));
304
+ this.db.exec("BEGIN IMMEDIATE");
305
+ try {
306
+ const nextSequence = this.nextSequence(input.runId);
307
+ const transition = this.db.prepare(`
308
+ UPDATE runs
309
+ SET status = 'awaiting', updated_at = ?
310
+ WHERE id = ? AND status IN ('pending', 'running')
311
+ `).run(now, input.runId);
312
+ if (transition.changes !== 1)
313
+ throw new Error(`Run '${input.runId}' cannot transition to awaiting.`);
314
+ for (const [nodeKey, output] of Object.entries(nodeOutputs)) {
315
+ const update = this.db.prepare(`
316
+ UPDATE node_states
317
+ SET status = 'completed', output_json = ?, error_json = NULL, updated_at = ?
318
+ WHERE run_id = ? AND node_key = ?
319
+ `).run(stableJson(output), now, input.runId, nodeKey);
320
+ if (update.changes !== 1)
321
+ throw new Error(`Node '${nodeKey}' was not found for run '${input.runId}'.`);
322
+ }
323
+ this.db.prepare(`
324
+ UPDATE node_states
325
+ SET status = 'awaiting', updated_at = ?
326
+ WHERE run_id = ? AND node_key = ?
327
+ `).run(now, input.runId, input.nodeKey);
328
+ this.db.prepare(`
329
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
330
+ VALUES (?, ?, 'signal.awaiting', ?, ?, ?, ?)
331
+ `).run(input.runId, nextSequence, input.nodeKey, stableJson({}), now, `await:${input.runId}:${input.nodeKey}`);
332
+ this.db.exec("COMMIT");
333
+ }
334
+ catch (error) {
335
+ this.db.exec("ROLLBACK");
336
+ throw error;
337
+ }
338
+ return this.requireRun(input.runId);
339
+ }
340
+ getSignalPayloads(runId) {
341
+ const rows = this.db.prepare(`
342
+ SELECT node_key, node_id, output_json
343
+ FROM node_states
344
+ WHERE run_id = ? AND status = 'completed' AND output_json IS NOT NULL
345
+ `).all(runId);
346
+ return completedOutputMap(rows.map(row => ({
347
+ nodeKey: String(row.node_key),
348
+ nodeId: String(row.node_id),
349
+ output: JSON.parse(String(row.output_json)),
350
+ })));
351
+ }
352
+ getCompletedNodeOutputs(runId) {
353
+ return completedOutputMap(completedSchedulerOutputRows(this.db, runId));
354
+ }
355
+ getFrozenRun(runId) {
356
+ const row = this.db.prepare(`
357
+ SELECT runs.id, runs.name, runs.workflow_entry, run_inputs.workflow_ir_json, run_inputs.input_json, run_inputs.agent_overrides_json
358
+ FROM run_inputs
359
+ JOIN runs ON runs.id = run_inputs.run_id
360
+ WHERE run_inputs.run_id = ?
361
+ `).get(runId);
362
+ if (!row?.workflow_ir_json)
363
+ return undefined;
364
+ const originalIr = JSON.parse(row.workflow_ir_json);
365
+ const agentOverrides = parseAgentOverrides(row.agent_overrides_json);
366
+ return {
367
+ ir: withAgentOverrides(originalIr, agentOverrides),
368
+ input: JSON.parse(row.input_json),
369
+ agentOverrides,
370
+ meta: {
371
+ runId: String(row.id),
372
+ workflowPath: String(row.workflow_entry),
373
+ workflowName: String(row.name),
374
+ workspaceDir: resolve(this.cwd),
375
+ },
376
+ };
377
+ }
378
+ claimDaemon(input) {
379
+ const now = new Date().toISOString();
380
+ const existing = this.db.prepare(`
381
+ SELECT generation
382
+ FROM daemon_lease
383
+ WHERE workspace_realpath = ?
384
+ `).get(input.workspaceRealpath);
385
+ const generation = (existing?.generation ?? 0) + 1;
386
+ this.db.prepare(`
387
+ INSERT INTO daemon_lease (
388
+ workspace_realpath, generation, pid, heartbeat_at,
389
+ idle_since_at, idle_stop_ms, protocol_version, package_version, node_version, exec_path, updated_at
390
+ )
391
+ VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?)
392
+ ON CONFLICT(workspace_realpath) DO UPDATE SET
393
+ generation = excluded.generation,
394
+ pid = excluded.pid,
395
+ heartbeat_at = excluded.heartbeat_at,
396
+ idle_since_at = excluded.idle_since_at,
397
+ idle_stop_ms = excluded.idle_stop_ms,
398
+ protocol_version = excluded.protocol_version,
399
+ package_version = excluded.package_version,
400
+ node_version = excluded.node_version,
401
+ exec_path = excluded.exec_path,
402
+ updated_at = excluded.updated_at
403
+ `).run(input.workspaceRealpath, generation, input.pid, now, input.idleStopMs, input.protocolVersion, input.packageVersion, input.nodeVersion, input.execPath, now);
404
+ return { workspaceRealpath: input.workspaceRealpath, generation, pid: input.pid, heartbeatAt: now };
405
+ }
406
+ heartbeatDaemon(input) {
407
+ const now = new Date().toISOString();
408
+ const result = this.db.prepare(`
409
+ UPDATE daemon_lease
410
+ SET heartbeat_at = ?, updated_at = ?
411
+ WHERE workspace_realpath = ? AND generation = ?
412
+ `).run(now, now, input.workspaceRealpath, input.generation);
413
+ return result.changes === 1;
414
+ }
415
+ setDaemonIdleState(input) {
416
+ const now = new Date().toISOString();
417
+ const result = this.db.prepare(`
418
+ UPDATE daemon_lease
419
+ SET idle_since_at = ?, idle_stop_ms = ?, updated_at = ?
420
+ WHERE workspace_realpath = ? AND generation = ?
421
+ `).run(input.idleSinceAt ?? null, input.idleStopMs, now, input.workspaceRealpath, input.generation);
422
+ return result.changes === 1;
423
+ }
424
+ releaseDaemon(input) {
425
+ const result = this.db.prepare(`
426
+ DELETE FROM daemon_lease
427
+ WHERE workspace_realpath = ? AND generation = ?
428
+ `).run(input.workspaceRealpath, input.generation);
429
+ return result.changes === 1;
430
+ }
431
+ listRunnableRuns() {
432
+ const nowIso = new Date().toISOString();
433
+ return this.db.prepare(`
434
+ SELECT ${this.runRecordColumns()}
435
+ FROM runs
436
+ WHERE (${RUNNABLE_RUNS_WHERE}) OR (${RECOVERABLE_RUNNING_RUNS_WHERE})
437
+ ORDER BY created_at ASC
438
+ `).all(nowIso).map(toRunRecord);
439
+ }
440
+ listDaemonWork(now = new Date()) {
441
+ const nowIso = now.toISOString();
442
+ const startableRuns = this.db.prepare(`
443
+ SELECT ${this.runRecordColumns()}
444
+ FROM runs
445
+ WHERE (${RUNNABLE_RUNS_WHERE}) OR (${DUE_SIGNAL_WAIT_WHERE}) OR (${RECOVERABLE_RUNNING_RUNS_WHERE})
446
+ ORDER BY created_at ASC
447
+ `).all(nowIso, nowIso).map(toRunRecord);
448
+ const row = this.db.prepare(`
449
+ SELECT COUNT(*) AS count
450
+ FROM runs
451
+ WHERE (${RUNNABLE_RUNS_WHERE}) OR (${TIMED_SIGNAL_WAIT_WHERE}) OR (${RECOVERABLE_RUNNING_RUNS_WHERE})
452
+ `).get(nowIso);
453
+ return { startableRuns, idleBlockers: Number(row.count) };
454
+ }
455
+ countRunnableRuns() {
456
+ const nowIso = new Date().toISOString();
457
+ const row = this.db.prepare(`
458
+ SELECT COUNT(*) AS count
459
+ FROM runs
460
+ WHERE (${RUNNABLE_RUNS_WHERE}) OR (${RECOVERABLE_RUNNING_RUNS_WHERE})
461
+ `).get(nowIso);
462
+ return Number(row.count);
463
+ }
464
+ async forkRun(runId, options = {}) {
465
+ const forkRequestKey = options.requestId === undefined ? undefined : `fork-request:${options.requestId}`;
466
+ const requestFingerprint = forkRequestFingerprint(runId, options);
467
+ if (forkRequestKey) {
468
+ const existing = this.db.prepare(`
469
+ SELECT run_id, payload_json
470
+ FROM run_events
471
+ WHERE idempotency_key = ? AND type = 'run.forked'
472
+ `).get(forkRequestKey);
473
+ if (existing) {
474
+ const payload = JSON.parse(existing.payload_json);
475
+ if (payload.requestFingerprint !== requestFingerprint) {
476
+ throw new Error(`Fork request '${options.requestId}' conflicts with a different fork input.`);
477
+ }
478
+ return { ...this.requireRun(existing.run_id), forkCreated: false };
479
+ }
480
+ }
481
+ const matchingFork = this.db.prepare(`
482
+ SELECT run_id, payload_json
483
+ FROM run_events
484
+ WHERE type = 'run.forked'
485
+ `).all()
486
+ .find(row => JSON.parse(row.payload_json).requestFingerprint === requestFingerprint);
487
+ if (matchingFork)
488
+ return { ...this.requireRun(matchingFork.run_id), forkCreated: false };
489
+ const source = this.getRunRecord(runId);
490
+ if (!source)
491
+ throw new Error(`Run '${runId}' was not found.`);
492
+ const input = this.db.prepare(`
493
+ SELECT workflow_ir_json, input_json, agent_overrides_json, lock_json, output_json, package_lock_digest, run_dir
494
+ FROM run_inputs
495
+ WHERE run_id = ?
496
+ `).get(runId);
497
+ if (!input?.workflow_ir_json || !input.lock_json)
498
+ throw new Error(`Run '${runId}' has no frozen input.`);
499
+ const forkIrJson = options.prepared?.irJson ?? input.workflow_ir_json;
500
+ if (options.prepared && digest(Buffer.from(forkIrJson)) !== options.prepared.irDigest)
501
+ throw new Error("Fork prepared workflow IR digest does not match payload.");
502
+ const forkIr = JSON.parse(forkIrJson);
503
+ const sourceAgentOverrides = parseAgentOverrides(input.agent_overrides_json);
504
+ const forkAgentOverrides = normalizeAgentOverrides(forkIr, options.agentOverrides, sourceAgentOverrides);
505
+ const sourceIr = JSON.parse(input.workflow_ir_json);
506
+ const sourceEffectiveIr = withAgentOverrides(sourceIr, sourceAgentOverrides);
507
+ const forkEffectiveIr = withAgentOverrides(forkIr, forkAgentOverrides);
508
+ const forkInputJson = options.input === undefined ? input.input_json : stableJson(options.input);
509
+ const forkLockJson = options.prepared ? stableJson(options.prepared.lock) : input.lock_json;
510
+ const forkPackageLockDigest = options.prepared?.packageLockDigest ?? input.package_lock_digest ?? null;
511
+ const forkName = options.prepared ? forkIr.name : source.name;
512
+ const forkWorkflowEntry = options.prepared ? relative(this.cwd, options.prepared.workflowPath) : source.workflowEntry;
513
+ const forkIrDigest = options.prepared?.irDigest ?? source.irDigest;
514
+ const forkSourceGraphDigest = options.prepared?.sourceGraphDigest ?? source.sourceGraphDigest;
515
+ const forkId = newRunId();
516
+ const now = new Date().toISOString();
517
+ const replacement = Boolean(options.prepared || options.input !== undefined || options.target !== undefined || options.agentOverrides !== undefined || options.unsafeReuse === true);
518
+ const targetedReplacement = replacement;
519
+ const forkStatus = source.status === "completed" && !replacement ? "completed" : "pending";
520
+ const sourceRunDir = input.run_dir ? containedRunDir(this.cwd, input.run_dir) : undefined;
521
+ const forkRunDir = join(localStateRoot, "runs", forkId);
522
+ const forkRunPath = join(this.cwd, forkRunDir);
523
+ const stagedForkRunPath = join(this.cwd, localStateRoot, "runs", `.staging-${forkId}`);
524
+ const completedOutputRows = completedSchedulerOutputRows(this.db, runId);
525
+ const completedNodeKeys = new Set([
526
+ ...this.db.prepare(`
527
+ SELECT node_key
528
+ FROM node_states
529
+ WHERE run_id = ? AND status = 'completed'
530
+ `).all(runId).map(row => String(row.node_key)),
531
+ ...completedOutputRows.map(row => row.nodeKey),
532
+ ]);
533
+ const sourceNodeSignatures = nodeSignatures(sourceEffectiveIr.root);
534
+ const forkNodeSignatures = nodeSignatures(forkEffectiveIr.root);
535
+ const irNodeKeys = new Set(forkNodeSignatures.keys());
536
+ const knownCompletedNodeKeys = new Set([...completedNodeKeys].filter(nodeKey => {
537
+ if (forkNodeSignatures.has(nodeKey))
538
+ return forkNodeSignatures.get(nodeKey) === sourceNodeSignatures.get(nodeKey);
539
+ return !replacement && source.status === "completed";
540
+ }));
541
+ const seedPlan = targetedReplacement
542
+ ? planTargetedForkSeed({
543
+ forkRunId: forkId,
544
+ sourceWorkflow: sourceEffectiveIr,
545
+ replacementWorkflow: forkEffectiveIr,
546
+ replacementScope: {
547
+ input: JSON.parse(forkInputJson),
548
+ nodes: {},
549
+ meta: {
550
+ runId: forkId,
551
+ workflowPath: forkWorkflowEntry,
552
+ workflowName: forkName,
553
+ workspaceDir: resolve(this.cwd),
554
+ },
555
+ fanout: {},
556
+ loop: {},
557
+ },
558
+ sourceProjection: this.scheduler.loadRunSnapshot(runId).projection,
559
+ inputChanged: options.input !== undefined,
560
+ unsafeReuse: options.unsafeReuse === true,
561
+ ...(options.target === undefined ? {} : { target: options.target }),
562
+ }).match(value => value, failure => {
563
+ throw new ForkSeedPlanError(failure);
564
+ })
565
+ : undefined;
566
+ const inheritableNodeKeys = seedPlan?.inheritedNodeKeys ?? (options.input !== undefined ? new Set() : source.status === "completed"
567
+ ? knownCompletedNodeKeys
568
+ : inheritableCompletedNodeKeys(forkIr, knownCompletedNodeKeys));
569
+ const nodeRows = this.db.prepare("SELECT node_key, node_id, status, output_json, error_json, attempt FROM node_states WHERE run_id = ?").all(runId);
570
+ const reachableArtifactIds = reachableInheritedArtifactIds({
571
+ runId,
572
+ outputJson: input.output_json,
573
+ nodeRows,
574
+ inheritableNodeKeys,
575
+ });
576
+ if (seedPlan) {
577
+ for (const event of seedPlan.events)
578
+ collectArtifactIds(event.payload, runId, reachableArtifactIds);
579
+ }
580
+ const artifacts = this.db.prepare(`
581
+ SELECT id, node_key, attempt, media_type, digest, size, relative_path
582
+ FROM artifacts
583
+ WHERE run_id = ?
584
+ `).all(runId).filter(artifact => reachableArtifactIds.has(String(artifact.id)) && inheritableNodeKeys.has(String(artifact.node_key)));
585
+ const artifactIdMap = Object.fromEntries(artifacts.map(artifact => [
586
+ String(artifact.id),
587
+ `artifact_${randomUUID()}`,
588
+ ]));
589
+ const forkOutputJson = source.status === "completed" && !replacement && input.output_json
590
+ ? forkCompletedOutputJson({
591
+ outputs: forkIr.outputs,
592
+ completedOutputRows,
593
+ inheritableNodeKeys,
594
+ inputJson: forkInputJson,
595
+ meta: {
596
+ runId: forkId,
597
+ workflowPath: forkWorkflowEntry,
598
+ workflowName: forkName,
599
+ workspaceDir: resolve(this.cwd),
600
+ },
601
+ sourceRunId: runId,
602
+ forkRunId: forkId,
603
+ artifactIdMap,
604
+ })
605
+ : null;
606
+ if (sourceRunDir) {
607
+ try {
608
+ await mkdir(dirname(stagedForkRunPath), { recursive: true });
609
+ await cp(sourceRunDir, stagedForkRunPath, { recursive: true });
610
+ await pruneNonInheritedArtifacts(stagedForkRunPath, artifacts);
611
+ if (options.prepared)
612
+ await writePreparedRunFiles(stagedForkRunPath, options.prepared);
613
+ await verifyFrozenRunFiles(stagedForkRunPath, forkIrDigest, forkLockJson, forkIrJson);
614
+ await verifyCopiedArtifacts(stagedForkRunPath, artifacts);
615
+ await rm(forkRunPath, { recursive: true, force: true });
616
+ await rename(stagedForkRunPath, forkRunPath);
617
+ }
618
+ catch (error) {
619
+ await rm(stagedForkRunPath, { recursive: true, force: true });
620
+ await rm(forkRunPath, { recursive: true, force: true });
621
+ throw error;
622
+ }
623
+ }
624
+ else if (artifacts.length > 0) {
625
+ throw new Error(`Run '${runId}' has artifacts but no run directory.`);
626
+ }
627
+ let transactionStarted = false;
628
+ try {
629
+ this.db.exec("BEGIN IMMEDIATE");
630
+ transactionStarted = true;
631
+ this.db.prepare(`
632
+ INSERT INTO runs (id, name, status, workflow_entry, ir_digest, source_graph_digest, created_at, updated_at)
633
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
634
+ `).run(forkId, forkName, forkStatus, forkWorkflowEntry, forkIrDigest, forkSourceGraphDigest, now, now);
635
+ this.db.prepare(`
636
+ INSERT INTO run_inputs (
637
+ run_id, workflow_ir_json, input_json, agent_overrides_json, output_json, lock_json, package_lock_digest, run_dir, created_at
638
+ )
639
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
640
+ `).run(forkId, forkIrJson, forkInputJson, stableJson(forkAgentOverrides), forkOutputJson, forkLockJson, forkPackageLockDigest, forkRunDir, now);
641
+ this.db.prepare(`
642
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
643
+ VALUES (?, 1, 'run.forked', NULL, ?, ?, ?)
644
+ `).run(forkId, stableJson({ sourceRunId: runId, requestFingerprint, ...(options.target === undefined ? {} : { target: options.target }), ...(options.unsafeReuse === true ? { unsafeReuse: true } : {}), ...(Object.keys(forkAgentOverrides).length > 0 ? { agentOverrides: forkAgentOverrides } : {}) }), now, forkRequestKey ?? `fork:${forkId}:${runId}`);
645
+ if (forkStatus === "completed" && forkOutputJson) {
646
+ this.db.prepare(`
647
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
648
+ VALUES (?, 2, 'run.completed', NULL, ?, ?, ?)
649
+ `).run(forkId, stableJson({ output: JSON.parse(forkOutputJson) }), now, `complete:${forkId}`);
650
+ }
651
+ if (targetedReplacement && seedPlan) {
652
+ this.schedulerStore().insertForkSeedEventsInTransaction({
653
+ runId: forkId,
654
+ sourceRunId: runId,
655
+ artifactIdMap,
656
+ plan: seedPlan,
657
+ now,
658
+ });
659
+ }
660
+ else {
661
+ const insertedNodeKeys = new Set();
662
+ for (const row of nodeRows) {
663
+ const nodeKey = String(row.node_key);
664
+ if (!irNodeKeys.has(nodeKey) && !inheritableNodeKeys.has(nodeKey))
665
+ continue;
666
+ insertedNodeKeys.add(nodeKey);
667
+ this.db.prepare(`
668
+ INSERT INTO node_states (run_id, node_key, node_id, status, output_json, error_json, attempt, created_at, updated_at)
669
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
670
+ `).run(forkId, nodeKey, String(row.node_id), inheritableNodeKeys.has(nodeKey) ? "completed" : "pending", inheritableNodeKeys.has(nodeKey) && row.output_json ? rewriteArtifactRefs(String(row.output_json), runId, forkId, artifactIdMap) : null, null, Number(row.attempt ?? 0), now, now);
671
+ }
672
+ for (const nodeKey of irNodeKeys) {
673
+ if (insertedNodeKeys.has(nodeKey))
674
+ continue;
675
+ this.db.prepare(`
676
+ INSERT INTO node_states (run_id, node_key, node_id, status, created_at, updated_at)
677
+ VALUES (?, ?, ?, 'pending', ?, ?)
678
+ `).run(forkId, nodeKey, nodeKey, now, now);
679
+ }
680
+ }
681
+ for (const artifact of artifacts) {
682
+ this.db.prepare(`
683
+ INSERT INTO artifacts (id, run_id, node_key, attempt, media_type, digest, size, relative_path, created_at)
684
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
685
+ `).run(requireArtifactId(artifactIdMap, String(artifact.id)), forkId, artifact.node_key === null ? null : String(artifact.node_key), Number(artifact.attempt ?? 0), artifact.media_type === null ? null : String(artifact.media_type), String(artifact.digest), Number(artifact.size), String(artifact.relative_path), now);
686
+ }
687
+ this.db.exec("COMMIT");
688
+ transactionStarted = false;
689
+ }
690
+ catch (error) {
691
+ if (transactionStarted)
692
+ this.db.exec("ROLLBACK");
693
+ await rm(stagedForkRunPath, { recursive: true, force: true });
694
+ await rm(forkRunPath, { recursive: true, force: true });
695
+ if (error instanceof ArtifactRewriteError) {
696
+ throw new ForkSeedPlanError({
697
+ type: "artifact-rewrite-failure",
698
+ artifactId: error.artifactId,
699
+ message: error.message,
700
+ });
701
+ }
702
+ throw error;
703
+ }
704
+ return { ...this.requireRun(forkId), forkCreated: true };
705
+ }
706
+ async cleanupRunDirectories(options = {}) {
707
+ const runsDir = join(this.cwd, localStateRoot, "runs");
708
+ const olderThanMs = options.olderThanMs ?? 60_000;
709
+ const removeOrphanedRuns = options.removeOrphanedRuns ?? false;
710
+ let entries;
711
+ try {
712
+ entries = await readdir(runsDir);
713
+ }
714
+ catch {
715
+ return { staged: 0, orphaned: 0 };
716
+ }
717
+ const validRunDirs = new Set(this.db.prepare("SELECT run_dir FROM run_inputs").all().map(row => String(row.run_dir)));
718
+ let staged = 0;
719
+ let orphaned = 0;
720
+ for (const entry of entries) {
721
+ const absolutePath = join(runsDir, entry);
722
+ if (!(await isStalePath(absolutePath, olderThanMs)))
723
+ continue;
724
+ if (entry.startsWith(".staging-")) {
725
+ await rm(absolutePath, { recursive: true, force: true });
726
+ staged += 1;
727
+ continue;
728
+ }
729
+ const relativePath = join(localStateRoot, "runs", entry);
730
+ if (removeOrphanedRuns && runIdPattern.test(entry) && !validRunDirs.has(relativePath)) {
731
+ await rm(absolutePath, { recursive: true, force: true });
732
+ orphaned += 1;
733
+ }
734
+ }
735
+ return { staged, orphaned };
736
+ }
737
+ async deleteRun(runId) {
738
+ const run = this.getRunRecord(runId);
739
+ if (!run)
740
+ return undefined;
741
+ const runDir = this.getRunDir(runId);
742
+ const absoluteRunDir = runDir ? containedRunDir(this.cwd, runDir) : undefined;
743
+ this.db.prepare("DELETE FROM runs WHERE id = ?").run(runId);
744
+ if (absoluteRunDir)
745
+ await rm(absoluteRunDir, { recursive: true, force: true });
746
+ return run;
747
+ }
748
+ writeHookJournal(entry) {
749
+ this.db.prepare(`
750
+ INSERT INTO hook_journal (
751
+ run_id, event_sequence, trigger_order, event, source, source_path, handler_id, definition_hash,
752
+ node_key, status, exit_code, stdout, stderr, duration_ms, error, triggered_at
753
+ )
754
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
755
+ ON CONFLICT(run_id, event_sequence, definition_hash) DO NOTHING
756
+ `).run(entry.runId, entry.eventSequence, entry.triggerOrder, entry.event, entry.source, entry.sourcePath, entry.handlerId, entry.definitionHash, entry.nodeKey ?? null, entry.status, entry.exitCode ?? null, entry.stdout ?? null, entry.stderr ?? null, entry.durationMs ?? null, entry.error ?? null, entry.triggeredAt);
757
+ }
758
+ getHookJournal(runId) {
759
+ const rows = this.db.prepare(`
760
+ SELECT id, run_id, event_sequence, trigger_order, event, source, source_path, handler_id, definition_hash,
761
+ node_key, status, exit_code, stdout, stderr, duration_ms, error, triggered_at
762
+ FROM hook_journal
763
+ WHERE run_id = ?
764
+ ORDER BY event_sequence ASC, trigger_order ASC, id ASC
765
+ `).all(runId);
766
+ return rows.map(hookJournalEntryFromRow);
767
+ }
768
+ pruneHookJournal(cutoff) {
769
+ return Number(this.db.prepare("DELETE FROM hook_journal WHERE triggered_at < ?").run(cutoff.toISOString()).changes);
770
+ }
771
+ getLastRunEventSequence(runId) {
772
+ const row = this.db.prepare("SELECT COALESCE(MAX(sequence), 0) AS sequence FROM run_events WHERE run_id = ?").get(runId);
773
+ return Number(row?.sequence ?? 0);
774
+ }
775
+ getCommittedRuntimeEventsAfter(runId, sequence) {
776
+ const rows = this.db.prepare(`
777
+ SELECT run_id, sequence, type, node_key, payload_json, created_at, idempotency_key
778
+ FROM run_events
779
+ WHERE run_id = ? AND sequence > ?
780
+ ORDER BY sequence ASC
781
+ `).all(runId, sequence);
782
+ return rows.map(decodeCommittedRuntimeEventRow);
783
+ }
784
+ getRunDir(runId) {
785
+ const row = this.db.prepare("SELECT run_dir FROM run_inputs WHERE run_id = ?").get(runId);
786
+ return row?.run_dir;
787
+ }
788
+ registerArtifact(input) {
789
+ this.db.prepare(`
790
+ INSERT INTO artifacts (id, run_id, node_key, attempt, media_type, digest, size, relative_path, created_at)
791
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
792
+ `).run(input.id, input.runId, input.nodeKey, input.attempt, input.mediaType ?? null, input.digest, input.size, input.relativePath, new Date().toISOString());
793
+ }
794
+ getArtifact(runId, artifactId) {
795
+ const row = this.db.prepare("SELECT id, run_id, node_key, attempt, media_type, digest, size, relative_path FROM artifacts WHERE run_id = ? AND id = ?").get(runId, artifactId);
796
+ if (!row)
797
+ return undefined;
798
+ return {
799
+ id: String(row.id),
800
+ runId: String(row.run_id),
801
+ nodeKey: String(row.node_key),
802
+ attempt: Number(row.attempt),
803
+ ...(row.media_type === null ? {} : { mediaType: String(row.media_type) }),
804
+ digest: String(row.digest),
805
+ size: Number(row.size),
806
+ relativePath: String(row.relative_path),
807
+ };
808
+ }
809
+ listArtifacts(runId) {
810
+ const rows = this.db.prepare("SELECT id, run_id, node_key, attempt, media_type, digest, size, relative_path FROM artifacts WHERE run_id = ? ORDER BY created_at ASC, id ASC").all(runId);
811
+ return rows.map(row => ({
812
+ id: String(row.id),
813
+ runId: String(row.run_id),
814
+ nodeKey: String(row.node_key),
815
+ attempt: Number(row.attempt),
816
+ ...(row.media_type === null ? {} : { mediaType: String(row.media_type) }),
817
+ digest: String(row.digest),
818
+ size: Number(row.size),
819
+ relativePath: String(row.relative_path),
820
+ }));
821
+ }
822
+ writeExecutionMetadata(input) {
823
+ this.db.prepare(`
824
+ INSERT INTO execution_metadata (run_id, attempt_id, kind, metadata_json, created_at)
825
+ VALUES (?, ?, ?, ?, ?)
826
+ `).run(input.runId, input.attemptId ?? null, input.kind, stableJson(input.metadata), new Date().toISOString());
827
+ }
828
+ writeNodeProgress(input) {
829
+ const now = new Date().toISOString();
830
+ this.db.exec("BEGIN IMMEDIATE");
831
+ try {
832
+ if (input.attemptId) {
833
+ const started = this.db.prepare(`
834
+ SELECT 1
835
+ FROM node_attempts
836
+ WHERE attempt_id = ? AND run_id = ? AND node_key = ? AND status = 'started'
837
+ `).get(input.attemptId, input.runId, input.nodeKey);
838
+ if (!started) {
839
+ this.db.exec("COMMIT");
840
+ return;
841
+ }
842
+ }
843
+ this.db.prepare(`
844
+ INSERT INTO node_progress (
845
+ run_id, node_key, node_id, attempt_id, attempt_no, kind, status, message,
846
+ output_tail, output_total_bytes, output_truncated,
847
+ context_json, token_usage_json, tools_json, updated_at
848
+ )
849
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
850
+ ON CONFLICT(run_id, node_key) DO UPDATE SET
851
+ node_id = excluded.node_id,
852
+ attempt_id = excluded.attempt_id,
853
+ attempt_no = excluded.attempt_no,
854
+ kind = excluded.kind,
855
+ status = excluded.status,
856
+ message = excluded.message,
857
+ output_tail = excluded.output_tail,
858
+ output_total_bytes = excluded.output_total_bytes,
859
+ output_truncated = excluded.output_truncated,
860
+ context_json = excluded.context_json,
861
+ token_usage_json = excluded.token_usage_json,
862
+ tools_json = excluded.tools_json,
863
+ updated_at = excluded.updated_at
864
+ `).run(input.runId, input.nodeKey, input.nodeId, input.attemptId ?? null, input.attemptNo ?? null, input.kind, input.status, input.message ?? null, input.output?.tail ?? null, input.output?.totalBytes ?? null, input.output?.truncated === undefined ? null : input.output.truncated ? 1 : 0, input.context === undefined ? null : stableJson(input.context), input.tokenUsage === undefined ? null : stableJson(input.tokenUsage), input.tools === undefined ? null : stableJson(input.tools), now);
865
+ this.db.prepare(`
866
+ UPDATE runs
867
+ SET progress_version = progress_version + 1, progress_updated_at = ?
868
+ WHERE id = ?
869
+ `).run(now, input.runId);
870
+ this.db.exec("COMMIT");
871
+ }
872
+ catch (error) {
873
+ this.db.exec("ROLLBACK");
874
+ throw error;
875
+ }
876
+ }
877
+ getRun(runId) {
878
+ const run = this.getRunRecord(runId);
879
+ if (!run)
880
+ return undefined;
881
+ const input = this.db.prepare(`
882
+ SELECT input_json, agent_overrides_json, output_json
883
+ FROM run_inputs
884
+ WHERE run_id = ?
885
+ `).get(runId);
886
+ if (!input)
887
+ return undefined;
888
+ const agentOverrides = parseAgentOverrides(input.agent_overrides_json);
889
+ const eventCount = this.count("run_events", runId);
890
+ const nodeCount = this.count("node_states", runId);
891
+ const dynamic = this.tryGetRunDynamicDetails(runId);
892
+ return {
893
+ ...run,
894
+ input: JSON.parse(input.input_json),
895
+ ...(input.output_json ? { output: JSON.parse(input.output_json) } : {}),
896
+ ...(Object.keys(agentOverrides).length > 0 ? { agentOverrides } : {}),
897
+ hooks: isTerminalRunStatus(run.status) ? this.getHookJournal(runId) : [],
898
+ eventCount,
899
+ nodeCount,
900
+ execution: this.getRunExecutionState(run),
901
+ ...(dynamic ? { dynamic } : {}),
902
+ };
903
+ }
904
+ getRunExecutionState(run) {
905
+ if (run.status === "completed" || run.status === "failed" || run.status === "canceled")
906
+ return { state: "terminal", lastStatus: run.status, reason: "terminal" };
907
+ const now = Date.now();
908
+ const daemon = this.db.prepare(`
909
+ SELECT pid, heartbeat_at
910
+ FROM daemon_lease
911
+ ORDER BY updated_at DESC
912
+ LIMIT 1
913
+ `).get();
914
+ const lease = this.db.prepare(`
915
+ SELECT owner_id, lease_expires_at
916
+ FROM run_leases
917
+ WHERE run_id = ? AND released_at IS NULL
918
+ ORDER BY claimed_at DESC
919
+ LIMIT 1
920
+ `).get(run.id);
921
+ if (daemon?.heartbeat_at && now - Date.parse(daemon.heartbeat_at) > 5_000) {
922
+ return { state: "stale", lastStatus: run.status, reason: "daemon_heartbeat_expired", daemonHeartbeatAt: daemon.heartbeat_at, ...(lease ? { ownerId: lease.owner_id, leaseExpiresAt: lease.lease_expires_at } : {}) };
923
+ }
924
+ if (daemon?.pid !== null && daemon?.pid !== undefined && !isProcessAlive(daemon.pid)) {
925
+ return { state: "stale", lastStatus: run.status, reason: "daemon_pid_dead", ...(daemon.heartbeat_at ? { daemonHeartbeatAt: daemon.heartbeat_at } : {}), ...(lease ? { ownerId: lease.owner_id, leaseExpiresAt: lease.lease_expires_at } : {}) };
926
+ }
927
+ if (lease && Date.parse(lease.lease_expires_at) <= now) {
928
+ return { state: "stale", lastStatus: run.status, reason: "run_lease_expired", ...(daemon?.heartbeat_at ? { daemonHeartbeatAt: daemon.heartbeat_at } : {}), ownerId: lease.owner_id, leaseExpiresAt: lease.lease_expires_at };
929
+ }
930
+ if (lease)
931
+ return { state: "active", lastStatus: run.status, reason: "run_lease_active", ...(daemon?.heartbeat_at ? { daemonHeartbeatAt: daemon.heartbeat_at } : {}), ownerId: lease.owner_id, leaseExpiresAt: lease.lease_expires_at };
932
+ if (daemon?.heartbeat_at)
933
+ return { state: "inactive", lastStatus: run.status, reason: "daemon_alive", daemonHeartbeatAt: daemon.heartbeat_at };
934
+ return { state: "inactive", lastStatus: run.status, reason: "no_liveness_evidence" };
935
+ }
936
+ tryGetRunDynamicDetails(runId) {
937
+ try {
938
+ const frames = readRunDynamicFrames(this.db, runId);
939
+ const nodeInstances = readRunDynamicNodeInstances(this.db, runId);
940
+ const attempts = readRunDynamicAttempts(this.db, runId);
941
+ const groupMembers = readRunDynamicGroupMembers(this.db, runId);
942
+ const signalWaits = readRunDynamicSignalWaits(this.db, runId);
943
+ const executionMetadata = readRunExecutionMetadata(this.db, runId);
944
+ const progress = readRunNodeProgress(this.db, runId);
945
+ const progressVersion = runProgressVersion(this.db, runId);
946
+ if (frames.length + nodeInstances.length + attempts.length + groupMembers.length + signalWaits.length + executionMetadata.length + progress.length === 0)
947
+ return undefined;
948
+ return {
949
+ version: this.nextSequence(runId) - 1,
950
+ progressVersion: progressVersion.version,
951
+ ...(progressVersion.updatedAt ? { progressUpdatedAt: progressVersion.updatedAt } : {}),
952
+ frames,
953
+ nodeInstances,
954
+ attempts,
955
+ groupMembers,
956
+ signalWaits,
957
+ executionMetadata,
958
+ progress,
959
+ };
960
+ }
961
+ catch {
962
+ return undefined;
963
+ }
964
+ }
965
+ listRuns() {
966
+ return this.db.prepare(`
967
+ SELECT ${this.runRecordColumns()}
968
+ FROM runs
969
+ ORDER BY updated_at DESC, created_at DESC
970
+ `).all().map(toRunRecord);
971
+ }
972
+ getRuntimeDiagnostics() {
973
+ const now = new Date().toISOString();
974
+ const daemon = this.db.prepare(`
975
+ SELECT workspace_realpath, generation, pid, heartbeat_at, idle_since_at, idle_stop_ms, protocol_version, package_version, node_version, exec_path, updated_at
976
+ FROM daemon_lease
977
+ ORDER BY updated_at DESC
978
+ LIMIT 1
979
+ `).get();
980
+ const runs = this.db.prepare(`
981
+ SELECT
982
+ COUNT(*) AS total,
983
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
984
+ SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
985
+ SUM(CASE WHEN status = 'awaiting' THEN 1 ELSE 0 END) AS awaiting,
986
+ SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) AS paused,
987
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
988
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
989
+ SUM(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END) AS canceled
990
+ FROM runs
991
+ `).get();
992
+ const stale = this.db.prepare(`
993
+ SELECT COUNT(*) AS count
994
+ FROM run_leases
995
+ WHERE released_at IS NULL AND lease_expires_at <= ?
996
+ `).get(now);
997
+ return {
998
+ ...(daemon ? {
999
+ daemon: {
1000
+ workspaceRealpath: daemon.workspace_realpath,
1001
+ generation: daemon.generation,
1002
+ ...(daemon.pid === null ? {} : { pid: daemon.pid }),
1003
+ ...(daemon.heartbeat_at === null ? {} : { heartbeatAt: daemon.heartbeat_at }),
1004
+ ...(daemon.idle_since_at === null ? {} : { idleSinceAt: daemon.idle_since_at }),
1005
+ ...(daemon.idle_stop_ms === null ? {} : { idleStopMs: daemon.idle_stop_ms }),
1006
+ protocolVersion: daemon.protocol_version,
1007
+ packageVersion: daemon.package_version,
1008
+ nodeVersion: daemon.node_version,
1009
+ execPath: daemon.exec_path,
1010
+ updatedAt: daemon.updated_at,
1011
+ },
1012
+ } : {}),
1013
+ runs: {
1014
+ total: Number(runs.total ?? 0),
1015
+ pending: Number(runs.pending ?? 0),
1016
+ running: Number(runs.running ?? 0),
1017
+ awaiting: Number(runs.awaiting ?? 0),
1018
+ paused: Number(runs.paused ?? 0),
1019
+ failed: Number(runs.failed ?? 0),
1020
+ completed: Number(runs.completed ?? 0),
1021
+ canceled: Number(runs.canceled ?? 0),
1022
+ runnable: this.countRunnableRuns(),
1023
+ },
1024
+ leases: {
1025
+ stale: Number(stale.count),
1026
+ },
1027
+ };
1028
+ }
1029
+ getRunRecord(runId) {
1030
+ const row = this.db.prepare(`
1031
+ SELECT ${this.runRecordColumns()}
1032
+ FROM runs
1033
+ WHERE id = ?
1034
+ `).get(runId);
1035
+ return row ? toRunRecord(row) : undefined;
1036
+ }
1037
+ requireRun(runId) {
1038
+ const run = this.getRunRecord(runId);
1039
+ if (!run)
1040
+ throw new Error(`Run '${runId}' was not found.`);
1041
+ return run;
1042
+ }
1043
+ count(table, runId) {
1044
+ const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE run_id = ?`).get(runId);
1045
+ return row?.count ?? 0;
1046
+ }
1047
+ nextSequence(runId) {
1048
+ const row = this.db.prepare("SELECT COALESCE(MAX(sequence), 0) + 1 AS count FROM run_events WHERE run_id = ?").get(runId);
1049
+ return row?.count ?? 1;
1050
+ }
1051
+ }
1052
+ class SqliteSchedulerStorePort {
1053
+ db;
1054
+ cwd;
1055
+ constructor(db, cwd) {
1056
+ this.db = db;
1057
+ this.cwd = cwd;
1058
+ }
1059
+ claimRun(runId, ownerId, leaseMs) {
1060
+ const now = new Date().toISOString();
1061
+ const leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString();
1062
+ this.db.exec("BEGIN IMMEDIATE");
1063
+ try {
1064
+ const run = this.db.prepare("SELECT id FROM runs WHERE id = ?").get(runId);
1065
+ if (!run)
1066
+ throw new Error(`Run '${runId}' was not found.`);
1067
+ const current = this.db.prepare("SELECT owner_epoch, lease_expires_at, released_at FROM run_leases WHERE run_id = ?").get(runId);
1068
+ if (current && current.released_at === null && current.lease_expires_at > now) {
1069
+ this.db.exec("ROLLBACK");
1070
+ return undefined;
1071
+ }
1072
+ const ownerEpoch = (current?.owner_epoch ?? 0) + 1;
1073
+ this.db.prepare(`
1074
+ INSERT INTO run_leases (run_id, owner_id, owner_epoch, lease_expires_at, heartbeat_at, claimed_at, released_at, reason)
1075
+ VALUES (?, ?, ?, ?, ?, ?, NULL, 'advance')
1076
+ ON CONFLICT(run_id) DO UPDATE SET
1077
+ owner_id = excluded.owner_id,
1078
+ owner_epoch = excluded.owner_epoch,
1079
+ lease_expires_at = excluded.lease_expires_at,
1080
+ heartbeat_at = excluded.heartbeat_at,
1081
+ claimed_at = excluded.claimed_at,
1082
+ released_at = NULL,
1083
+ reason = excluded.reason
1084
+ `).run(runId, ownerId, ownerEpoch, leaseExpiresAt, now, now);
1085
+ this.db.exec("COMMIT");
1086
+ return { runId, ownerId, ownerEpoch, leaseExpiresAt };
1087
+ }
1088
+ catch (error) {
1089
+ this.db.exec("ROLLBACK");
1090
+ throw error;
1091
+ }
1092
+ }
1093
+ heartbeatRun(claim, leaseMs) {
1094
+ const now = new Date().toISOString();
1095
+ const leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString();
1096
+ const result = this.db.prepare(`
1097
+ UPDATE run_leases
1098
+ SET lease_expires_at = ?, heartbeat_at = ?
1099
+ WHERE run_id = ? AND owner_id = ? AND owner_epoch = ? AND released_at IS NULL AND lease_expires_at > ?
1100
+ `).run(leaseExpiresAt, now, claim.runId, claim.ownerId, claim.ownerEpoch, now);
1101
+ return result.changes === 1;
1102
+ }
1103
+ releaseRun(claim) {
1104
+ const now = new Date().toISOString();
1105
+ const result = this.db.prepare(`
1106
+ UPDATE run_leases
1107
+ SET released_at = ?, heartbeat_at = ?
1108
+ WHERE run_id = ? AND owner_id = ? AND owner_epoch = ? AND released_at IS NULL
1109
+ `).run(now, now, claim.runId, claim.ownerId, claim.ownerEpoch);
1110
+ return result.changes === 1;
1111
+ }
1112
+ tryLoadRunSnapshot(runId) {
1113
+ return schedulerStoreResult(() => this.loadRunSnapshot(runId));
1114
+ }
1115
+ loadRunSnapshot(runId) {
1116
+ const row = this.db.prepare("SELECT id FROM runs WHERE id = ?").get(runId);
1117
+ if (!row)
1118
+ throwSchedulerStoreError({ type: "run-not-found", runId, message: `Run '${runId}' was not found.` });
1119
+ const events = this.schedulerEvents(runId);
1120
+ return {
1121
+ runId,
1122
+ version: this.currentVersion(runId),
1123
+ projection: applySchedulerEvents(createSchedulerProjection(runId), events),
1124
+ };
1125
+ }
1126
+ tryAppendSchedulerEvents(commit) {
1127
+ return schedulerStoreResult(() => this.appendSchedulerEvents(commit));
1128
+ }
1129
+ appendSchedulerEvents(commit) {
1130
+ if (commit.events.length === 0)
1131
+ return this.loadRunSnapshot(commit.runId);
1132
+ const duplicate = this.duplicateAppendIdempotency(commit);
1133
+ if (duplicate)
1134
+ return duplicate;
1135
+ const now = new Date().toISOString();
1136
+ const commitDigest = schedulerCommitDigest(commit.events);
1137
+ this.db.exec("BEGIN IMMEDIATE");
1138
+ try {
1139
+ const currentVersion = this.currentVersion(commit.runId);
1140
+ if (currentVersion !== commit.expectedVersion) {
1141
+ throwSchedulerStoreError({
1142
+ type: "version-mismatch",
1143
+ runId: commit.runId,
1144
+ expectedVersion: commit.expectedVersion,
1145
+ actualVersion: currentVersion,
1146
+ message: `Run '${commit.runId}' scheduler version mismatch.`,
1147
+ });
1148
+ }
1149
+ this.requireOwnerEpoch(commit.runId, commit.ownerEpoch);
1150
+ applySchedulerEvents(createSchedulerProjection(commit.runId), [...this.schedulerEvents(commit.runId), ...commit.events]);
1151
+ this.db.prepare(`
1152
+ INSERT INTO scheduler_commits (run_id, idempotency_key, event_count, event_digest, created_at)
1153
+ VALUES (?, ?, ?, ?, ?)
1154
+ `).run(commit.runId, commit.idempotencyKey, commit.events.length, commitDigest, now);
1155
+ let sequence = currentVersion + 1;
1156
+ for (const [index, event] of commit.events.entries()) {
1157
+ this.db.prepare(`
1158
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1159
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1160
+ `).run(commit.runId, sequence, event.type, eventNodeKey(event), encodeSchedulerPayload(event.payload), now, schedulerEventIdempotencyKey(commit.runId, commit.idempotencyKey, index));
1161
+ sequence += 1;
1162
+ }
1163
+ this.syncSchedulerProjectionTables(commit.runId, now);
1164
+ this.syncPublicRunProjection(commit.runId, now);
1165
+ this.db.exec("COMMIT");
1166
+ return this.loadRunSnapshot(commit.runId);
1167
+ }
1168
+ catch (error) {
1169
+ this.db.exec("ROLLBACK");
1170
+ throw error;
1171
+ }
1172
+ }
1173
+ insertForkSeedEventsInTransaction(input) {
1174
+ if (input.plan.events.length === 0)
1175
+ return;
1176
+ const events = input.plan.events.map(event => ({
1177
+ ...event,
1178
+ payload: rewriteArtifactValue(event.payload, input.sourceRunId, input.runId, input.artifactIdMap),
1179
+ }));
1180
+ applySchedulerEvents(createSchedulerProjection(input.runId), events);
1181
+ const currentVersion = this.currentVersion(input.runId);
1182
+ const commitKey = `fork-seed:${input.runId}`;
1183
+ this.db.prepare(`
1184
+ INSERT INTO scheduler_commits (run_id, idempotency_key, event_count, event_digest, created_at)
1185
+ VALUES (?, ?, ?, ?, ?)
1186
+ `).run(input.runId, commitKey, events.length, schedulerCommitDigest(events), input.now);
1187
+ let sequence = currentVersion + 1;
1188
+ for (const [index, event] of events.entries()) {
1189
+ this.db.prepare(`
1190
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1191
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1192
+ `).run(input.runId, sequence, event.type, eventNodeKey(event), encodeSchedulerPayload(event.payload), input.now, schedulerEventIdempotencyKey(input.runId, commitKey, index));
1193
+ sequence += 1;
1194
+ }
1195
+ this.syncSchedulerProjectionTables(input.runId, input.now);
1196
+ this.syncPublicRunProjection(input.runId, input.now);
1197
+ }
1198
+ tryStartAttempt(input) {
1199
+ return schedulerStoreResult(() => this.startAttempt(input));
1200
+ }
1201
+ startAttempt(input) {
1202
+ const existing = this.eventByIdempotencyKey(input.idempotencyKey);
1203
+ if (existing && existing.type === "attempt.started") {
1204
+ const payload = existing.payload;
1205
+ if (existing.run_id !== input.runId)
1206
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey: input.idempotencyKey, runId: input.runId, message: `Attempt start idempotency key '${input.idempotencyKey}' conflicts with another run.` });
1207
+ if (!matchesAttemptStartInput(input, payload))
1208
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey: input.idempotencyKey, runId: input.runId, message: `Attempt start idempotency key '${input.idempotencyKey}' conflicts with different input.` });
1209
+ if (typeof payload.attemptId === "string" && typeof payload.attemptNo === "number")
1210
+ return { attemptId: payload.attemptId, attemptNo: payload.attemptNo };
1211
+ throw new Error(`Attempt start idempotency key '${input.idempotencyKey}' has invalid payload.`);
1212
+ }
1213
+ if (existing)
1214
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey: input.idempotencyKey, runId: input.runId, message: `Attempt start idempotency key '${input.idempotencyKey}' conflicts with ${existing.type}.` });
1215
+ const now = new Date().toISOString();
1216
+ const attemptId = `attempt_${randomUUID()}`;
1217
+ this.db.exec("BEGIN IMMEDIATE");
1218
+ try {
1219
+ this.requireOwnerEpoch(input.runId, input.ownerEpoch);
1220
+ const currentProjection = applySchedulerEvents(createSchedulerProjection(input.runId), this.schedulerEvents(input.runId));
1221
+ if (currentProjection.run.status === "paused")
1222
+ throwSchedulerStoreError({ type: "run-paused", runId: input.runId, message: `Run '${input.runId}' is paused.` });
1223
+ const row = this.db.prepare("SELECT COALESCE(MAX(attempt_no), 0) + 1 AS count FROM node_attempts WHERE run_id = ? AND node_key = ?").get(input.runId, input.nodeKey);
1224
+ const attemptNo = row?.count ?? 1;
1225
+ const sequence = this.nextSequence(input.runId);
1226
+ const payload = {
1227
+ runId: input.runId,
1228
+ attemptId,
1229
+ nodeKey: input.nodeKey,
1230
+ nodeId: input.nodeId,
1231
+ attemptNo,
1232
+ ownerEpoch: input.ownerEpoch,
1233
+ ...(input.deadlineAt === undefined ? {} : { deadlineAt: input.deadlineAt }),
1234
+ };
1235
+ const instanceStartedEvent = { type: "instance.started", payload: { nodeKey: input.nodeKey } };
1236
+ const attemptStartedEvent = { type: "attempt.started", payload };
1237
+ const memberStartedEvents = this.groupMemberStartedEventsForNode(input.runId, input.nodeKey);
1238
+ const events = [instanceStartedEvent, ...memberStartedEvents, attemptStartedEvent];
1239
+ applySchedulerEvents(createSchedulerProjection(input.runId), [...this.schedulerEvents(input.runId), ...events]);
1240
+ let sequenceOffset = 0;
1241
+ this.db.prepare(`
1242
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1243
+ VALUES (?, ?, 'instance.started', ?, ?, ?, ?)
1244
+ `).run(input.runId, sequence + sequenceOffset, input.nodeKey, encodeSchedulerPayload(instanceStartedEvent.payload), now, derivedIdempotencyKey(input.idempotencyKey, "instance"));
1245
+ sequenceOffset += 1;
1246
+ for (const [index, memberStartedEvent] of memberStartedEvents.entries()) {
1247
+ this.db.prepare(`
1248
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1249
+ VALUES (?, ?, 'group.member_started', ?, ?, ?, ?)
1250
+ `).run(input.runId, sequence + sequenceOffset, input.nodeKey, encodeSchedulerPayload(memberStartedEvent.payload), now, derivedIdempotencyKey(input.idempotencyKey, index === 0 ? "member" : `member:${index}`));
1251
+ sequenceOffset += 1;
1252
+ }
1253
+ this.db.prepare(`
1254
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1255
+ VALUES (?, ?, 'attempt.started', ?, ?, ?, ?)
1256
+ `).run(input.runId, sequence + sequenceOffset, input.nodeKey, encodeSchedulerPayload(payload), now, input.idempotencyKey);
1257
+ this.syncSchedulerProjectionTables(input.runId, now);
1258
+ const clearedProgress = this.db.prepare("DELETE FROM node_progress WHERE run_id = ? AND node_key = ?").run(input.runId, input.nodeKey);
1259
+ if (clearedProgress.changes > 0) {
1260
+ this.db.prepare(`
1261
+ UPDATE runs
1262
+ SET progress_version = progress_version + 1, progress_updated_at = ?
1263
+ WHERE id = ?
1264
+ `).run(now, input.runId);
1265
+ }
1266
+ this.syncPublicRunProjection(input.runId, now);
1267
+ this.db.exec("COMMIT");
1268
+ return { attemptId, attemptNo };
1269
+ }
1270
+ catch (error) {
1271
+ this.db.exec("ROLLBACK");
1272
+ throw error;
1273
+ }
1274
+ }
1275
+ tryCommitAttemptResult(input) {
1276
+ return schedulerStoreResult(() => this.commitAttemptResult(input));
1277
+ }
1278
+ commitAttemptResult(input) {
1279
+ const existing = this.eventByIdempotencyKey(input.idempotencyKey);
1280
+ if (existing) {
1281
+ if (existing.run_id !== input.runId)
1282
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey: input.idempotencyKey, runId: input.runId, message: `Attempt commit idempotency key '${input.idempotencyKey}' conflicts with another run.` });
1283
+ const attempt = this.db.prepare("SELECT node_key FROM node_attempts WHERE run_id = ? AND attempt_id = ?").get(input.runId, input.attemptId);
1284
+ if (!attempt)
1285
+ throwSchedulerStoreError({ type: "attempt-not-found", attemptId: input.attemptId, message: `Attempt '${input.attemptId}' was not found.` });
1286
+ const event = attemptResultEvent(input, attempt.node_key);
1287
+ if (existing.type !== event.type || stableJson(existing.payload) !== stableJson(event.payload)) {
1288
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey: input.idempotencyKey, runId: input.runId, message: `Attempt commit idempotency key '${input.idempotencyKey}' conflicts with different input.` });
1289
+ }
1290
+ return this.loadRunSnapshot(input.runId);
1291
+ }
1292
+ const now = new Date().toISOString();
1293
+ this.db.exec("BEGIN IMMEDIATE");
1294
+ try {
1295
+ const attempt = this.db.prepare("SELECT run_id, node_key, owner_epoch, status FROM node_attempts WHERE attempt_id = ?").get(input.attemptId);
1296
+ if (!attempt || attempt.run_id !== input.runId)
1297
+ throwSchedulerStoreError({ type: "attempt-not-found", attemptId: input.attemptId, message: `Attempt '${input.attemptId}' was not found.` });
1298
+ if (attempt.owner_epoch !== input.ownerEpoch)
1299
+ throwSchedulerStoreError({ type: "owner-epoch-stale", runId: input.runId, attemptId: input.attemptId, ownerEpoch: input.ownerEpoch, message: `Attempt '${input.attemptId}' owner epoch is stale.` });
1300
+ this.requireOwnerEpoch(input.runId, input.ownerEpoch);
1301
+ if (attempt.status !== "started")
1302
+ throwSchedulerStoreError({ type: "terminal-attempt", attemptId: input.attemptId, status: attempt.status, message: `Attempt '${input.attemptId}' is already ${attempt.status}.` });
1303
+ const event = attemptResultEvent(input, String(attempt.node_key));
1304
+ const instanceEvent = instanceResultEvent(input, String(attempt.node_key), event);
1305
+ const memberEvent = this.groupMemberResultEventForNode(input.runId, String(attempt.node_key), input.result);
1306
+ const events = [event, instanceEvent, ...(memberEvent ? [memberEvent] : [])];
1307
+ applySchedulerEvents(createSchedulerProjection(input.runId), [...this.schedulerEvents(input.runId), ...events]);
1308
+ const sequence = this.nextSequence(input.runId);
1309
+ let sequenceOffset = 0;
1310
+ this.db.prepare(`
1311
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1312
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1313
+ `).run(input.runId, sequence + sequenceOffset, event.type, String(attempt.node_key), encodeSchedulerPayload(event.payload), now, input.idempotencyKey);
1314
+ sequenceOffset += 1;
1315
+ this.db.prepare(`
1316
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1317
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1318
+ `).run(input.runId, sequence + sequenceOffset, instanceEvent.type, String(attempt.node_key), encodeSchedulerPayload(instanceEvent.payload), now, derivedIdempotencyKey(input.idempotencyKey, "instance"));
1319
+ sequenceOffset += 1;
1320
+ if (memberEvent) {
1321
+ this.db.prepare(`
1322
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1323
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1324
+ `).run(input.runId, sequence + sequenceOffset, memberEvent.type, String(attempt.node_key), encodeSchedulerPayload(memberEvent.payload), now, derivedIdempotencyKey(input.idempotencyKey, "member"));
1325
+ }
1326
+ this.syncSchedulerProjectionTables(input.runId, now);
1327
+ this.syncPublicRunProjection(input.runId, now);
1328
+ this.db.exec("COMMIT");
1329
+ return this.loadRunSnapshot(input.runId);
1330
+ }
1331
+ catch (error) {
1332
+ this.db.exec("ROLLBACK");
1333
+ throw error;
1334
+ }
1335
+ }
1336
+ tryConsumeSignal(input) {
1337
+ return schedulerStoreResult(() => this.consumeSignal(input));
1338
+ }
1339
+ consumeSignal(input) {
1340
+ const now = input.now ?? new Date();
1341
+ let snapshot = this.loadRunSnapshot(input.runId);
1342
+ let wait = snapshot.projection.signalWaits[input.nodeKey];
1343
+ if (!wait)
1344
+ throwSchedulerStoreError({ type: "signal-wait-not-found", runId: input.runId, nodeKey: input.nodeKey, message: `Signal wait '${input.nodeKey}' was not found.` });
1345
+ if (wait.status === "consumed" && stableJson(wait.payload) === stableJson(input.payload)) {
1346
+ return snapshot;
1347
+ }
1348
+ if (wait.status === "consumed") {
1349
+ throwSchedulerStoreError({ type: "signal-wait-terminal", runId: input.runId, nodeKey: input.nodeKey, status: wait.status, message: `Signal wait '${input.nodeKey}' has already consumed a different payload.` });
1350
+ }
1351
+ if (wait.status !== "awaiting") {
1352
+ throwSchedulerStoreError({ type: "signal-wait-terminal", runId: input.runId, nodeKey: input.nodeKey, status: wait.status, message: `Signal wait '${input.nodeKey}' is already ${wait.status}.` });
1353
+ }
1354
+ snapshot = this.drainDueSignalTimeouts(input.runId, input.ownerEpoch, now);
1355
+ wait = snapshot.projection.signalWaits[input.nodeKey];
1356
+ if (!wait)
1357
+ throwSchedulerStoreError({ type: "signal-wait-not-found", runId: input.runId, nodeKey: input.nodeKey, message: `Signal wait '${input.nodeKey}' was not found.` });
1358
+ if (wait.status !== "awaiting") {
1359
+ throwSchedulerStoreError({ type: "signal-wait-terminal", runId: input.runId, nodeKey: input.nodeKey, status: wait.status, message: `Signal wait '${input.nodeKey}' is already ${wait.status}.` });
1360
+ }
1361
+ if (snapshot.projection.run.status === "paused")
1362
+ throwSchedulerStoreError({ type: "run-paused", runId: input.runId, message: `Run '${input.runId}' is paused.` });
1363
+ const payloadDigest = createHash("sha256").update(stableJson(input.payload)).digest("hex");
1364
+ const events = [
1365
+ {
1366
+ type: "signal.consumed",
1367
+ payload: {
1368
+ nodeKey: input.nodeKey,
1369
+ payload: input.payload,
1370
+ payloadDigest,
1371
+ commandIdempotencyKey: input.commandIdempotencyKey,
1372
+ },
1373
+ },
1374
+ {
1375
+ type: "instance.completed",
1376
+ payload: {
1377
+ nodeKey: input.nodeKey,
1378
+ output: input.payload,
1379
+ },
1380
+ },
1381
+ ];
1382
+ const member = snapshot.projection.groupMembers[input.nodeKey];
1383
+ if (member?.status === "ready" || member?.status === "running") {
1384
+ events.push({
1385
+ type: "group.member_completed",
1386
+ payload: {
1387
+ memberKey: member.memberKey,
1388
+ completionSequence: snapshot.version + events.length + 1,
1389
+ output: input.payload,
1390
+ },
1391
+ });
1392
+ }
1393
+ return this.appendSchedulerEvents({
1394
+ runId: input.runId,
1395
+ expectedVersion: snapshot.version,
1396
+ ownerEpoch: input.ownerEpoch,
1397
+ idempotencyKey: input.idempotencyKey,
1398
+ events,
1399
+ });
1400
+ }
1401
+ tryPauseRun(input) {
1402
+ return schedulerStoreResult(() => this.pauseRun(input));
1403
+ }
1404
+ pauseRun(input) {
1405
+ const duplicate = this.duplicateIntentIdempotency(input.runId, input.idempotencyKey);
1406
+ if (duplicate)
1407
+ return duplicate;
1408
+ const now = input.now ?? new Date();
1409
+ const snapshot = this.drainDueSignalTimeouts(input.runId, input.ownerEpoch, now);
1410
+ if (snapshot.projection.run.status === "paused")
1411
+ return snapshot;
1412
+ const events = [
1413
+ { type: "control.paused", payload: input.reason === undefined ? {} : { reason: input.reason } },
1414
+ ];
1415
+ for (const wait of Object.values(snapshot.projection.signalWaits).filter(wait => wait.status === "awaiting" && wait.deadlineAt !== undefined)) {
1416
+ const deadlineAt = wait.deadlineAt;
1417
+ if (deadlineAt === undefined)
1418
+ continue;
1419
+ events.push({
1420
+ type: "signal.timeout_paused",
1421
+ payload: {
1422
+ nodeKey: wait.nodeKey,
1423
+ remainingMs: Math.max(0, new Date(deadlineAt).getTime() - now.getTime()),
1424
+ },
1425
+ });
1426
+ }
1427
+ const requeuedMemberKeys = new Set();
1428
+ for (const attempt of Object.values(snapshot.projection.attempts).filter(attempt => attempt.status === "started")) {
1429
+ const instance = snapshot.projection.instances[attempt.nodeKey];
1430
+ events.push({ type: "attempt.cancelled", payload: { attemptId: attempt.attemptId, cancelReason: "paused" } });
1431
+ if (instance?.status === "running" || instance?.status === "awaiting") {
1432
+ events.push({
1433
+ type: "instance.requeued",
1434
+ payload: {
1435
+ nodeKey: instance.nodeKey,
1436
+ reason: "paused",
1437
+ ...(instance.readinessSequence === undefined ? {} : { readinessSequence: instance.readinessSequence }),
1438
+ },
1439
+ });
1440
+ }
1441
+ for (const member of ancestorGroupMembersForNode(snapshot.projection, attempt.nodeKey)) {
1442
+ if (member.status !== "running" || requeuedMemberKeys.has(member.memberKey))
1443
+ continue;
1444
+ requeuedMemberKeys.add(member.memberKey);
1445
+ events.push({
1446
+ type: "group.member_requeued",
1447
+ payload: {
1448
+ memberKey: member.memberKey,
1449
+ reason: "paused",
1450
+ readinessSequence: member.readinessSequence,
1451
+ },
1452
+ });
1453
+ }
1454
+ }
1455
+ return this.appendSchedulerEvents({
1456
+ runId: input.runId,
1457
+ expectedVersion: snapshot.version,
1458
+ ownerEpoch: input.ownerEpoch,
1459
+ idempotencyKey: input.idempotencyKey,
1460
+ events,
1461
+ });
1462
+ }
1463
+ tryResumeRun(input) {
1464
+ return schedulerStoreResult(() => this.resumeRun(input));
1465
+ }
1466
+ resumeRun(input) {
1467
+ const duplicate = this.duplicateIntentIdempotency(input.runId, input.idempotencyKey);
1468
+ if (duplicate)
1469
+ return duplicate;
1470
+ const now = input.now ?? new Date();
1471
+ const snapshot = this.loadRunSnapshot(input.runId);
1472
+ if (snapshot.projection.run.status !== "paused") {
1473
+ return snapshot;
1474
+ }
1475
+ const events = [{ type: "control.resumed", payload: {} }];
1476
+ for (const wait of Object.values(snapshot.projection.signalWaits).filter(wait => wait.status === "awaiting" && wait.timeoutRemainingMs !== undefined)) {
1477
+ const timeoutRemainingMs = wait.timeoutRemainingMs;
1478
+ if (timeoutRemainingMs === undefined)
1479
+ continue;
1480
+ events.push({
1481
+ type: "signal.timeout_resumed",
1482
+ payload: {
1483
+ nodeKey: wait.nodeKey,
1484
+ deadlineAt: new Date(now.getTime() + timeoutRemainingMs).toISOString(),
1485
+ },
1486
+ });
1487
+ }
1488
+ return this.appendSchedulerEvents({
1489
+ runId: input.runId,
1490
+ expectedVersion: snapshot.version,
1491
+ ownerEpoch: input.ownerEpoch,
1492
+ idempotencyKey: input.idempotencyKey,
1493
+ events,
1494
+ });
1495
+ }
1496
+ tryRetryRun(input) {
1497
+ return schedulerStoreResult(() => this.retryRun(input));
1498
+ }
1499
+ retryRun(input) {
1500
+ const duplicate = this.duplicateIntentIdempotency(input.runId, input.idempotencyKey);
1501
+ if (duplicate)
1502
+ return duplicate;
1503
+ const snapshot = this.loadRunSnapshot(input.runId);
1504
+ if (snapshot.projection.run.status !== "failed") {
1505
+ throwSchedulerStoreError({ type: "invalid-retry-target", runId: input.runId, status: snapshot.projection.run.status, message: `Cannot retry run from ${snapshot.projection.run.status}.` });
1506
+ }
1507
+ return this.appendSchedulerEvents({
1508
+ runId: input.runId,
1509
+ expectedVersion: snapshot.version,
1510
+ ownerEpoch: input.ownerEpoch,
1511
+ idempotencyKey: input.idempotencyKey,
1512
+ events: [{ type: "control.run_retry_requested", payload: {} }],
1513
+ });
1514
+ }
1515
+ tryRetry(input) {
1516
+ return schedulerStoreResult(() => this.retry(input));
1517
+ }
1518
+ retry(input) {
1519
+ const idempotencyKey = input.idempotencyKey;
1520
+ const duplicate = this.duplicateIntentIdempotency(input.runId, idempotencyKey);
1521
+ if (duplicate)
1522
+ return duplicate;
1523
+ const snapshot = this.loadRunSnapshot(input.runId);
1524
+ const instance = snapshot.projection.instances[input.targetKey];
1525
+ const frame = snapshot.projection.frames[input.targetKey];
1526
+ if (!instance && !frame)
1527
+ throwSchedulerStoreError({ type: "missing-retry-target", runId: input.runId, targetKey: input.targetKey, message: `Retry target '${input.targetKey}' was not found.` });
1528
+ if (frame && !instance) {
1529
+ if (frame.status !== "failed") {
1530
+ throwSchedulerStoreError({ type: "invalid-retry-target", runId: input.runId, targetKey: input.targetKey, status: frame.status, message: `Frame '${input.targetKey}' cannot be retried from ${frame.status}.` });
1531
+ }
1532
+ if (frame.frameKind !== "node" && frame.frameKind !== "loop") {
1533
+ throwSchedulerStoreError({ type: "invalid-retry-target", runId: input.runId, targetKey: input.targetKey, status: frame.status, message: `Frame '${input.targetKey}' is not a retryable public node frame.` });
1534
+ }
1535
+ const events = [{ type: "frame.retry_requested", payload: { frameKey: input.targetKey, source: "control" } }];
1536
+ for (const member of ancestorGroupMembersForFrame(snapshot.projection, frame.parentFrameKey)) {
1537
+ if (member.status !== "failed") {
1538
+ throwSchedulerStoreError({ type: "invalid-retry-target", runId: input.runId, targetKey: input.targetKey, status: member.status, message: `Group member '${member.memberKey}' cannot be retried from ${member.status}.` });
1539
+ }
1540
+ events.push({
1541
+ type: "group.member_retry_requested",
1542
+ payload: {
1543
+ memberKey: member.memberKey,
1544
+ readinessSequence: member.readinessSequence,
1545
+ source: "control",
1546
+ },
1547
+ });
1548
+ }
1549
+ return this.appendSchedulerEvents({
1550
+ runId: input.runId,
1551
+ expectedVersion: snapshot.version,
1552
+ ownerEpoch: input.ownerEpoch,
1553
+ idempotencyKey,
1554
+ events,
1555
+ });
1556
+ }
1557
+ if (!instance)
1558
+ throwSchedulerStoreError({ type: "missing-retry-target", runId: input.runId, targetKey: input.targetKey, message: `Retry target '${input.targetKey}' was not found.` });
1559
+ if (instance.status !== "failed") {
1560
+ throwSchedulerStoreError({ type: "invalid-retry-target", runId: input.runId, targetKey: input.targetKey, status: instance.status, message: `Node instance '${input.targetKey}' cannot be retried from ${instance.status}.` });
1561
+ }
1562
+ const events = [
1563
+ {
1564
+ type: "instance.retry_requested",
1565
+ payload: {
1566
+ nodeKey: input.targetKey,
1567
+ ...(instance.readinessSequence === undefined ? {} : { readinessSequence: instance.readinessSequence }),
1568
+ source: "control",
1569
+ },
1570
+ },
1571
+ ];
1572
+ const members = ancestorGroupMembersForNode(snapshot.projection, input.targetKey);
1573
+ for (const member of members) {
1574
+ if (member.status !== "failed") {
1575
+ throwSchedulerStoreError({ type: "invalid-retry-target", runId: input.runId, targetKey: input.targetKey, status: member.status, message: `Group member '${member.memberKey}' cannot be retried from ${member.status}.` });
1576
+ }
1577
+ events.push({
1578
+ type: "group.member_retry_requested",
1579
+ payload: {
1580
+ memberKey: member.memberKey,
1581
+ readinessSequence: member.readinessSequence,
1582
+ source: "control",
1583
+ },
1584
+ });
1585
+ }
1586
+ return this.appendSchedulerEvents({
1587
+ runId: input.runId,
1588
+ expectedVersion: snapshot.version,
1589
+ ownerEpoch: input.ownerEpoch,
1590
+ idempotencyKey,
1591
+ events,
1592
+ });
1593
+ }
1594
+ tryCancel(input) {
1595
+ return schedulerStoreResult(() => this.cancel(input));
1596
+ }
1597
+ cancel(input) {
1598
+ const duplicate = this.duplicateIntentIdempotency(input.runId, input.idempotencyKey);
1599
+ if (duplicate)
1600
+ return duplicate;
1601
+ const snapshot = this.loadRunSnapshot(input.runId);
1602
+ if (input.targetKey === undefined && snapshot.projection.run.status === "canceled")
1603
+ return snapshot;
1604
+ const targetKey = input.targetKey ?? "root";
1605
+ const events = targetKey === "root" && !snapshot.projection.frames.root && snapshot.projection.run.status === "pending"
1606
+ ? [
1607
+ { type: "frame.started", payload: { runId: input.runId, frameKey: "root", frameKind: "root" } },
1608
+ { type: "frame.cancelled", payload: { frameKey: "root", cancelReason: "operator_cancelled" } },
1609
+ ]
1610
+ : targetKey === "root"
1611
+ ? cancellationEventsForFrame(snapshot.projection, "root", "operator_cancelled")
1612
+ : snapshot.projection.frames[targetKey]
1613
+ ? cancellationEventsForFrame(snapshot.projection, targetKey, "operator_cancelled")
1614
+ : cancellationEventsForNode(snapshot.projection, targetKey, "operator_cancelled");
1615
+ if (events.length === 0) {
1616
+ const status = snapshot.projection.frames[targetKey]?.status ?? snapshot.projection.instances[targetKey]?.status;
1617
+ if (status)
1618
+ throwSchedulerStoreError({ type: "invalid-cancel-target", runId: input.runId, targetKey, status, message: `Cancel target '${targetKey}' is already ${status}.` });
1619
+ throwSchedulerStoreError({ type: "missing-cancel-target", runId: input.runId, targetKey, message: `Cancel target '${targetKey}' was not found.` });
1620
+ }
1621
+ return this.appendSchedulerEvents({
1622
+ runId: input.runId,
1623
+ expectedVersion: snapshot.version,
1624
+ ownerEpoch: input.ownerEpoch,
1625
+ idempotencyKey: input.idempotencyKey,
1626
+ events,
1627
+ });
1628
+ }
1629
+ tryMarkExpiredOwnerAttemptsSuperseded(runId, ownerEpoch) {
1630
+ return schedulerStoreResult(() => this.markExpiredOwnerAttemptsSuperseded(runId, ownerEpoch));
1631
+ }
1632
+ markExpiredOwnerAttemptsSuperseded(runId, ownerEpoch) {
1633
+ const now = new Date().toISOString();
1634
+ this.db.exec("BEGIN IMMEDIATE");
1635
+ try {
1636
+ this.assertOwnerEpochExpired(runId, ownerEpoch);
1637
+ const attempts = this.db.prepare("SELECT attempt_id, node_key FROM node_attempts WHERE run_id = ? AND owner_epoch = ? AND status = 'started'").all(runId, ownerEpoch);
1638
+ for (const attempt of attempts) {
1639
+ const projection = applySchedulerEvents(createSchedulerProjection(runId), this.schedulerEvents(runId));
1640
+ const instance = projection.instances[attempt.node_key];
1641
+ const members = ancestorGroupMembersForNode(projection, attempt.node_key).filter(member => member.status === "running");
1642
+ const events = [
1643
+ { type: "attempt.superseded", payload: { attemptId: attempt.attempt_id, cancelReason: "superseded" } },
1644
+ ...(instance && (instance.status === "running" || instance.status === "awaiting")
1645
+ ? [{ type: "instance.requeued", payload: { nodeKey: instance.nodeKey, reason: "superseded", ...(instance.readinessSequence === undefined ? {} : { readinessSequence: instance.readinessSequence }) } }]
1646
+ : []),
1647
+ ...members.map(member => ({ type: "group.member_requeued", payload: { memberKey: member.memberKey, reason: "superseded", readinessSequence: member.readinessSequence } })),
1648
+ ];
1649
+ applySchedulerEvents(createSchedulerProjection(runId), [...this.schedulerEvents(runId), ...events]);
1650
+ const sequence = this.nextSequence(runId);
1651
+ for (const [index, event] of events.entries()) {
1652
+ this.db.prepare(`
1653
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1654
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1655
+ `).run(runId, sequence + index, event.type, eventNodeKey(event) ?? attempt.node_key, encodeSchedulerPayload(event.payload), now, `supersede:${runId}:${attempt.attempt_id}:${index}`);
1656
+ }
1657
+ }
1658
+ this.syncSchedulerProjectionTables(runId, now);
1659
+ this.syncPublicRunProjection(runId, now);
1660
+ this.db.exec("COMMIT");
1661
+ return this.loadRunSnapshot(runId);
1662
+ }
1663
+ catch (error) {
1664
+ this.db.exec("ROLLBACK");
1665
+ throw error;
1666
+ }
1667
+ }
1668
+ schedulerEvents(runId) {
1669
+ return schedulerEvents(this.db, runId);
1670
+ }
1671
+ timestampedSchedulerEvents(runId) {
1672
+ return timestampedSchedulerEvents(this.db, runId);
1673
+ }
1674
+ currentVersion(runId) {
1675
+ return this.nextSequence(runId) - 1;
1676
+ }
1677
+ nextSequence(runId) {
1678
+ const row = this.db.prepare("SELECT COALESCE(MAX(sequence), 0) + 1 AS count FROM run_events WHERE run_id = ?").get(runId);
1679
+ return row?.count ?? 1;
1680
+ }
1681
+ requireOwnerEpoch(runId, ownerEpoch) {
1682
+ const row = this.db.prepare("SELECT owner_epoch, lease_expires_at, released_at FROM run_leases WHERE run_id = ?").get(runId);
1683
+ const now = new Date().toISOString();
1684
+ if (!row || row.owner_epoch !== ownerEpoch || row.released_at !== null || row.lease_expires_at <= now) {
1685
+ throwSchedulerStoreError({ type: "owner-epoch-inactive", runId, ownerEpoch, message: `Run '${runId}' scheduler owner epoch is not active.` });
1686
+ }
1687
+ }
1688
+ drainDueSignalTimeouts(runId, ownerEpoch, now) {
1689
+ const frozen = this.loadFrozenRun(runId);
1690
+ return drainDerivedTransitions(this, runId, { runId, ownerEpoch }, () => now, snapshot => continueRootEvents(frozen.ir, snapshot.projection, rootScope(frozen)), () => undefined);
1691
+ }
1692
+ loadFrozenRun(runId) {
1693
+ const row = this.db.prepare(`
1694
+ SELECT runs.id, runs.name, runs.workflow_entry, run_inputs.workflow_ir_json, run_inputs.input_json, run_inputs.agent_overrides_json
1695
+ FROM run_inputs
1696
+ JOIN runs ON runs.id = run_inputs.run_id
1697
+ WHERE run_inputs.run_id = ?
1698
+ `).get(runId);
1699
+ if (!row?.workflow_ir_json)
1700
+ throw new Error(`Run '${runId}' has no frozen workflow.`);
1701
+ const originalIr = JSON.parse(row.workflow_ir_json);
1702
+ const agentOverrides = parseAgentOverrides(row.agent_overrides_json);
1703
+ return {
1704
+ ir: withAgentOverrides(originalIr, agentOverrides),
1705
+ input: JSON.parse(row.input_json),
1706
+ agentOverrides,
1707
+ meta: {
1708
+ runId: String(row.id),
1709
+ workflowPath: String(row.workflow_entry),
1710
+ workflowName: String(row.name),
1711
+ workspaceDir: resolve(this.cwd),
1712
+ },
1713
+ };
1714
+ }
1715
+ assertOwnerEpochExpired(runId, ownerEpoch) {
1716
+ const row = this.db.prepare("SELECT owner_epoch, lease_expires_at, released_at FROM run_leases WHERE run_id = ?").get(runId);
1717
+ const now = new Date().toISOString();
1718
+ if (row && row.owner_epoch === ownerEpoch && row.released_at === null && row.lease_expires_at > now) {
1719
+ throwSchedulerStoreError({ type: "owner-epoch-still-active", runId, ownerEpoch, message: `Run '${runId}' scheduler owner epoch ${ownerEpoch} is still active.` });
1720
+ }
1721
+ }
1722
+ eventByIdempotencyKey(idempotencyKey) {
1723
+ const row = this.db.prepare("SELECT run_id, type, payload_json FROM run_events WHERE idempotency_key = ?").get(idempotencyKey);
1724
+ if (!row)
1725
+ return undefined;
1726
+ if (!isSchedulerEventType(row.type)) {
1727
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey, runId: row.run_id, message: `Scheduler idempotency key '${idempotencyKey}' conflicts with non-scheduler event.` });
1728
+ }
1729
+ return { run_id: row.run_id, type: row.type, payload: decodeSchedulerPayload(row.payload_json, row.type) };
1730
+ }
1731
+ groupMemberStartedEventsForNode(runId, nodeKey) {
1732
+ const projection = applySchedulerEvents(createSchedulerProjection(runId), this.schedulerEvents(runId));
1733
+ return ancestorGroupMembersForNode(projection, nodeKey)
1734
+ .filter(member => member.status === "ready")
1735
+ .map(member => ({ type: "group.member_started", payload: { memberKey: member.memberKey } }));
1736
+ }
1737
+ groupMemberResultEventForNode(runId, nodeKey, result) {
1738
+ const projection = applySchedulerEvents(createSchedulerProjection(runId), this.schedulerEvents(runId));
1739
+ const member = projection.groupMembers[nodeKey];
1740
+ if (!member || member.status !== "running")
1741
+ return undefined;
1742
+ if (result.status === "completed") {
1743
+ return { type: "group.member_completed", payload: { memberKey: member.memberKey, completionSequence: this.nextSequence(runId), ...(result.output === undefined ? {} : { output: result.output }) } };
1744
+ }
1745
+ if (result.status === "cancelled")
1746
+ return { type: "group.member_cancelled", payload: { memberKey: member.memberKey, cancelReason: result.reason } };
1747
+ return { type: "group.member_failed", payload: { memberKey: member.memberKey, error: { reason: result.reason }, terminalReason: result.status === "timed_out" ? "timed_out" : result.reason } };
1748
+ }
1749
+ duplicateAppendIdempotency(commit) {
1750
+ const row = this.db.prepare(`
1751
+ SELECT event_count, event_digest
1752
+ FROM scheduler_commits
1753
+ WHERE run_id = ? AND idempotency_key = ?
1754
+ `).get(commit.runId, commit.idempotencyKey);
1755
+ if (!row)
1756
+ return undefined;
1757
+ if (row.event_count !== commit.events.length || row.event_digest !== schedulerCommitDigest(commit.events)) {
1758
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey: commit.idempotencyKey, runId: commit.runId, message: `Scheduler commit idempotency key '${commit.idempotencyKey}' conflicts with different events.` });
1759
+ }
1760
+ return this.loadRunSnapshot(commit.runId);
1761
+ }
1762
+ duplicateIntentIdempotency(runId, idempotencyKey) {
1763
+ const row = this.db.prepare(`
1764
+ SELECT run_id
1765
+ FROM scheduler_commits
1766
+ WHERE idempotency_key = ?
1767
+ `).get(idempotencyKey);
1768
+ if (!row)
1769
+ return undefined;
1770
+ if (row.run_id !== runId)
1771
+ throwSchedulerStoreError({ type: "idempotency-conflict", idempotencyKey, runId, message: `Scheduler intent idempotency key '${idempotencyKey}' conflicts with another run.` });
1772
+ return this.loadRunSnapshot(runId);
1773
+ }
1774
+ syncSchedulerProjectionTables(runId, now) {
1775
+ const { projection, timings } = applyTimestampedSchedulerEvents(runId, this.timestampedSchedulerEvents(runId));
1776
+ const existingSignalWaits = new Map(this.db.prepare("SELECT node_key, consumed_at, timed_out_at, created_at FROM signal_waits WHERE run_id = ?").all(runId)
1777
+ .map(row => [row.node_key, row]));
1778
+ this.db.prepare("DELETE FROM scheduler_frames WHERE run_id = ?").run(runId);
1779
+ this.db.prepare("DELETE FROM node_instances WHERE run_id = ?").run(runId);
1780
+ this.db.prepare("DELETE FROM node_attempts WHERE run_id = ?").run(runId);
1781
+ this.db.prepare("DELETE FROM group_members WHERE run_id = ?").run(runId);
1782
+ this.db.prepare("DELETE FROM signal_waits WHERE run_id = ?").run(runId);
1783
+ for (const frame of Object.values(projection.frames)) {
1784
+ const timing = timings.frame.get(frame.frameKey);
1785
+ this.db.prepare(`
1786
+ INSERT INTO scheduler_frames (
1787
+ run_id, frame_key, parent_frame_key, node_key, node_id, frame_kind, status, strategy,
1788
+ terminal_reason, instance_path_json, scope_json, loop_json, result_json, error_json, created_at, updated_at
1789
+ )
1790
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1791
+ `).run(frame.runId, frame.frameKey, frame.parentFrameKey ?? null, frame.nodeKey ?? null, frame.nodeId ?? null, frame.frameKind, frame.status, frame.strategy ?? null, frame.terminalReason ?? null, frame.instancePath === undefined ? null : stableJson(frame.instancePath), stableJson(frame.scope), frame.loop === undefined ? null : stableJson(frame.loop), frame.result === undefined ? null : stableJson(frame.result), frame.error === undefined ? null : stableJson(frame.error), timing?.createdAt ?? now, timing?.updatedAt ?? now);
1792
+ }
1793
+ for (const instance of Object.values(projection.instances)) {
1794
+ const timing = timings.instance.get(instance.nodeKey);
1795
+ this.db.prepare(`
1796
+ INSERT INTO node_instances (
1797
+ run_id, node_key, node_id, parent_frame_key, instance_path_json, status, status_reason,
1798
+ readiness_sequence, output_json, error_json, accepted_attempt_id, created_at, updated_at
1799
+ )
1800
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1801
+ `).run(instance.runId, instance.nodeKey, instance.nodeId, instance.parentFrameKey ?? null, stableJson(instance.instancePath), instance.status, instance.statusReason ?? null, instance.readinessSequence ?? null, instance.output === undefined ? null : stableJson(instance.output), instance.error === undefined ? null : stableJson(instance.error), instance.acceptedAttemptId ?? null, timing?.createdAt ?? now, timing?.updatedAt ?? now);
1802
+ }
1803
+ for (const attempt of Object.values(projection.attempts)) {
1804
+ const timing = timings.attempt.get(attempt.attemptId);
1805
+ this.db.prepare(`
1806
+ INSERT INTO node_attempts (
1807
+ run_id, attempt_id, node_key, node_id, attempt_no, owner_epoch, status, deadline_at,
1808
+ started_at, finished_at, result_json, error_json, terminal_reason, cancel_reason
1809
+ )
1810
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1811
+ `).run(attempt.runId, attempt.attemptId, attempt.nodeKey, attempt.nodeId, attempt.attemptNo, attempt.ownerEpoch, attempt.status, attempt.deadlineAt ?? null, timing?.createdAt ?? now, attempt.status === "started" ? null : timing?.updatedAt ?? now, attempt.result === undefined ? null : stableJson(attempt.result), attempt.error === undefined ? null : stableJson(attempt.error), attempt.terminalReason ?? null, attempt.cancelReason ?? null);
1812
+ }
1813
+ for (const member of Object.values(projection.groupMembers)) {
1814
+ const timing = timings.member.get(member.memberKey);
1815
+ this.db.prepare(`
1816
+ INSERT INTO group_members (
1817
+ run_id, group_key, member_key, member_kind, branch_id, item_key, item_index, item_json, child_frame_key,
1818
+ status, readiness_sequence, completion_sequence, accepted_rank, terminal_reason, output_json, error_json, created_at, updated_at
1819
+ )
1820
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1821
+ `).run(member.runId, member.groupKey, member.memberKey, member.memberKind, member.branchId ?? null, member.itemKey === undefined ? null : String(member.itemKey), member.itemIndex ?? null, member.item === undefined ? null : stableJson(member.item), member.childFrameKey ?? null, member.status, member.readinessSequence, member.completionSequence ?? null, member.acceptedRank ?? null, member.terminalReason ?? null, member.output === undefined ? null : stableJson(member.output), member.error === undefined ? null : stableJson(member.error), timing?.createdAt ?? now, timing?.updatedAt ?? now);
1822
+ }
1823
+ for (const wait of Object.values(projection.signalWaits)) {
1824
+ const existing = existingSignalWaits.get(wait.nodeKey);
1825
+ const timing = timings.signal.get(wait.nodeKey);
1826
+ this.db.prepare(`
1827
+ INSERT INTO signal_waits (
1828
+ run_id, node_key, node_id, status, payload_json, payload_digest, command_idempotency_key,
1829
+ deadline_at, timeout_message, timeout_remaining_ms, rendered_prompt, consumed_at, timed_out_at, terminal_reason, created_at, updated_at
1830
+ )
1831
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1832
+ `).run(wait.runId, wait.nodeKey, wait.nodeId, wait.status, wait.payload === undefined ? null : stableJson(wait.payload), wait.payloadDigest ?? null, wait.commandIdempotencyKey ?? null, wait.deadlineAt ?? null, wait.timeoutMessage ?? null, wait.timeoutRemainingMs ?? null, wait.renderedPrompt ?? null, wait.status === "consumed" ? timing?.updatedAt ?? existing?.consumed_at ?? now : null, wait.status === "timed_out" ? timing?.updatedAt ?? existing?.timed_out_at ?? now : null, wait.terminalReason ?? null, timing?.createdAt ?? existing?.created_at ?? now, timing?.updatedAt ?? now);
1833
+ }
1834
+ }
1835
+ syncPublicRunProjection(runId, now) {
1836
+ const schedulerEvents = this.schedulerEvents(runId);
1837
+ const projection = applySchedulerEvents(createSchedulerProjection(runId), schedulerEvents);
1838
+ const current = this.db.prepare("SELECT status FROM runs WHERE id = ?").get(runId);
1839
+ const hasTargetedRetry = schedulerEvents.some(event => event.type === "instance.retry_requested" || event.type === "frame.retry_requested");
1840
+ if (current?.status === "failed" && projection.run.status === "pending" && Object.keys(projection.frames).length === 0) {
1841
+ this.db.prepare("UPDATE runs SET status = 'pending', updated_at = ? WHERE id = ?").run(now, runId);
1842
+ this.db.prepare("UPDATE run_inputs SET output_json = NULL WHERE run_id = ?").run(runId);
1843
+ this.db.prepare("DELETE FROM node_states WHERE run_id = ?").run(runId);
1844
+ return;
1845
+ }
1846
+ if (!current || current.status === "completed" || current.status === "canceled" || (current.status === "failed" && !hasTargetedRetry))
1847
+ return;
1848
+ this.syncPublicNodeStates(projection, now);
1849
+ const root = projection.frames.root;
1850
+ if (projection.run.status === "completed") {
1851
+ const output = root?.result ?? {};
1852
+ this.db.prepare("UPDATE runs SET status = 'completed', updated_at = ? WHERE id = ?").run(now, runId);
1853
+ this.db.prepare("UPDATE run_inputs SET output_json = ? WHERE run_id = ?").run(stableJson(output), runId);
1854
+ this.insertPublicRunEvent(runId, "run.completed", { output }, now, `scheduler-public:completed:${runId}:${rootTerminalEventCount(this.schedulerEvents(runId), "frame.completed")}`);
1855
+ return;
1856
+ }
1857
+ if (projection.run.status === "failed") {
1858
+ const error = root?.error ?? { reason: root?.terminalReason ?? "scheduler_failed" };
1859
+ this.db.prepare("UPDATE runs SET status = 'failed', updated_at = ? WHERE id = ?").run(now, runId);
1860
+ this.db.prepare("UPDATE run_inputs SET output_json = NULL WHERE run_id = ?").run(runId);
1861
+ this.insertPublicRunEvent(runId, "run.failed", error, now, `scheduler-public:failed:${runId}:${rootTerminalEventCount(this.schedulerEvents(runId), "frame.failed")}`);
1862
+ return;
1863
+ }
1864
+ if (projection.run.status === "canceled") {
1865
+ this.db.prepare("UPDATE runs SET status = 'canceled', updated_at = ? WHERE id = ?").run(now, runId);
1866
+ this.db.prepare("UPDATE run_inputs SET output_json = NULL WHERE run_id = ?").run(runId);
1867
+ this.insertPublicRunEvent(runId, "run.canceled", { reason: root?.terminalReason ?? "operator_cancelled" }, now, `scheduler-public:canceled:${runId}:${rootTerminalEventCount(this.schedulerEvents(runId), "frame.cancelled")}`);
1868
+ return;
1869
+ }
1870
+ if (projection.run.status === "paused") {
1871
+ this.db.prepare("UPDATE runs SET status = 'paused', updated_at = ? WHERE id = ? AND status NOT IN ('completed', 'failed', 'canceled')").run(now, runId);
1872
+ return;
1873
+ }
1874
+ const status = publicRunStatus(projection);
1875
+ if (current.status === "failed") {
1876
+ this.db.prepare("UPDATE runs SET status = ?, updated_at = ? WHERE id = ?").run(status, now, runId);
1877
+ this.db.prepare("UPDATE run_inputs SET output_json = NULL WHERE run_id = ?").run(runId);
1878
+ return;
1879
+ }
1880
+ this.db.prepare("UPDATE runs SET status = ?, updated_at = ? WHERE id = ? AND status NOT IN ('completed', 'failed', 'canceled')").run(status, now, runId);
1881
+ }
1882
+ syncPublicNodeStates(projection, now) {
1883
+ const nodeKeys = Object.keys(projection.instances);
1884
+ const dynamicNodeIds = [...new Set(Object.values(projection.instances)
1885
+ .filter(instance => instance.nodeKey !== instance.nodeId)
1886
+ .map(instance => instance.nodeId))];
1887
+ if (dynamicNodeIds.length > 0) {
1888
+ const placeholders = dynamicNodeIds.map(() => "?").join(", ");
1889
+ this.db.prepare(`DELETE FROM node_states WHERE run_id = ? AND node_key = node_id AND node_id IN (${placeholders})`).run(projection.run.runId, ...dynamicNodeIds);
1890
+ }
1891
+ const historicalNodeKeys = schedulerEvents(this.db, projection.run.runId)
1892
+ .filter(event => event.type === "instance.ready")
1893
+ .map(event => event.payload.nodeKey)
1894
+ .filter(nodeKey => !nodeKeys.includes(nodeKey));
1895
+ if (historicalNodeKeys.length > 0) {
1896
+ const placeholders = historicalNodeKeys.map(() => "?").join(", ");
1897
+ this.db.prepare(`DELETE FROM node_states WHERE run_id = ? AND node_key IN (${placeholders})`).run(projection.run.runId, ...historicalNodeKeys);
1898
+ }
1899
+ for (const instance of Object.values(projection.instances)) {
1900
+ this.db.prepare(`
1901
+ INSERT INTO node_states (run_id, node_key, node_id, status, output_json, error_json, created_at, updated_at)
1902
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1903
+ ON CONFLICT(run_id, node_key) DO UPDATE SET
1904
+ node_id = excluded.node_id,
1905
+ status = excluded.status,
1906
+ output_json = excluded.output_json,
1907
+ error_json = excluded.error_json,
1908
+ updated_at = excluded.updated_at
1909
+ `).run(instance.runId, instance.nodeKey, instance.nodeId, instance.status, instance.output === undefined ? null : stableJson(instance.output), instance.error === undefined ? null : stableJson(instance.error), now, now);
1910
+ }
1911
+ }
1912
+ insertPublicRunEvent(runId, type, payload, now, idempotencyKey) {
1913
+ const existing = this.db.prepare("SELECT id FROM run_events WHERE idempotency_key = ?").get(idempotencyKey);
1914
+ if (existing)
1915
+ return;
1916
+ this.db.prepare(`
1917
+ INSERT INTO run_events (run_id, sequence, type, node_key, payload_json, created_at, idempotency_key)
1918
+ VALUES (?, ?, ?, NULL, ?, ?, ?)
1919
+ `).run(runId, this.nextSequence(runId), type, stableJson(payload), now, idempotencyKey);
1920
+ }
1921
+ }
1922
+ function schedulerEvents(db, runId) {
1923
+ const rows = db.prepare("SELECT type, payload_json FROM run_events WHERE run_id = ? ORDER BY sequence").all(runId);
1924
+ return rows.flatMap(row => {
1925
+ if (!isSchedulerEventType(row.type))
1926
+ return [];
1927
+ return [{ type: row.type, payload: decodeSchedulerPayload(row.payload_json, row.type) }];
1928
+ });
1929
+ }
1930
+ function timestampedSchedulerEvents(db, runId) {
1931
+ const rows = db.prepare("SELECT type, payload_json, created_at FROM run_events WHERE run_id = ? ORDER BY sequence").all(runId);
1932
+ return rows.flatMap(row => {
1933
+ if (!isSchedulerEventType(row.type))
1934
+ return [];
1935
+ return [{ event: { type: row.type, payload: decodeSchedulerPayload(row.payload_json, row.type) }, createdAt: row.created_at }];
1936
+ });
1937
+ }
1938
+ function rootTerminalEventCount(events, type) {
1939
+ return events.filter(event => event.type === type && event.payload.frameKey === "root").length;
1940
+ }
1941
+ function publicRunStatus(projection) {
1942
+ if (projection.run.status === "awaiting" || hasAwaitingWork(projection))
1943
+ return "awaiting";
1944
+ if (hasRunningWork(projection))
1945
+ return "running";
1946
+ return "pending";
1947
+ }
1948
+ function hasAwaitingWork(projection) {
1949
+ return Object.values(projection.instances).some(instance => instance.status === "awaiting")
1950
+ || Object.values(projection.frames).some(frame => frame.status === "awaiting")
1951
+ || Object.values(projection.signalWaits).some(wait => wait.status === "awaiting");
1952
+ }
1953
+ function hasRunningWork(projection) {
1954
+ if (projection.frames.root !== undefined)
1955
+ return true;
1956
+ return Object.values(projection.frames).some(frame => frame.status === "ready" || frame.status === "running")
1957
+ || Object.values(projection.instances).some(instance => instance.status === "ready" || instance.status === "running")
1958
+ || Object.values(projection.groupMembers).some(member => member.status === "ready" || member.status === "running")
1959
+ || Object.values(projection.groups).some(group => group.status === "running")
1960
+ || Object.values(projection.attempts).some(attempt => attempt.status === "started");
1961
+ }
1962
+ function readRunDynamicFrames(db, runId) {
1963
+ const rows = db.prepare(`
1964
+ SELECT frame_key, parent_frame_key, node_key, node_id, frame_kind, status, strategy,
1965
+ terminal_reason, instance_path_json, result_json, error_json, created_at, updated_at
1966
+ FROM scheduler_frames
1967
+ WHERE run_id = ?
1968
+ ORDER BY frame_key
1969
+ `).all(runId);
1970
+ return rows.map(row => withoutUndefined({
1971
+ frameKey: String(row.frame_key),
1972
+ parentFrameKey: nullableString(row.parent_frame_key),
1973
+ nodeKey: nullableString(row.node_key),
1974
+ nodeId: nullableString(row.node_id),
1975
+ instancePath: parseOptionalJson(row.instance_path_json),
1976
+ frameKind: String(row.frame_kind),
1977
+ status: String(row.status),
1978
+ strategy: nullableString(row.strategy),
1979
+ terminalReason: nullableString(row.terminal_reason),
1980
+ result: parseOptionalJson(row.result_json),
1981
+ error: parseOptionalJson(row.error_json),
1982
+ createdAt: String(row.created_at),
1983
+ updatedAt: String(row.updated_at),
1984
+ }));
1985
+ }
1986
+ function readRunDynamicNodeInstances(db, runId) {
1987
+ const rows = db.prepare(`
1988
+ SELECT node_key, node_id, parent_frame_key, instance_path_json, status, status_reason,
1989
+ output_json, error_json, accepted_attempt_id, created_at, updated_at
1990
+ FROM node_instances
1991
+ WHERE run_id = ?
1992
+ ORDER BY node_key
1993
+ `).all(runId);
1994
+ return rows.map(row => withoutUndefined({
1995
+ nodeKey: String(row.node_key),
1996
+ nodeId: String(row.node_id),
1997
+ parentFrameKey: nullableString(row.parent_frame_key),
1998
+ instancePath: parseOptionalJson(row.instance_path_json),
1999
+ status: String(row.status),
2000
+ statusReason: nullableString(row.status_reason),
2001
+ output: parseOptionalJson(row.output_json),
2002
+ error: parseOptionalJson(row.error_json),
2003
+ acceptedAttemptId: nullableString(row.accepted_attempt_id),
2004
+ createdAt: String(row.created_at),
2005
+ updatedAt: String(row.updated_at),
2006
+ }));
2007
+ }
2008
+ function readRunDynamicAttempts(db, runId) {
2009
+ const rows = db.prepare(`
2010
+ SELECT attempt_id, node_key, node_id, attempt_no, status, deadline_at,
2011
+ started_at, finished_at, result_json, error_json, terminal_reason, cancel_reason
2012
+ FROM node_attempts
2013
+ WHERE run_id = ?
2014
+ ORDER BY attempt_id
2015
+ `).all(runId);
2016
+ return rows.map(row => withoutUndefined({
2017
+ attemptId: String(row.attempt_id),
2018
+ nodeKey: String(row.node_key),
2019
+ nodeId: String(row.node_id),
2020
+ attemptNo: Number(row.attempt_no),
2021
+ status: String(row.status),
2022
+ deadlineAt: nullableString(row.deadline_at),
2023
+ startedAt: String(row.started_at),
2024
+ finishedAt: nullableString(row.finished_at),
2025
+ result: parseOptionalJson(row.result_json),
2026
+ error: parseOptionalJson(row.error_json),
2027
+ terminalReason: nullableString(row.terminal_reason),
2028
+ cancelReason: nullableString(row.cancel_reason),
2029
+ }));
2030
+ }
2031
+ function readRunDynamicGroupMembers(db, runId) {
2032
+ const rows = db.prepare(`
2033
+ SELECT group_key, member_key, member_kind, branch_id, item_key, item_index, item_json, child_frame_key,
2034
+ status, completion_sequence, accepted_rank, terminal_reason, output_json, error_json, created_at, updated_at
2035
+ FROM group_members
2036
+ WHERE run_id = ?
2037
+ ORDER BY member_key
2038
+ `).all(runId);
2039
+ return rows.map(row => withoutUndefined({
2040
+ groupKey: String(row.group_key),
2041
+ memberKey: String(row.member_key),
2042
+ memberKind: String(row.member_kind),
2043
+ branchId: nullableString(row.branch_id),
2044
+ itemKey: nullableString(row.item_key),
2045
+ itemIndex: nullableNumber(row.item_index),
2046
+ item: parseOptionalJson(row.item_json),
2047
+ childFrameKey: nullableString(row.child_frame_key),
2048
+ status: String(row.status),
2049
+ completionSequence: nullableNumber(row.completion_sequence),
2050
+ acceptedRank: nullableNumber(row.accepted_rank),
2051
+ terminalReason: nullableString(row.terminal_reason),
2052
+ output: parseOptionalJson(row.output_json),
2053
+ error: parseOptionalJson(row.error_json),
2054
+ createdAt: String(row.created_at),
2055
+ updatedAt: String(row.updated_at),
2056
+ }));
2057
+ }
2058
+ function readRunDynamicSignalWaits(db, runId) {
2059
+ const rows = db.prepare(`
2060
+ SELECT node_key, node_id, status, deadline_at,
2061
+ timeout_message, timeout_remaining_ms, rendered_prompt, terminal_reason, created_at, updated_at
2062
+ FROM signal_waits
2063
+ WHERE run_id = ?
2064
+ ORDER BY node_key
2065
+ `).all(runId);
2066
+ return rows.map(row => withoutUndefined({
2067
+ nodeKey: String(row.node_key),
2068
+ nodeId: String(row.node_id),
2069
+ status: String(row.status),
2070
+ deadlineAt: nullableString(row.deadline_at),
2071
+ timeoutMessage: nullableString(row.timeout_message),
2072
+ timeoutRemainingMs: nullableNumber(row.timeout_remaining_ms),
2073
+ renderedPrompt: nullableString(row.rendered_prompt),
2074
+ terminalReason: nullableString(row.terminal_reason),
2075
+ createdAt: String(row.created_at),
2076
+ updatedAt: String(row.updated_at),
2077
+ }));
2078
+ }
2079
+ function readRunExecutionMetadata(db, runId) {
2080
+ const rows = db.prepare(`
2081
+ SELECT id, attempt_id, kind, metadata_json, created_at
2082
+ FROM execution_metadata
2083
+ WHERE run_id = ?
2084
+ ORDER BY id
2085
+ `).all(runId);
2086
+ return rows.map(row => withoutUndefined({
2087
+ id: Number(row.id),
2088
+ attemptId: nullableString(row.attempt_id),
2089
+ kind: String(row.kind),
2090
+ metadata: JSON.parse(String(row.metadata_json)),
2091
+ createdAt: String(row.created_at),
2092
+ }));
2093
+ }
2094
+ function readRunNodeProgress(db, runId) {
2095
+ const rows = db.prepare(`
2096
+ SELECT node_key, node_id, attempt_id, attempt_no, kind, status, message,
2097
+ output_tail, output_total_bytes, output_truncated,
2098
+ context_json, token_usage_json, tools_json, updated_at
2099
+ FROM node_progress
2100
+ WHERE run_id = ?
2101
+ ORDER BY updated_at ASC, node_key ASC
2102
+ `).all(runId);
2103
+ return rows.map(row => withoutUndefined({
2104
+ nodeKey: String(row.node_key),
2105
+ nodeId: String(row.node_id),
2106
+ attemptId: nullableString(row.attempt_id),
2107
+ attemptNo: nullableNumber(row.attempt_no),
2108
+ kind: String(row.kind),
2109
+ status: String(row.status),
2110
+ message: nullableString(row.message),
2111
+ output: row.output_tail === null ? undefined : {
2112
+ tail: String(row.output_tail),
2113
+ totalBytes: Number(row.output_total_bytes ?? 0),
2114
+ truncated: Boolean(row.output_truncated),
2115
+ },
2116
+ context: row.context_json === null ? undefined : JSON.parse(String(row.context_json)),
2117
+ tokenUsage: row.token_usage_json === null ? undefined : JSON.parse(String(row.token_usage_json)),
2118
+ tools: row.tools_json === null ? undefined : JSON.parse(String(row.tools_json)),
2119
+ updatedAt: String(row.updated_at),
2120
+ }));
2121
+ }
2122
+ function runProgressVersion(db, runId) {
2123
+ const row = db.prepare("SELECT progress_version, progress_updated_at FROM runs WHERE id = ?").get(runId);
2124
+ return {
2125
+ version: Number(row?.progress_version ?? 0),
2126
+ ...(row?.progress_updated_at ? { updatedAt: row.progress_updated_at } : {}),
2127
+ };
2128
+ }
2129
+ function withoutUndefined(input) {
2130
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
2131
+ }
2132
+ function nullableString(value) {
2133
+ return value === null || value === undefined ? undefined : String(value);
2134
+ }
2135
+ function nullableNumber(value) {
2136
+ return value === null || value === undefined ? undefined : Number(value);
2137
+ }
2138
+ function hookJournalEntryFromRow(row) {
2139
+ return {
2140
+ id: Number(row.id),
2141
+ runId: row.run_id,
2142
+ eventSequence: Number(row.event_sequence),
2143
+ triggerOrder: Number(row.trigger_order),
2144
+ event: row.event,
2145
+ source: row.source,
2146
+ sourcePath: row.source_path,
2147
+ handlerId: row.handler_id,
2148
+ definitionHash: row.definition_hash,
2149
+ ...(row.node_key === null ? {} : { nodeKey: row.node_key }),
2150
+ status: row.status,
2151
+ ...(row.exit_code === null ? {} : { exitCode: Number(row.exit_code) }),
2152
+ ...(row.stdout === null ? {} : { stdout: row.stdout }),
2153
+ ...(row.stderr === null ? {} : { stderr: row.stderr }),
2154
+ ...(row.duration_ms === null ? {} : { durationMs: Number(row.duration_ms) }),
2155
+ ...(row.error === null ? {} : { error: row.error }),
2156
+ triggeredAt: row.triggered_at,
2157
+ };
2158
+ }
2159
+ function isTerminalRunStatus(status) {
2160
+ return status === "completed" || status === "failed" || status === "canceled";
2161
+ }
2162
+ function parseOptionalJson(value) {
2163
+ return value === null || value === undefined ? undefined : JSON.parse(String(value));
2164
+ }
2165
+ function eventNodeKey(event) {
2166
+ const payload = event.payload;
2167
+ return typeof payload.nodeKey === "string" ? payload.nodeKey : null;
2168
+ }
2169
+ function attemptResultEvent(input, nodeKey) {
2170
+ if (input.result.status === "completed") {
2171
+ return {
2172
+ type: "attempt.completed",
2173
+ payload: {
2174
+ attemptId: input.attemptId,
2175
+ ...(input.result.output === undefined ? {} : { result: input.result.output }),
2176
+ },
2177
+ };
2178
+ }
2179
+ if (input.result.status === "timed_out") {
2180
+ return { type: "attempt.timed_out", payload: { attemptId: input.attemptId, error: { reason: input.result.reason, nodeKey } } };
2181
+ }
2182
+ if (input.result.status === "cancelled") {
2183
+ return { type: "attempt.cancelled", payload: { attemptId: input.attemptId, cancelReason: input.result.reason } };
2184
+ }
2185
+ return { type: "attempt.failed", payload: { attemptId: input.attemptId, error: { reason: input.result.reason, nodeKey }, terminalReason: input.result.reason } };
2186
+ }
2187
+ function instanceResultEvent(input, nodeKey, attemptEvent) {
2188
+ if (attemptEvent.type === "attempt.completed") {
2189
+ return {
2190
+ type: "instance.completed",
2191
+ payload: {
2192
+ nodeKey,
2193
+ acceptedAttemptId: input.attemptId,
2194
+ ...(input.result.status === "completed" && input.result.output !== undefined ? { output: input.result.output } : {}),
2195
+ },
2196
+ };
2197
+ }
2198
+ if (attemptEvent.type === "attempt.cancelled") {
2199
+ return { type: "instance.cancelled", payload: { nodeKey, cancelReason: attemptEvent.payload.cancelReason } };
2200
+ }
2201
+ const statusReason = attemptEvent.type === "attempt.timed_out" ? "timed_out" : attemptEvent.payload.terminalReason;
2202
+ return {
2203
+ type: "instance.failed",
2204
+ payload: {
2205
+ nodeKey,
2206
+ error: attemptEvent.payload.error ?? { reason: input.result.status === "failed" || input.result.status === "timed_out" ? input.result.reason : "attempt_failed" },
2207
+ ...(statusReason === undefined ? {} : { statusReason }),
2208
+ },
2209
+ };
2210
+ }
2211
+ function matchesAttemptStartInput(input, payload) {
2212
+ return payload.runId === input.runId
2213
+ && payload.nodeKey === input.nodeKey
2214
+ && payload.nodeId === input.nodeId
2215
+ && payload.ownerEpoch === input.ownerEpoch
2216
+ && (payload.deadlineAt ?? undefined) === input.deadlineAt;
2217
+ }
2218
+ function encodeSchedulerPayload(payload) {
2219
+ return stableJson({ schedulerEventVersion: 1, payload });
2220
+ }
2221
+ function schedulerCommitDigest(events) {
2222
+ return createHash("sha256").update(stableJson(events)).digest("hex");
2223
+ }
2224
+ function schedulerEventIdempotencyKey(runId, commitKey, index) {
2225
+ const digest = createHash("sha256").update(commitKey).digest("hex");
2226
+ return `scheduler-event:${runId}:${digest}:${index}`;
2227
+ }
2228
+ function derivedIdempotencyKey(idempotencyKey, suffix) {
2229
+ return `${idempotencyKey}:${suffix}`;
2230
+ }
2231
+ function openDatabase(path, readOnly = false) {
2232
+ const db = new DatabaseSync(path, { enableForeignKeyConstraints: true, readOnly });
2233
+ db.exec("PRAGMA foreign_keys = ON;");
2234
+ return db;
2235
+ }
2236
+ function migrate(db) {
2237
+ db.exec(`
2238
+ DROP TABLE IF EXISTS commands;
2239
+
2240
+ CREATE TABLE IF NOT EXISTS schema_migrations (
2241
+ version INTEGER PRIMARY KEY,
2242
+ applied_at TEXT NOT NULL
2243
+ );
2244
+
2245
+ CREATE TABLE IF NOT EXISTS daemon_lease (
2246
+ workspace_realpath TEXT PRIMARY KEY,
2247
+ generation INTEGER NOT NULL,
2248
+ pid INTEGER,
2249
+ heartbeat_at TEXT,
2250
+ idle_since_at TEXT,
2251
+ idle_stop_ms INTEGER,
2252
+ protocol_version INTEGER NOT NULL,
2253
+ package_version TEXT NOT NULL,
2254
+ node_version TEXT NOT NULL,
2255
+ exec_path TEXT NOT NULL,
2256
+ updated_at TEXT NOT NULL
2257
+ );
2258
+
2259
+ CREATE TABLE IF NOT EXISTS runs (
2260
+ id TEXT PRIMARY KEY,
2261
+ name TEXT NOT NULL,
2262
+ status TEXT NOT NULL,
2263
+ workflow_entry TEXT NOT NULL,
2264
+ ir_digest TEXT NOT NULL,
2265
+ source_graph_digest TEXT NOT NULL,
2266
+ created_at TEXT NOT NULL,
2267
+ updated_at TEXT NOT NULL,
2268
+ progress_version INTEGER NOT NULL DEFAULT 0,
2269
+ progress_updated_at TEXT
2270
+ );
2271
+
2272
+ CREATE TABLE IF NOT EXISTS run_inputs (
2273
+ run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
2274
+ workflow_ir_json TEXT NOT NULL,
2275
+ input_json TEXT NOT NULL,
2276
+ agent_overrides_json TEXT NOT NULL DEFAULT '{}',
2277
+ output_json TEXT,
2278
+ lock_json TEXT NOT NULL,
2279
+ package_lock_digest TEXT,
2280
+ run_dir TEXT NOT NULL,
2281
+ created_at TEXT NOT NULL
2282
+ );
2283
+
2284
+ CREATE TABLE IF NOT EXISTS run_events (
2285
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2286
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2287
+ sequence INTEGER NOT NULL,
2288
+ type TEXT NOT NULL,
2289
+ node_key TEXT,
2290
+ payload_json TEXT NOT NULL,
2291
+ created_at TEXT NOT NULL,
2292
+ idempotency_key TEXT NOT NULL UNIQUE,
2293
+ UNIQUE(run_id, sequence)
2294
+ );
2295
+
2296
+ CREATE TABLE IF NOT EXISTS scheduler_commits (
2297
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2298
+ idempotency_key TEXT NOT NULL,
2299
+ event_count INTEGER NOT NULL,
2300
+ event_digest TEXT NOT NULL,
2301
+ created_at TEXT NOT NULL,
2302
+ PRIMARY KEY (run_id, idempotency_key)
2303
+ );
2304
+
2305
+ CREATE TABLE IF NOT EXISTS node_states (
2306
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2307
+ node_key TEXT NOT NULL,
2308
+ node_id TEXT NOT NULL,
2309
+ status TEXT NOT NULL,
2310
+ output_json TEXT,
2311
+ error_json TEXT,
2312
+ attempt INTEGER NOT NULL DEFAULT 0,
2313
+ created_at TEXT NOT NULL,
2314
+ updated_at TEXT NOT NULL,
2315
+ PRIMARY KEY (run_id, node_key)
2316
+ );
2317
+
2318
+ CREATE TABLE IF NOT EXISTS run_leases (
2319
+ run_id TEXT PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
2320
+ owner_id TEXT NOT NULL,
2321
+ owner_epoch INTEGER NOT NULL,
2322
+ lease_expires_at TEXT NOT NULL,
2323
+ heartbeat_at TEXT NOT NULL,
2324
+ claimed_at TEXT NOT NULL,
2325
+ released_at TEXT,
2326
+ reason TEXT
2327
+ );
2328
+
2329
+ CREATE TABLE IF NOT EXISTS scheduler_frames (
2330
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2331
+ frame_key TEXT NOT NULL,
2332
+ parent_frame_key TEXT,
2333
+ node_key TEXT,
2334
+ node_id TEXT,
2335
+ frame_kind TEXT NOT NULL,
2336
+ status TEXT NOT NULL,
2337
+ strategy TEXT,
2338
+ terminal_reason TEXT,
2339
+ instance_path_json TEXT,
2340
+ scope_json TEXT NOT NULL,
2341
+ loop_json TEXT,
2342
+ result_json TEXT,
2343
+ error_json TEXT,
2344
+ created_at TEXT NOT NULL,
2345
+ updated_at TEXT NOT NULL,
2346
+ PRIMARY KEY (run_id, frame_key)
2347
+ );
2348
+
2349
+ CREATE TABLE IF NOT EXISTS node_instances (
2350
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2351
+ node_key TEXT NOT NULL,
2352
+ node_id TEXT NOT NULL,
2353
+ parent_frame_key TEXT,
2354
+ instance_path_json TEXT NOT NULL,
2355
+ status TEXT NOT NULL,
2356
+ status_reason TEXT,
2357
+ readiness_sequence INTEGER,
2358
+ output_json TEXT,
2359
+ error_json TEXT,
2360
+ accepted_attempt_id TEXT,
2361
+ created_at TEXT NOT NULL,
2362
+ updated_at TEXT NOT NULL,
2363
+ PRIMARY KEY (run_id, node_key)
2364
+ );
2365
+
2366
+ CREATE TABLE IF NOT EXISTS node_attempts (
2367
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2368
+ attempt_id TEXT PRIMARY KEY,
2369
+ node_key TEXT NOT NULL,
2370
+ node_id TEXT NOT NULL,
2371
+ attempt_no INTEGER NOT NULL,
2372
+ owner_id TEXT,
2373
+ owner_epoch INTEGER NOT NULL,
2374
+ status TEXT NOT NULL,
2375
+ deadline_at TEXT,
2376
+ started_at TEXT NOT NULL,
2377
+ finished_at TEXT,
2378
+ result_json TEXT,
2379
+ error_json TEXT,
2380
+ terminal_reason TEXT,
2381
+ cancel_reason TEXT,
2382
+ UNIQUE(run_id, node_key, attempt_no)
2383
+ );
2384
+
2385
+ CREATE TABLE IF NOT EXISTS group_members (
2386
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2387
+ group_key TEXT NOT NULL,
2388
+ member_key TEXT NOT NULL,
2389
+ member_kind TEXT NOT NULL,
2390
+ branch_id TEXT,
2391
+ item_key TEXT,
2392
+ item_index INTEGER,
2393
+ item_json TEXT,
2394
+ child_frame_key TEXT,
2395
+ status TEXT NOT NULL,
2396
+ readiness_sequence INTEGER NOT NULL,
2397
+ completion_sequence INTEGER,
2398
+ accepted_rank INTEGER,
2399
+ terminal_reason TEXT,
2400
+ output_json TEXT,
2401
+ error_json TEXT,
2402
+ created_at TEXT NOT NULL,
2403
+ updated_at TEXT NOT NULL,
2404
+ PRIMARY KEY (run_id, member_key)
2405
+ );
2406
+
2407
+ CREATE TABLE IF NOT EXISTS signal_waits (
2408
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2409
+ node_key TEXT NOT NULL,
2410
+ node_id TEXT NOT NULL,
2411
+ status TEXT NOT NULL,
2412
+ payload_json TEXT,
2413
+ payload_digest TEXT,
2414
+ command_idempotency_key TEXT,
2415
+ deadline_at TEXT,
2416
+ timeout_message TEXT,
2417
+ timeout_remaining_ms INTEGER,
2418
+ rendered_prompt TEXT,
2419
+ consumed_at TEXT,
2420
+ timed_out_at TEXT,
2421
+ terminal_reason TEXT,
2422
+ created_at TEXT,
2423
+ updated_at TEXT,
2424
+ PRIMARY KEY (run_id, node_key)
2425
+ );
2426
+
2427
+ CREATE TABLE IF NOT EXISTS execution_metadata (
2428
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2429
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2430
+ attempt_id TEXT,
2431
+ kind TEXT NOT NULL,
2432
+ metadata_json TEXT NOT NULL,
2433
+ created_at TEXT NOT NULL
2434
+ );
2435
+
2436
+ CREATE TABLE IF NOT EXISTS node_progress (
2437
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2438
+ node_key TEXT NOT NULL,
2439
+ node_id TEXT NOT NULL,
2440
+ attempt_id TEXT,
2441
+ attempt_no INTEGER,
2442
+ kind TEXT NOT NULL,
2443
+ status TEXT NOT NULL,
2444
+ message TEXT,
2445
+ output_tail TEXT,
2446
+ output_total_bytes INTEGER,
2447
+ output_truncated INTEGER,
2448
+ context_json TEXT,
2449
+ token_usage_json TEXT,
2450
+ tools_json TEXT,
2451
+ updated_at TEXT NOT NULL,
2452
+ PRIMARY KEY (run_id, node_key)
2453
+ );
2454
+
2455
+ CREATE TABLE IF NOT EXISTS hook_journal (
2456
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2457
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2458
+ event_sequence INTEGER NOT NULL,
2459
+ trigger_order INTEGER NOT NULL,
2460
+ event TEXT NOT NULL,
2461
+ source TEXT NOT NULL,
2462
+ source_path TEXT NOT NULL,
2463
+ handler_id TEXT NOT NULL,
2464
+ definition_hash TEXT NOT NULL,
2465
+ node_key TEXT,
2466
+ status TEXT NOT NULL CHECK (status IN ('completed', 'failed', 'timed_out')),
2467
+ exit_code INTEGER,
2468
+ stdout TEXT,
2469
+ stderr TEXT,
2470
+ duration_ms INTEGER,
2471
+ error TEXT,
2472
+ triggered_at TEXT NOT NULL
2473
+ );
2474
+
2475
+ CREATE INDEX IF NOT EXISTS idx_run_leases_expires ON run_leases(lease_expires_at);
2476
+ CREATE INDEX IF NOT EXISTS idx_scheduler_frames_parent_status ON scheduler_frames(run_id, parent_frame_key, status);
2477
+ CREATE INDEX IF NOT EXISTS idx_node_instances_node_status ON node_instances(run_id, node_id, status);
2478
+ CREATE INDEX IF NOT EXISTS idx_node_instances_frame_status ON node_instances(run_id, parent_frame_key, status);
2479
+ CREATE INDEX IF NOT EXISTS idx_node_attempts_owner_status ON node_attempts(run_id, owner_epoch, status);
2480
+ CREATE INDEX IF NOT EXISTS idx_node_attempts_deadline_status ON node_attempts(run_id, deadline_at, status);
2481
+ CREATE INDEX IF NOT EXISTS idx_group_members_ready ON group_members(run_id, group_key, readiness_sequence);
2482
+ CREATE INDEX IF NOT EXISTS idx_group_members_status ON group_members(run_id, group_key, status);
2483
+ CREATE INDEX IF NOT EXISTS idx_signal_waits_status ON signal_waits(run_id, node_key, status);
2484
+ CREATE INDEX IF NOT EXISTS idx_signal_waits_deadline_status ON signal_waits(run_id, deadline_at, status);
2485
+ CREATE INDEX IF NOT EXISTS idx_node_progress_run_updated ON node_progress(run_id, updated_at);
2486
+ CREATE INDEX IF NOT EXISTS idx_hook_journal_run_id ON hook_journal(run_id);
2487
+ CREATE INDEX IF NOT EXISTS idx_hook_journal_triggered_at ON hook_journal(triggered_at);
2488
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hook_journal_event_handler
2489
+ ON hook_journal(run_id, event_sequence, definition_hash);
2490
+
2491
+ CREATE TABLE IF NOT EXISTS artifacts (
2492
+ id TEXT PRIMARY KEY,
2493
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2494
+ node_key TEXT,
2495
+ attempt INTEGER,
2496
+ media_type TEXT,
2497
+ digest TEXT NOT NULL,
2498
+ size INTEGER NOT NULL,
2499
+ relative_path TEXT NOT NULL,
2500
+ created_at TEXT NOT NULL
2501
+ );
2502
+
2503
+ INSERT OR IGNORE INTO schema_migrations (version, applied_at)
2504
+ VALUES (1, datetime('now'));
2505
+ `);
2506
+ addColumnIfMissing(db, "scheduler_frames", "instance_path_json", "TEXT");
2507
+ addColumnIfMissing(db, "scheduler_frames", "loop_json", "TEXT");
2508
+ addColumnIfMissing(db, "node_instances", "instance_path_json", "TEXT NOT NULL DEFAULT '[]'");
2509
+ addColumnIfMissing(db, "group_members", "item_json", "TEXT");
2510
+ addColumnIfMissing(db, "group_members", "child_frame_key", "TEXT");
2511
+ addColumnIfMissing(db, "group_members", "completion_sequence", "INTEGER");
2512
+ addColumnIfMissing(db, "run_inputs", "agent_overrides_json", "TEXT NOT NULL DEFAULT '{}'");
2513
+ addColumnIfMissing(db, "daemon_lease", "idle_since_at", "TEXT");
2514
+ addColumnIfMissing(db, "daemon_lease", "idle_stop_ms", "INTEGER");
2515
+ addColumnIfMissing(db, "signal_waits", "rendered_prompt", "TEXT");
2516
+ addColumnIfMissing(db, "signal_waits", "timeout_message", "TEXT");
2517
+ addColumnIfMissing(db, "signal_waits", "timeout_remaining_ms", "INTEGER");
2518
+ addColumnIfMissing(db, "signal_waits", "created_at", "TEXT");
2519
+ addColumnIfMissing(db, "signal_waits", "updated_at", "TEXT");
2520
+ addColumnIfMissing(db, "hook_journal", "trigger_order", "INTEGER NOT NULL DEFAULT 0");
2521
+ addColumnIfMissing(db, "runs", "progress_version", "INTEGER NOT NULL DEFAULT 0");
2522
+ addColumnIfMissing(db, "runs", "progress_updated_at", "TEXT");
2523
+ db.exec("UPDATE signal_waits SET created_at = COALESCE(created_at, datetime('now')), updated_at = COALESCE(updated_at, datetime('now')) WHERE created_at IS NULL OR updated_at IS NULL;");
2524
+ }
2525
+ function storeNeedsMigration(db) {
2526
+ return !hasTable(db, "schema_migrations")
2527
+ || db.prepare("SELECT 1 FROM schema_migrations WHERE version = 1 LIMIT 1").get() === undefined
2528
+ || !hasColumn(db, "runs", "progress_version")
2529
+ || !hasColumn(db, "runs", "progress_updated_at")
2530
+ || !hasTable(db, "node_progress");
2531
+ }
2532
+ function hasTable(db, table) {
2533
+ const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").all(table);
2534
+ return rows.length > 0;
2535
+ }
2536
+ function addColumnIfMissing(db, table, column, definition) {
2537
+ if (!hasColumn(db, table, column))
2538
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
2539
+ }
2540
+ function hasColumn(db, table, column) {
2541
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
2542
+ return rows.some(row => row.name === column);
2543
+ }
2544
+ function parseAgentOverrides(json) {
2545
+ if (!json)
2546
+ return {};
2547
+ return parseAgentOverrideMap(JSON.parse(json));
2548
+ }
2549
+ export function validateAgentOverrides(ir, input) {
2550
+ return normalizeAgentOverrides(ir, input);
2551
+ }
2552
+ function normalizeAgentOverrides(ir, input, inherited = {}) {
2553
+ const base = Object.fromEntries(Object.entries(inherited).filter(([name]) => ir.agents[name]));
2554
+ if (input === undefined)
2555
+ return base;
2556
+ const incoming = parseAgentOverrideMap(input, ir.agents);
2557
+ const merged = Object.fromEntries(Object.entries(incoming).map(([name, override]) => {
2558
+ const previous = base[name] ?? {};
2559
+ const declared = ir.agents[name];
2560
+ return [name, mergeAgentOverride(declared, previous, override)];
2561
+ }));
2562
+ return { ...base, ...merged };
2563
+ }
2564
+ function mergeAgentOverride(declared, previous, incoming) {
2565
+ const before = agentIdentity(declared, previous);
2566
+ const after = agentIdentity(declared, { ...previous, ...incoming });
2567
+ const changedIdentity = incoming.use !== undefined || incoming.command !== undefined
2568
+ ? before.kind !== after.kind || before.value !== after.value
2569
+ : false;
2570
+ const merged = changedIdentity
2571
+ ? { ...previous, model: undefined, agentMode: undefined, ...incoming }
2572
+ : { ...previous, ...incoming };
2573
+ if (incoming.use !== undefined)
2574
+ delete merged.command;
2575
+ if (incoming.command !== undefined)
2576
+ delete merged.use;
2577
+ return compactUndefined(merged);
2578
+ }
2579
+ function withAgentOverrides(ir, overrides) {
2580
+ if (Object.keys(overrides).length === 0)
2581
+ return ir;
2582
+ return {
2583
+ ...ir,
2584
+ agents: Object.fromEntries(Object.entries(ir.agents).map(([name, definition]) => [
2585
+ name,
2586
+ applyAgentOverride(definition, overrides[name]),
2587
+ ])),
2588
+ };
2589
+ }
2590
+ function applyAgentOverride(definition, override) {
2591
+ if (!override)
2592
+ return definition;
2593
+ const identityChanged = override.use !== undefined || override.command !== undefined
2594
+ ? agentIdentity(definition, {}).kind !== agentIdentity(definition, override).kind
2595
+ || agentIdentity(definition, {}).value !== agentIdentity(definition, override).value
2596
+ : false;
2597
+ const shared = {
2598
+ model: override.model ?? (identityChanged ? undefined : definition.model),
2599
+ permissionMode: override.permissionMode ?? definition.permissionMode,
2600
+ agentMode: override.agentMode ?? (identityChanged ? undefined : definition.agentMode),
2601
+ cwd: override.cwd === undefined ? definition.cwd : valueToExprIR(override.cwd),
2602
+ env: override.env === undefined ? definition.env : Object.fromEntries(Object.entries(override.env).map(([key, value]) => [key, valueToExprIR(value)])),
2603
+ };
2604
+ if (override.command !== undefined)
2605
+ return compactUndefined({ kind: "agent_command", command: override.command, ...shared });
2606
+ if (override.use !== undefined)
2607
+ return compactUndefined({ kind: "agent_definition", use: override.use, ...shared });
2608
+ return compactUndefined({ ...definition, ...shared });
2609
+ }
2610
+ function agentIdentity(definition, override) {
2611
+ if (override.command !== undefined)
2612
+ return { kind: "command", value: override.command };
2613
+ if (override.use !== undefined)
2614
+ return { kind: "use", value: override.use };
2615
+ return definition.kind === "agent_command"
2616
+ ? { kind: "command", value: definition.command }
2617
+ : { kind: "use", value: definition.use };
2618
+ }
2619
+ function toRunRecord(row) {
2620
+ return withoutUndefined({
2621
+ id: String(row.id),
2622
+ name: String(row.name),
2623
+ status: String(row.status),
2624
+ workflowEntry: String(row.workflow_entry),
2625
+ irDigest: String(row.ir_digest),
2626
+ sourceGraphDigest: String(row.source_graph_digest),
2627
+ createdAt: String(row.created_at),
2628
+ updatedAt: String(row.updated_at),
2629
+ progressVersion: Number(row.progress_version ?? 0),
2630
+ progressUpdatedAt: nullableString(row.progress_updated_at),
2631
+ });
2632
+ }
2633
+ function collectNodeIds(scope) {
2634
+ const ids = [];
2635
+ for (const node of scope.nodes) {
2636
+ ids.push(node.id);
2637
+ switch (node.kind) {
2638
+ case "if":
2639
+ ids.push(...collectNodeIds(node.then));
2640
+ if (node.else)
2641
+ ids.push(...collectNodeIds(node.else));
2642
+ break;
2643
+ case "switch":
2644
+ for (const c of node.cases)
2645
+ ids.push(...collectNodeIds(c.then));
2646
+ if (node.default)
2647
+ ids.push(...collectNodeIds(node.default));
2648
+ break;
2649
+ case "parallel":
2650
+ for (const branch of Object.values(node.branches))
2651
+ ids.push(...collectNodeIds(branch.scope));
2652
+ break;
2653
+ case "fanout":
2654
+ ids.push(...collectNodeIds(node.do));
2655
+ break;
2656
+ case "loop":
2657
+ ids.push(...collectNodeIds(node.do));
2658
+ break;
2659
+ }
2660
+ }
2661
+ return ids;
2662
+ }
2663
+ function isProcessAlive(pid) {
2664
+ try {
2665
+ process.kill(pid, 0);
2666
+ return true;
2667
+ }
2668
+ catch (error) {
2669
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
2670
+ }
2671
+ }
2672
+ function inheritableCompletedNodeKeys(ir, completed) {
2673
+ const ancestors = nodeAncestors(ir.root);
2674
+ return new Set([...completed].filter(nodeKey => (ancestors.get(nodeKey) ?? []).every(parent => completed.has(parent))));
2675
+ }
2676
+ function nodeAncestors(scope, parents = []) {
2677
+ const out = new Map();
2678
+ for (const node of scope.nodes) {
2679
+ out.set(node.id, parents);
2680
+ const childParents = [...parents, node.id];
2681
+ switch (node.kind) {
2682
+ case "if":
2683
+ mergeAncestors(out, nodeAncestors(node.then, childParents));
2684
+ if (node.else)
2685
+ mergeAncestors(out, nodeAncestors(node.else, childParents));
2686
+ break;
2687
+ case "switch":
2688
+ for (const c of node.cases)
2689
+ mergeAncestors(out, nodeAncestors(c.then, childParents));
2690
+ if (node.default)
2691
+ mergeAncestors(out, nodeAncestors(node.default, childParents));
2692
+ break;
2693
+ case "parallel":
2694
+ for (const branch of Object.values(node.branches))
2695
+ mergeAncestors(out, nodeAncestors(branch.scope, childParents));
2696
+ break;
2697
+ case "fanout":
2698
+ case "loop":
2699
+ mergeAncestors(out, nodeAncestors(node.do, childParents));
2700
+ break;
2701
+ }
2702
+ }
2703
+ return out;
2704
+ }
2705
+ function nodeSignatures(scope) {
2706
+ const out = new Map();
2707
+ for (const node of scope.nodes) {
2708
+ out.set(node.id, stableJson(node));
2709
+ switch (node.kind) {
2710
+ case "if":
2711
+ mergeSignatures(out, nodeSignatures(node.then));
2712
+ if (node.else)
2713
+ mergeSignatures(out, nodeSignatures(node.else));
2714
+ break;
2715
+ case "switch":
2716
+ for (const c of node.cases)
2717
+ mergeSignatures(out, nodeSignatures(c.then));
2718
+ if (node.default)
2719
+ mergeSignatures(out, nodeSignatures(node.default));
2720
+ break;
2721
+ case "parallel":
2722
+ for (const branch of Object.values(node.branches))
2723
+ mergeSignatures(out, nodeSignatures(branch.scope));
2724
+ break;
2725
+ case "fanout":
2726
+ case "loop":
2727
+ mergeSignatures(out, nodeSignatures(node.do));
2728
+ break;
2729
+ }
2730
+ }
2731
+ return out;
2732
+ }
2733
+ function mergeSignatures(target, source) {
2734
+ for (const [key, value] of source)
2735
+ target.set(key, value);
2736
+ }
2737
+ function mergeAncestors(target, source) {
2738
+ for (const [key, value] of source)
2739
+ target.set(key, value);
2740
+ }
2741
+ function stableJson(value) {
2742
+ return `${JSON.stringify(sortJson(value))}\n`;
2743
+ }
2744
+ function assertJsonValue(value, path) {
2745
+ if (!isJsonValue(value))
2746
+ throw new Error(`${path} is not JSON-serializable.`);
2747
+ return value;
2748
+ }
2749
+ function isJsonValue(value) {
2750
+ if (value === null)
2751
+ return true;
2752
+ if (typeof value === "number")
2753
+ return Number.isFinite(value);
2754
+ if (typeof value === "string" || typeof value === "boolean")
2755
+ return true;
2756
+ if (Array.isArray(value))
2757
+ return value.every(isJsonValue);
2758
+ if (!value || typeof value !== "object")
2759
+ return false;
2760
+ return Object.values(value).every(isJsonValue);
2761
+ }
2762
+ function sortJson(value) {
2763
+ if (!value || typeof value !== "object")
2764
+ return value;
2765
+ if (Array.isArray(value))
2766
+ return value.map(sortJson);
2767
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
2768
+ }
2769
+ function evaluateRecordedOutputs(outputs, nodes, input, meta) {
2770
+ return assertJsonValue(Object.fromEntries(Object.entries(outputs).map(([key, expr]) => [
2771
+ key,
2772
+ evaluateExpr(expr, {
2773
+ input,
2774
+ meta,
2775
+ nodes: Object.fromEntries(Object.entries(nodes).map(([nodeKey, output]) => [nodeKey, { status: "completed", output }])),
2776
+ }),
2777
+ ])), "fork output");
2778
+ }
2779
+ function forkCompletedOutputJson(args) {
2780
+ const nodes = completedOutputMap(args.completedOutputRows
2781
+ .filter(row => args.inheritableNodeKeys.has(row.nodeKey))
2782
+ .map(row => ({
2783
+ ...row,
2784
+ output: rewriteArtifactValue(assertJsonValue(row.output, `fork node '${row.nodeKey}' output`), args.sourceRunId, args.forkRunId, args.artifactIdMap),
2785
+ })));
2786
+ const output = evaluateRecordedOutputs(args.outputs, nodes, JSON.parse(args.inputJson), args.meta);
2787
+ return rewriteArtifactRefs(stableJson(output), args.sourceRunId, args.forkRunId, args.artifactIdMap);
2788
+ }
2789
+ function completedSchedulerOutputRows(db, runId) {
2790
+ const nodeRows = db.prepare(`
2791
+ SELECT node_key, node_id, output_json
2792
+ FROM node_states
2793
+ WHERE run_id = ? AND status = 'completed' AND output_json IS NOT NULL
2794
+ `).all(runId);
2795
+ const frameRows = db.prepare(`
2796
+ SELECT frame_key, node_id, result_json
2797
+ FROM scheduler_frames
2798
+ WHERE run_id = ?
2799
+ AND status = 'completed'
2800
+ AND frame_kind IN ('node', 'loop')
2801
+ AND node_id IS NOT NULL
2802
+ AND result_json IS NOT NULL
2803
+ `).all(runId);
2804
+ return [
2805
+ ...nodeRows.map(row => ({
2806
+ nodeKey: String(row.node_key),
2807
+ nodeId: String(row.node_id),
2808
+ output: JSON.parse(String(row.output_json)),
2809
+ })),
2810
+ ...frameRows.map(row => ({
2811
+ nodeKey: String(row.frame_key),
2812
+ nodeId: String(row.node_id),
2813
+ output: JSON.parse(String(row.result_json)),
2814
+ })),
2815
+ ];
2816
+ }
2817
+ function completedOutputMap(rows) {
2818
+ const byKey = Object.fromEntries(rows.map(row => [row.nodeKey, row.output]));
2819
+ const byId = new Map();
2820
+ for (const row of rows)
2821
+ byId.set(row.nodeId, [...byId.get(row.nodeId) ?? [], { nodeKey: row.nodeKey, output: row.output }]);
2822
+ for (const [nodeId, matches] of byId) {
2823
+ if (matches.length === 1 && byKey[nodeId] === undefined)
2824
+ byKey[nodeId] = matches[0].output;
2825
+ }
2826
+ return byKey;
2827
+ }
2828
+ function rewriteArtifactRefs(json, sourceRunId, forkRunId, artifactIds) {
2829
+ const value = JSON.parse(json);
2830
+ return stableJson(rewriteArtifactValue(value, sourceRunId, forkRunId, artifactIds));
2831
+ }
2832
+ function requireArtifactId(map, sourceId) {
2833
+ const id = map[sourceId];
2834
+ if (!id)
2835
+ throw new Error(`Missing fork artifact id for '${sourceId}'.`);
2836
+ return id;
2837
+ }
2838
+ function reachableInheritedArtifactIds(args) {
2839
+ const ids = new Set();
2840
+ if (args.outputJson)
2841
+ collectArtifactIds(JSON.parse(args.outputJson), args.runId, ids);
2842
+ for (const row of args.nodeRows) {
2843
+ if (!args.inheritableNodeKeys.has(String(row.node_key)) || !row.output_json)
2844
+ continue;
2845
+ collectArtifactIds(JSON.parse(String(row.output_json)), args.runId, ids);
2846
+ }
2847
+ return ids;
2848
+ }
2849
+ function collectArtifactIds(value, runId, out) {
2850
+ if (Array.isArray(value)) {
2851
+ for (const item of value)
2852
+ collectArtifactIds(item, runId, out);
2853
+ return;
2854
+ }
2855
+ if (!value || typeof value !== "object")
2856
+ return;
2857
+ const uri = value.uri;
2858
+ const prefix = `artifact://${runId}/`;
2859
+ if (typeof uri === "string" && uri.startsWith(prefix))
2860
+ out.add(uri.slice(prefix.length));
2861
+ for (const item of Object.values(value))
2862
+ collectArtifactIds(item, runId, out);
2863
+ }
2864
+ async function verifyCopiedArtifacts(runDir, artifacts) {
2865
+ for (const artifact of artifacts) {
2866
+ const relativePath = String(artifact.relative_path);
2867
+ let bytes;
2868
+ try {
2869
+ bytes = await readContainedFile(runDir, relativePath);
2870
+ }
2871
+ catch (error) {
2872
+ if (error instanceof PathEscapeError)
2873
+ throw new Error(`Fork artifact '${String(artifact.id)}' has invalid relative path.`);
2874
+ throw error;
2875
+ }
2876
+ const expectedSize = Number(artifact.size);
2877
+ const expectedDigest = String(artifact.digest);
2878
+ const actualDigest = digest(bytes);
2879
+ if (bytes.byteLength !== expectedSize || actualDigest !== expectedDigest) {
2880
+ throw new Error(`Fork artifact '${String(artifact.id)}' failed copy verification.`);
2881
+ }
2882
+ }
2883
+ }
2884
+ async function pruneNonInheritedArtifacts(runDir, artifacts) {
2885
+ const artifactDir = join(runDir, "artifacts");
2886
+ const keep = new Set(artifacts.map(artifact => String(artifact.relative_path)));
2887
+ let entries;
2888
+ try {
2889
+ entries = await readdir(artifactDir);
2890
+ }
2891
+ catch {
2892
+ return;
2893
+ }
2894
+ await Promise.all(entries.map(entry => pruneArtifactEntry(runDir, join("artifacts", entry), keep)));
2895
+ }
2896
+ async function pruneArtifactEntry(runDir, relativePath, keep) {
2897
+ const absolutePath = join(runDir, relativePath);
2898
+ const info = await lstat(absolutePath);
2899
+ if (!info.isDirectory()) {
2900
+ if (keep.has(relativePath))
2901
+ return false;
2902
+ await rm(absolutePath, { force: true });
2903
+ return true;
2904
+ }
2905
+ const children = await readdir(absolutePath);
2906
+ const removed = await Promise.all(children.map(child => pruneArtifactEntry(runDir, join(relativePath, child), keep)));
2907
+ if (removed.every(Boolean)) {
2908
+ await rm(absolutePath, { recursive: true, force: true });
2909
+ return true;
2910
+ }
2911
+ return false;
2912
+ }
2913
+ async function writePreparedRunFiles(runDir, prepared) {
2914
+ await writeFile(join(runDir, "workflow.ir.json"), prepared.irJson);
2915
+ await writeFile(join(runDir, "lock.json"), `${JSON.stringify(prepared.lock, null, 2)}\n`);
2916
+ }
2917
+ async function verifyFrozenRunFiles(runDir, irDigest, lockJson, workflowIrJson) {
2918
+ const irBytes = await readContainedFile(runDir, "workflow.ir.json");
2919
+ if (digest(irBytes) !== irDigest)
2920
+ throw new Error("Fork workflow.ir.json failed copy verification.");
2921
+ if (stableJson(JSON.parse(irBytes.toString("utf8"))) !== stableJson(JSON.parse(workflowIrJson)))
2922
+ throw new Error("Fork workflow.ir.json does not match frozen runtime state.");
2923
+ const lockBytes = await readContainedFile(runDir, "lock.json");
2924
+ if (stableJson(JSON.parse(lockBytes.toString("utf8"))) !== stableJson(JSON.parse(lockJson)))
2925
+ throw new Error("Fork lock.json failed copy verification.");
2926
+ }
2927
+ async function readContainedFile(root, relativePath) {
2928
+ const rootPath = resolve(root);
2929
+ const absolutePath = resolve(rootPath, relativePath);
2930
+ if (absolutePath !== rootPath && !absolutePath.startsWith(`${rootPath}/`))
2931
+ throw new PathEscapeError(`Path '${relativePath}' escapes run directory.`);
2932
+ const info = await lstat(absolutePath);
2933
+ if (info.isSymbolicLink())
2934
+ throw new PathEscapeError(`Path '${relativePath}' is a symbolic link.`);
2935
+ if (!info.isFile())
2936
+ throw new PathEscapeError(`Path '${relativePath}' is not a file.`);
2937
+ const real = await realpath(absolutePath);
2938
+ const realRoot = await realpath(rootPath);
2939
+ if (!real.startsWith(`${realRoot}/`))
2940
+ throw new PathEscapeError(`Path '${relativePath}' escapes run directory.`);
2941
+ return readFile(absolutePath);
2942
+ }
2943
+ function forkRequestFingerprint(runId, options) {
2944
+ return stableJson({
2945
+ runId,
2946
+ ...(options.prepared === undefined ? {} : {
2947
+ prepared: {
2948
+ workflowPath: options.prepared.workflowPath,
2949
+ irDigest: options.prepared.irDigest,
2950
+ sourceGraphDigest: options.prepared.sourceGraphDigest,
2951
+ ...(options.prepared.packageLockDigest === undefined ? {} : { packageLockDigest: options.prepared.packageLockDigest }),
2952
+ },
2953
+ }),
2954
+ ...(options.input === undefined ? {} : { input: options.input }),
2955
+ ...(options.agentOverrides === undefined ? {} : { agentOverrides: options.agentOverrides }),
2956
+ ...(options.target === undefined ? {} : { target: options.target }),
2957
+ ...(options.unsafeReuse === true ? { unsafeReuse: true } : {}),
2958
+ });
2959
+ }
2960
+ class PathEscapeError extends Error {
2961
+ }
2962
+ function containedRunDir(cwd, runDir) {
2963
+ const runsRoot = resolve(cwd, localStateRoot, "runs");
2964
+ const absolute = resolve(cwd, runDir);
2965
+ const name = absolute.slice(runsRoot.length + 1);
2966
+ if (!absolute.startsWith(`${runsRoot}/`) || !runIdPattern.test(name) || name.includes("/")) {
2967
+ throw new Error(`Run directory '${runDir}' is outside ${localStateRoot}/runs.`);
2968
+ }
2969
+ return absolute;
2970
+ }
2971
+ async function isStalePath(path, olderThanMs) {
2972
+ try {
2973
+ const info = await stat(path);
2974
+ return Date.now() - info.mtimeMs >= olderThanMs;
2975
+ }
2976
+ catch {
2977
+ return false;
2978
+ }
2979
+ }
2980
+ function summarizeWorkflowForEvent(ir) {
2981
+ return {
2982
+ name: ir.name,
2983
+ ...(ir.description === undefined ? {} : { description: ir.description }),
2984
+ irVersion: ir.irVersion,
2985
+ nodeCount: countNodes(ir.root),
2986
+ outputKeys: Object.keys(ir.outputs).sort(),
2987
+ diagnostics: {
2988
+ total: ir.diagnostics.length,
2989
+ errors: ir.diagnostics.filter(diagnostic => diagnostic.severity === "error").length,
2990
+ warnings: ir.diagnostics.filter(diagnostic => diagnostic.severity === "warning").length,
2991
+ infos: ir.diagnostics.filter(diagnostic => diagnostic.severity === "info").length,
2992
+ },
2993
+ };
2994
+ }
2995
+ function countNodes(scope) {
2996
+ let total = scope.nodes.length;
2997
+ for (const node of scope.nodes) {
2998
+ if (node.kind === "if") {
2999
+ total += countNodes(node.then);
3000
+ if (node.else)
3001
+ total += countNodes(node.else);
3002
+ }
3003
+ else if (node.kind === "switch") {
3004
+ for (const c of node.cases)
3005
+ total += countNodes(c.then);
3006
+ if (node.default)
3007
+ total += countNodes(node.default);
3008
+ }
3009
+ else if (node.kind === "parallel") {
3010
+ for (const branch of Object.values(node.branches))
3011
+ total += countNodes(branch.scope);
3012
+ }
3013
+ else if (node.kind === "fanout") {
3014
+ total += countNodes(node.do);
3015
+ }
3016
+ else if (node.kind === "loop") {
3017
+ total += countNodes(node.do);
3018
+ }
3019
+ }
3020
+ return total;
3021
+ }
3022
+ function rootScope(frozen) {
3023
+ return {
3024
+ input: frozen.input,
3025
+ nodes: {},
3026
+ meta: frozen.meta,
3027
+ fanout: {},
3028
+ loop: {},
3029
+ };
3030
+ }
3031
+ function throwSchedulerStoreError(error) {
3032
+ throw new SchedulerStoreException(error);
3033
+ }
3034
+ function digest(bytes) {
3035
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
3036
+ }
3037
+ function newRunId() {
3038
+ return `${localTimestampId(new Date())}${randomBytes(10).toString("hex").toUpperCase()}`;
3039
+ }
3040
+ function localTimestampId(date) {
3041
+ return [
3042
+ date.getFullYear(),
3043
+ pad2(date.getMonth() + 1),
3044
+ pad2(date.getDate()),
3045
+ pad2(date.getHours()),
3046
+ pad2(date.getMinutes()),
3047
+ pad2(date.getSeconds()),
3048
+ ].join("");
3049
+ }
3050
+ function pad2(value) {
3051
+ return String(value).padStart(2, "0");
3052
+ }
3053
+ //# sourceMappingURL=store.js.map