@osolmaz/pi-workflows 0.13.4 → 0.15.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 (237) hide show
  1. package/README.md +146 -160
  2. package/dist/builtins/autoimplement.workflow.js +3 -1
  3. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  4. package/dist/builtins/change-verification.workflow.js +7 -2
  5. package/dist/builtins/change-verification.workflow.js.map +1 -1
  6. package/dist/builtins/metadata.d.ts +22 -0
  7. package/dist/builtins/metadata.js +10 -0
  8. package/dist/builtins/metadata.js.map +1 -0
  9. package/dist/builtins/monitor.workflow.js +4 -1
  10. package/dist/builtins/monitor.workflow.js.map +1 -1
  11. package/dist/builtins/sanity-check.workflow.js +4 -1
  12. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  13. package/dist/builtins/workspace-preparation.workflow.js +3 -1
  14. package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
  15. package/dist/controllers/index.d.ts +1 -2
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +85 -31
  19. package/dist/controllers/sqlite.js +541 -126
  20. package/dist/controllers/sqlite.js.map +1 -1
  21. package/dist/controllers/store.d.ts +1 -0
  22. package/dist/extension/controller-command.d.ts +22 -0
  23. package/dist/extension/controller-command.js +32 -0
  24. package/dist/extension/controller-command.js.map +1 -0
  25. package/dist/extension/index.d.ts +2 -9
  26. package/dist/extension/index.js +876 -2928
  27. package/dist/extension/index.js.map +1 -1
  28. package/dist/extension/message-card.d.ts +13 -0
  29. package/dist/extension/message-card.js +46 -0
  30. package/dist/extension/message-card.js.map +1 -0
  31. package/dist/extension/step-message.d.ts +5 -6
  32. package/dist/extension/step-message.js +41 -52
  33. package/dist/extension/step-message.js.map +1 -1
  34. package/dist/host/child-worker-supervisor.d.ts +50 -0
  35. package/dist/host/child-worker-supervisor.js +159 -0
  36. package/dist/host/child-worker-supervisor.js.map +1 -0
  37. package/dist/host/client.d.ts +48 -0
  38. package/dist/host/client.js +216 -0
  39. package/dist/host/client.js.map +1 -0
  40. package/dist/host/controller-worker-entry.d.ts +2 -0
  41. package/dist/host/controller-worker-entry.js +244 -0
  42. package/dist/host/controller-worker-entry.js.map +1 -0
  43. package/dist/host/controller-worker-protocol.d.ts +36 -0
  44. package/dist/host/controller-worker-protocol.js +49 -0
  45. package/dist/host/controller-worker-protocol.js.map +1 -0
  46. package/dist/host/controller-worker-supervisor.d.ts +21 -0
  47. package/dist/host/controller-worker-supervisor.js +54 -0
  48. package/dist/host/controller-worker-supervisor.js.map +1 -0
  49. package/dist/host/host-entry.d.ts +2 -0
  50. package/dist/host/host-entry.js +23 -0
  51. package/dist/host/host-entry.js.map +1 -0
  52. package/dist/host/processes.d.ts +17 -12
  53. package/dist/host/processes.js +154 -50
  54. package/dist/host/processes.js.map +1 -1
  55. package/dist/host/protocol.d.ts +38 -0
  56. package/dist/host/protocol.js +156 -0
  57. package/dist/host/protocol.js.map +1 -0
  58. package/dist/host/resolver-entry.d.ts +49 -0
  59. package/dist/host/resolver-entry.js +149 -0
  60. package/dist/host/resolver-entry.js.map +1 -0
  61. package/dist/host/rpc-executor.d.ts +14 -3
  62. package/dist/host/rpc-executor.js +63 -30
  63. package/dist/host/rpc-executor.js.map +1 -1
  64. package/dist/host/runner.d.ts +95 -29
  65. package/dist/host/runner.js +2311 -348
  66. package/dist/host/runner.js.map +1 -1
  67. package/dist/host/state.d.ts +174 -0
  68. package/dist/host/state.js +689 -0
  69. package/dist/host/state.js.map +1 -0
  70. package/dist/host/worker-entry.d.ts +10 -0
  71. package/dist/host/worker-entry.js +446 -0
  72. package/dist/host/worker-entry.js.map +1 -0
  73. package/dist/host/worker-protocol.d.ts +31 -0
  74. package/dist/host/worker-protocol.js +122 -0
  75. package/dist/host/worker-protocol.js.map +1 -0
  76. package/dist/host/worker-store.d.ts +80 -0
  77. package/dist/host/worker-store.js +91 -0
  78. package/dist/host/worker-store.js.map +1 -0
  79. package/dist/host/worker-supervisor.d.ts +22 -0
  80. package/dist/host/worker-supervisor.js +54 -0
  81. package/dist/host/worker-supervisor.js.map +1 -0
  82. package/dist/render/canvas.d.ts +2 -1
  83. package/dist/render/canvas.js +40 -15
  84. package/dist/render/canvas.js.map +1 -1
  85. package/dist/render/graph-render.d.ts +7 -3
  86. package/dist/render/graph-render.js +125 -74
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.js +3 -1
  89. package/dist/state/database.js.map +1 -1
  90. package/dist/state/index.d.ts +1 -0
  91. package/dist/state/index.js +1 -0
  92. package/dist/state/index.js.map +1 -1
  93. package/dist/state/prune.js +36 -10
  94. package/dist/state/prune.js.map +1 -1
  95. package/dist/state/schema.d.ts +1 -1
  96. package/dist/state/schema.js +171 -3
  97. package/dist/state/schema.js.map +1 -1
  98. package/dist/state/viewer.d.ts +46 -0
  99. package/dist/state/viewer.js +249 -0
  100. package/dist/state/viewer.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +1 -1
  102. package/dist/viewer/cli.js +47 -18
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/workflows/composition.js +25 -2
  105. package/dist/workflows/composition.js.map +1 -1
  106. package/dist/workflows/definition.d.ts +3 -1
  107. package/dist/workflows/definition.js +25 -0
  108. package/dist/workflows/definition.js.map +1 -1
  109. package/dist/workflows/engine.d.ts +1 -0
  110. package/dist/workflows/engine.js +157 -42
  111. package/dist/workflows/engine.js.map +1 -1
  112. package/dist/workflows/errors.d.ts +3 -1
  113. package/dist/workflows/errors.js +4 -7
  114. package/dist/workflows/errors.js.map +1 -1
  115. package/dist/workflows/human-decision.d.ts +3 -0
  116. package/dist/workflows/human-decision.js +31 -0
  117. package/dist/workflows/human-decision.js.map +1 -1
  118. package/dist/workflows/index.d.ts +1 -1
  119. package/dist/workflows/index.js +1 -1
  120. package/dist/workflows/index.js.map +1 -1
  121. package/dist/workflows/schema.js +17 -6
  122. package/dist/workflows/schema.js.map +1 -1
  123. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  124. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  125. package/dist/workflows/session-reducer.js.map +1 -0
  126. package/dist/workflows/store.d.ts +87 -1
  127. package/dist/workflows/store.js +892 -93
  128. package/dist/workflows/store.js.map +1 -1
  129. package/dist/workflows/tool-input.d.ts +0 -22
  130. package/dist/workflows/tool-input.js +1 -44
  131. package/dist/workflows/tool-input.js.map +1 -1
  132. package/dist/workflows/types.d.ts +36 -4
  133. package/docs/2026-08-25-workflow-follow-ups.md +8 -6
  134. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  135. package/docs/CONTROLLERS.md +11 -11
  136. package/docs/DEFERRED_TURNS.md +61 -27
  137. package/docs/HUMAN_DECISIONS.md +12 -4
  138. package/docs/SQLITE_STATE.md +42 -8
  139. package/docs/WORKFLOW_HOST.md +452 -0
  140. package/docs/development.md +46 -30
  141. package/docs/live-replay-protocol.md +129 -100
  142. package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
  143. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
  144. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  145. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  146. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  147. package/docs/tui-viewer.md +18 -3
  148. package/docs/workflows.md +164 -156
  149. package/examples/workflows/command-batch.workflow.ts +2 -0
  150. package/examples/workflows/shell.workflow.ts +2 -1
  151. package/herdr-plugin.toml +1 -1
  152. package/package.json +1 -1
  153. package/skills/autodoc/SKILL.md +1 -1
  154. package/skills/autoimplement/SKILL.md +1 -1
  155. package/skills/autoplan/SKILL.md +1 -1
  156. package/skills/pi-workflows/SKILL.md +2 -0
  157. package/src/builtins/autoimplement.workflow.ts +3 -0
  158. package/src/builtins/change-verification.workflow.ts +7 -2
  159. package/src/builtins/metadata.ts +9 -0
  160. package/src/builtins/monitor.workflow.ts +4 -0
  161. package/src/builtins/sanity-check.workflow.ts +4 -0
  162. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  163. package/src/controllers/index.ts +3 -5
  164. package/src/controllers/sqlite.ts +929 -225
  165. package/src/controllers/store.ts +1 -0
  166. package/src/extension/controller-command.ts +45 -0
  167. package/src/extension/index.ts +948 -3506
  168. package/src/extension/message-card.ts +61 -0
  169. package/src/extension/step-message.ts +58 -63
  170. package/src/host/child-worker-supervisor.ts +183 -0
  171. package/src/host/client.ts +293 -0
  172. package/src/host/controller-worker-entry.ts +311 -0
  173. package/src/host/controller-worker-protocol.ts +104 -0
  174. package/src/host/controller-worker-supervisor.ts +79 -0
  175. package/src/host/host-entry.ts +23 -0
  176. package/src/host/processes.ts +171 -54
  177. package/src/host/protocol.ts +196 -0
  178. package/src/host/resolver-entry.ts +241 -0
  179. package/src/host/rpc-executor.ts +76 -34
  180. package/src/host/runner.ts +2813 -422
  181. package/src/host/state.ts +1160 -0
  182. package/src/host/worker-entry.ts +533 -0
  183. package/src/host/worker-protocol.ts +165 -0
  184. package/src/host/worker-store.ts +229 -0
  185. package/src/host/worker-supervisor.ts +74 -0
  186. package/src/render/canvas.ts +44 -10
  187. package/src/render/graph-render.ts +145 -90
  188. package/src/state/database.ts +2 -1
  189. package/src/state/index.ts +14 -0
  190. package/src/state/prune.ts +35 -9
  191. package/src/state/schema.ts +171 -3
  192. package/src/state/viewer.ts +356 -0
  193. package/src/viewer/cli.ts +49 -17
  194. package/src/workflows/composition.ts +36 -2
  195. package/src/workflows/definition.ts +32 -0
  196. package/src/workflows/engine.ts +157 -54
  197. package/src/workflows/errors.ts +11 -2
  198. package/src/workflows/human-decision.ts +49 -0
  199. package/src/workflows/index.ts +2 -0
  200. package/src/workflows/schema.ts +19 -6
  201. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  202. package/src/workflows/store.ts +1316 -108
  203. package/src/workflows/tool-input.ts +2 -60
  204. package/src/workflows/types.ts +32 -4
  205. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  206. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  207. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  208. package/dist/extension/controller-host.d.ts +0 -48
  209. package/dist/extension/controller-host.js +0 -110
  210. package/dist/extension/controller-host.js.map +0 -1
  211. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  212. package/dist/extension/deferred-turn-coordinator.js +0 -143
  213. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  214. package/dist/extension/deferred-turn.d.ts +0 -44
  215. package/dist/extension/deferred-turn.js +0 -110
  216. package/dist/extension/deferred-turn.js.map +0 -1
  217. package/dist/extension/executor.d.ts +0 -86
  218. package/dist/extension/executor.js +0 -311
  219. package/dist/extension/executor.js.map +0 -1
  220. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  221. package/dist/extension/follow-up-coordinator.js +0 -131
  222. package/dist/extension/follow-up-coordinator.js.map +0 -1
  223. package/dist/extension/recorder.d.ts +0 -84
  224. package/dist/extension/recorder.js +0 -528
  225. package/dist/extension/recorder.js.map +0 -1
  226. package/dist/extension/session-events.d.ts +0 -133
  227. package/dist/extension/session-events.js +0 -61
  228. package/dist/extension/session-events.js.map +0 -1
  229. package/dist/viewer/session-reducer.js.map +0 -1
  230. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  231. package/src/extension/controller-host.ts +0 -167
  232. package/src/extension/deferred-turn-coordinator.ts +0 -171
  233. package/src/extension/deferred-turn.ts +0 -166
  234. package/src/extension/executor.ts +0 -411
  235. package/src/extension/follow-up-coordinator.ts +0 -151
  236. package/src/extension/recorder.ts +0 -655
  237. package/src/extension/session-events.ts +0 -121
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  export const STATE_APPLICATION_ID = 0x50495746;
4
4
  export const STATE_SCHEMA_NAME = "pi-workflows-state";
5
5
  export const STATE_SCHEMA_VERSION = 1;
6
- export const STATE_APP_VERSION = "0.13.3";
6
+ export const STATE_APP_VERSION = "0.14.0";
7
7
 
8
8
  export const STATE_SCHEMA_SQL = String.raw`
9
9
  CREATE TABLE schema_meta (
@@ -22,6 +22,28 @@ CREATE TABLE projects (
22
22
  created_at INTEGER NOT NULL
23
23
  ) STRICT;
24
24
 
25
+ CREATE TABLE workflow_host_state (
26
+ id INTEGER PRIMARY KEY CHECK (id = 1),
27
+ epoch INTEGER NOT NULL DEFAULT 0 CHECK (epoch >= 0),
28
+ host_id TEXT,
29
+ token_hash BLOB CHECK (token_hash IS NULL OR length(token_hash) = 32),
30
+ pid INTEGER CHECK (pid IS NULL OR pid > 0),
31
+ process_start_identity TEXT,
32
+ started_at INTEGER,
33
+ heartbeat_at INTEGER,
34
+ expires_at INTEGER,
35
+ CHECK (
36
+ (host_id IS NULL AND token_hash IS NULL AND pid IS NULL AND process_start_identity IS NULL
37
+ AND started_at IS NULL AND heartbeat_at IS NULL AND expires_at IS NULL)
38
+ OR
39
+ (host_id IS NOT NULL AND token_hash IS NOT NULL AND pid IS NOT NULL
40
+ AND process_start_identity IS NOT NULL AND started_at IS NOT NULL
41
+ AND heartbeat_at IS NOT NULL AND expires_at IS NOT NULL)
42
+ )
43
+ ) STRICT;
44
+
45
+ INSERT INTO workflow_host_state(id, epoch) VALUES (1, 0);
46
+
25
47
  CREATE TABLE blobs (
26
48
  blob_hash BLOB PRIMARY KEY CHECK (length(blob_hash) = 32),
27
49
  media_type TEXT NOT NULL,
@@ -115,6 +137,39 @@ CREATE INDEX runs_project_idx ON runs(project_id, created_at DESC);
115
137
  CREATE INDEX runs_status_idx ON runs(status, updated_at DESC);
116
138
  CREATE INDEX runs_parent_idx ON runs(parent_run_id);
117
139
 
140
+ CREATE TABLE viewer_runs (
141
+ run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
142
+ presentation_revision INTEGER NOT NULL CHECK (presentation_revision >= 1),
143
+ retained_from_revision INTEGER NOT NULL CHECK (
144
+ retained_from_revision >= 1 AND retained_from_revision <= presentation_revision
145
+ ),
146
+ updated_at INTEGER NOT NULL
147
+ ) STRICT;
148
+
149
+ CREATE TABLE viewer_deltas (
150
+ run_id TEXT NOT NULL REFERENCES viewer_runs(run_id) ON DELETE CASCADE,
151
+ presentation_revision INTEGER NOT NULL CHECK (presentation_revision >= 1),
152
+ delta_index INTEGER NOT NULL CHECK (delta_index >= 0),
153
+ target_type TEXT NOT NULL CHECK (target_type IN (
154
+ 'summary', 'graph', 'replay', 'timeline', 'conversation', 'inspector'
155
+ )),
156
+ target_key TEXT NOT NULL,
157
+ patch_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
158
+ recorded_at INTEGER NOT NULL,
159
+ PRIMARY KEY (run_id, presentation_revision, delta_index)
160
+ ) STRICT;
161
+
162
+ CREATE INDEX viewer_deltas_resume_idx
163
+ ON viewer_deltas(run_id, presentation_revision, delta_index);
164
+
165
+ CREATE TABLE viewer_session_checkpoints (
166
+ run_id TEXT NOT NULL REFERENCES viewer_runs(run_id) ON DELETE CASCADE,
167
+ event_seq INTEGER NOT NULL CHECK (event_seq > 0),
168
+ state_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
169
+ recorded_at INTEGER NOT NULL,
170
+ PRIMARY KEY (run_id, event_seq)
171
+ ) STRICT;
172
+
118
173
  CREATE TABLE run_sources (
119
174
  run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
120
175
  mount_path TEXT NOT NULL,
@@ -240,6 +295,70 @@ CREATE INDEX run_queue_claim_idx ON run_queue(status, available_at, created_at);
240
295
  CREATE UNIQUE INDEX run_queue_active_session_idx ON run_queue(origin_session_id)
241
296
  WHERE status IN ('queued', 'starting', 'running');
242
297
 
298
+ CREATE TABLE host_commands (
299
+ request_id TEXT PRIMARY KEY,
300
+ client_id TEXT NOT NULL,
301
+ operation TEXT NOT NULL CHECK (operation IN (
302
+ 'run.start', 'run.pause', 'run.resume', 'run.cancel', 'run.status', 'run.list',
303
+ 'checkpoint.answer', 'decision.answer', 'interaction.submit', 'interaction.update',
304
+ 'notification.claim', 'notification.deliver', 'turn.claim', 'turn.resolve',
305
+ 'controller.list', 'controller.get', 'controller.apply', 'controller.reconcile', 'controller.delete',
306
+ 'host.status', 'host.stop'
307
+ )),
308
+ idempotency_key TEXT NOT NULL,
309
+ request_fingerprint BLOB NOT NULL CHECK (length(request_fingerprint) = 32),
310
+ run_id TEXT REFERENCES runs(run_id) ON DELETE SET NULL,
311
+ accepted_revision INTEGER CHECK (accepted_revision IS NULL OR accepted_revision >= 0),
312
+ outcome TEXT NOT NULL CHECK (outcome IN (
313
+ 'accepted', 'adopted', 'rejected', 'conflict', 'notFound', 'claimLost', 'unavailable'
314
+ )),
315
+ receipt_hash BLOB REFERENCES blobs(blob_hash),
316
+ error_hash BLOB REFERENCES blobs(blob_hash),
317
+ host_epoch INTEGER NOT NULL CHECK (host_epoch > 0),
318
+ created_at INTEGER NOT NULL,
319
+ completed_at INTEGER NOT NULL,
320
+ UNIQUE (client_id, idempotency_key)
321
+ ) STRICT;
322
+
323
+ CREATE INDEX host_commands_run_idx ON host_commands(run_id, created_at);
324
+
325
+ CREATE TABLE run_workers (
326
+ worker_epoch TEXT PRIMARY KEY,
327
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
328
+ generation INTEGER NOT NULL CHECK (generation > 0),
329
+ host_epoch INTEGER NOT NULL CHECK (host_epoch > 0),
330
+ launch_envelope_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
331
+ pid INTEGER,
332
+ process_start_identity TEXT,
333
+ status TEXT NOT NULL CHECK (status IN (
334
+ 'starting', 'ready', 'running', 'exited', 'cancelled', 'timedOut',
335
+ 'crashed', 'claimLost', 'orphaned'
336
+ )),
337
+ started_at INTEGER NOT NULL,
338
+ ready_at INTEGER,
339
+ finished_at INTEGER,
340
+ exit_code INTEGER,
341
+ signal TEXT,
342
+ diagnostic_hash BLOB REFERENCES blobs(blob_hash),
343
+ CHECK ((pid IS NULL) = (process_start_identity IS NULL))
344
+ ) STRICT;
345
+
346
+ CREATE UNIQUE INDEX run_workers_active_idx ON run_workers(run_id)
347
+ WHERE status IN ('starting', 'ready', 'running');
348
+ CREATE INDEX run_workers_generation_idx ON run_workers(run_id, generation, started_at);
349
+
350
+ CREATE TABLE worker_messages (
351
+ worker_epoch TEXT NOT NULL REFERENCES run_workers(worker_epoch) ON DELETE CASCADE,
352
+ message_id TEXT NOT NULL,
353
+ request_fingerprint BLOB NOT NULL CHECK (length(request_fingerprint) = 32),
354
+ outcome TEXT NOT NULL CHECK (outcome IN ('accepted', 'adopted', 'rejected', 'claimLost')),
355
+ accepted_revision INTEGER CHECK (accepted_revision IS NULL OR accepted_revision >= 0),
356
+ result_hash BLOB REFERENCES blobs(blob_hash),
357
+ error_hash BLOB REFERENCES blobs(blob_hash),
358
+ completed_at INTEGER NOT NULL,
359
+ PRIMARY KEY (worker_epoch, message_id)
360
+ ) STRICT;
361
+
243
362
  CREATE TABLE node_attempts (
244
363
  attempt_id TEXT PRIMARY KEY,
245
364
  run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
@@ -268,6 +387,7 @@ CREATE TABLE node_attempts (
268
387
  CREATE UNIQUE INDEX node_attempts_active_idx ON node_attempts(run_id)
269
388
  WHERE status IN ('pending', 'running', 'waiting');
270
389
  CREATE INDEX node_attempts_run_idx ON node_attempts(run_id, created_at);
390
+ CREATE INDEX node_attempts_deadline_idx ON node_attempts(deadline_at) WHERE deadline_at IS NOT NULL;
271
391
 
272
392
  CREATE TABLE run_steps (
273
393
  run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
@@ -314,14 +434,19 @@ CREATE INDEX session_segments_run_idx ON session_segments(run_id, created_at);
314
434
 
315
435
  CREATE TABLE session_entries (
316
436
  segment_id TEXT NOT NULL REFERENCES session_segments(segment_id) ON DELETE CASCADE,
437
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
317
438
  entry_seq INTEGER NOT NULL CHECK (entry_seq > 0),
439
+ run_seq INTEGER NOT NULL CHECK (run_seq > 0),
318
440
  entry_id TEXT NOT NULL,
319
441
  entry_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
320
442
  recorded_at INTEGER NOT NULL,
321
443
  PRIMARY KEY (segment_id, entry_seq),
322
- UNIQUE (segment_id, entry_id)
444
+ UNIQUE (segment_id, entry_id),
445
+ UNIQUE (run_id, run_seq)
323
446
  ) STRICT;
324
447
 
448
+ CREATE INDEX session_entries_run_idx ON session_entries(run_id, run_seq);
449
+
325
450
  CREATE TABLE attempt_entries (
326
451
  attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
327
452
  role TEXT NOT NULL CHECK (role IN ('prompt', 'response', 'first', 'last')),
@@ -336,7 +461,9 @@ CREATE INDEX attempt_entries_entry_idx ON attempt_entries(segment_id, entry_id);
336
461
 
337
462
  CREATE TABLE session_events (
338
463
  segment_id TEXT NOT NULL REFERENCES session_segments(segment_id) ON DELETE CASCADE,
464
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
339
465
  event_seq INTEGER NOT NULL CHECK (event_seq > 0),
466
+ run_seq INTEGER NOT NULL CHECK (run_seq > 0),
340
467
  event_type TEXT NOT NULL,
341
468
  node_id TEXT,
342
469
  attempt_id TEXT,
@@ -345,9 +472,12 @@ CREATE TABLE session_events (
345
472
  tool_call_id TEXT,
346
473
  payload_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
347
474
  recorded_at INTEGER NOT NULL,
348
- PRIMARY KEY (segment_id, event_seq)
475
+ PRIMARY KEY (segment_id, event_seq),
476
+ UNIQUE (run_id, run_seq)
349
477
  ) STRICT;
350
478
 
479
+ CREATE INDEX session_events_run_idx ON session_events(run_id, run_seq);
480
+
351
481
  CREATE TABLE human_decisions (
352
482
  decision_id TEXT PRIMARY KEY,
353
483
  resource_id TEXT NOT NULL UNIQUE REFERENCES resources(resource_id),
@@ -369,6 +499,44 @@ CREATE TABLE human_decisions (
369
499
  CREATE INDEX human_decisions_run_idx ON human_decisions(run_id, created_at);
370
500
  CREATE INDEX human_decisions_deadline_idx ON human_decisions(deadline_at) WHERE deadline_at IS NOT NULL;
371
501
 
502
+ CREATE TABLE interactive_requests (
503
+ request_id TEXT PRIMARY KEY,
504
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
505
+ attempt_id TEXT NOT NULL UNIQUE REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
506
+ target_session_id TEXT NOT NULL,
507
+ kind TEXT NOT NULL CHECK (kind IN ('agent', 'assistant', 'decision')),
508
+ contract_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
509
+ revision INTEGER NOT NULL DEFAULT 1 CHECK (revision > 0),
510
+ status TEXT NOT NULL CHECK (status IN ('pending', 'presenting', 'settled', 'cancelled')),
511
+ presenter_id TEXT,
512
+ presentation_claim_expires_at INTEGER,
513
+ presentation_session_entry_id TEXT,
514
+ accepted_submission_id TEXT,
515
+ created_at INTEGER NOT NULL,
516
+ updated_at INTEGER NOT NULL,
517
+ settled_at INTEGER,
518
+ consumed_at INTEGER,
519
+ CHECK ((status = 'settled') = (accepted_submission_id IS NOT NULL AND settled_at IS NOT NULL)),
520
+ CHECK ((presenter_id IS NULL) = (presentation_claim_expires_at IS NULL)),
521
+ CHECK (presenter_id IS NULL OR status = 'presenting'),
522
+ CHECK (consumed_at IS NULL OR status = 'settled')
523
+ ) STRICT;
524
+
525
+ CREATE INDEX interactive_requests_session_idx
526
+ ON interactive_requests(target_session_id, status, created_at);
527
+
528
+ CREATE TABLE interactive_submissions (
529
+ submission_id TEXT PRIMARY KEY,
530
+ request_id TEXT NOT NULL REFERENCES interactive_requests(request_id) ON DELETE CASCADE,
531
+ idempotency_key TEXT NOT NULL,
532
+ request_revision INTEGER NOT NULL CHECK (request_revision > 0),
533
+ payload_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
534
+ outcome TEXT NOT NULL CHECK (outcome IN ('validating', 'accepted', 'rejected', 'adopted')),
535
+ receipt_hash BLOB REFERENCES blobs(blob_hash),
536
+ submitted_at INTEGER NOT NULL,
537
+ UNIQUE (request_id, idempotency_key)
538
+ ) STRICT;
539
+
372
540
  CREATE TABLE human_decision_resolutions (
373
541
  decision_id TEXT PRIMARY KEY REFERENCES human_decisions(decision_id) ON DELETE CASCADE,
374
542
  outcome TEXT NOT NULL CHECK (outcome IN ('accepted', 'cancelled')),
@@ -0,0 +1,356 @@
1
+ import { StateDatabase } from "./database.js";
2
+ import { canonicalJson, parseJson, type JsonValue } from "./json.js";
3
+
4
+ export const VIEWER_DELTA_RETENTION = 256;
5
+ export const VIEWER_PAGE_SIZE = 256;
6
+
7
+ export type ViewerTargetType =
8
+ | "summary"
9
+ | "graph"
10
+ | "replay"
11
+ | "timeline"
12
+ | "conversation"
13
+ | "inspector";
14
+
15
+ export type ViewerPatchOperation =
16
+ | { op: "add" | "replace"; path: string; value: JsonValue }
17
+ | { op: "remove"; path: string }
18
+ | { op: "append"; path: string; value: JsonValue[] };
19
+
20
+ export type ViewerDeltaDraft = {
21
+ targetType: ViewerTargetType;
22
+ targetKey?: string;
23
+ patch?: ViewerPatchOperation[];
24
+ };
25
+
26
+ export type ViewerTailItem = JsonValue;
27
+
28
+ export type ViewerDeltaRecord = {
29
+ runId: string;
30
+ presentationRevision: number;
31
+ deltaIndex: number;
32
+ targetType: ViewerTargetType;
33
+ targetKey: string;
34
+ patch: ViewerPatchOperation[];
35
+ recordedAt: number;
36
+ };
37
+
38
+ export type ViewerCursorResult =
39
+ | {
40
+ kind: "deltas";
41
+ currentRevision: number;
42
+ retainedFromRevision: number;
43
+ deltas: ViewerDeltaRecord[];
44
+ }
45
+ | {
46
+ kind: "snapshot_required";
47
+ currentRevision: number;
48
+ retainedFromRevision: number;
49
+ };
50
+
51
+ type ViewerRunRow = {
52
+ presentationRevision: number;
53
+ retainedFromRevision: number;
54
+ };
55
+
56
+ type ViewerDeltaRow = {
57
+ runId: string;
58
+ presentationRevision: number;
59
+ deltaIndex: number;
60
+ targetType: string;
61
+ targetKey: string;
62
+ patchHash: Buffer;
63
+ recordedAt: number;
64
+ };
65
+
66
+ export function initializeViewerRun(state: StateDatabase, runId: string, now: number): number {
67
+ state.connection
68
+ .prepare(
69
+ `INSERT INTO viewer_runs(
70
+ run_id, presentation_revision, retained_from_revision, updated_at
71
+ ) VALUES (?, 1, 1, ?)
72
+ ON CONFLICT(run_id) DO NOTHING`,
73
+ )
74
+ .run(runId, now);
75
+ const row = state.connection
76
+ .prepare(
77
+ `SELECT presentation_revision AS presentationRevision,
78
+ retained_from_revision AS retainedFromRevision
79
+ FROM viewer_runs WHERE run_id = ?`,
80
+ )
81
+ .get(runId);
82
+ if (!isViewerRunRow(row)) {
83
+ throw new Error(`Viewer projection could not be initialized for workflow run: ${runId}`);
84
+ }
85
+ return row.presentationRevision;
86
+ }
87
+
88
+ export function recordViewerDeltas(
89
+ state: StateDatabase,
90
+ runId: string,
91
+ drafts: readonly ViewerDeltaDraft[],
92
+ now: number = Date.now(),
93
+ ): number {
94
+ if (!state.connection.inTransaction) {
95
+ throw new Error("Viewer changes must be recorded inside the durable state transaction");
96
+ }
97
+ if (drafts.length === 0) {
98
+ throw new Error("Viewer change must name at least one bounded target");
99
+ }
100
+ const row = state.connection
101
+ .prepare(
102
+ `SELECT presentation_revision AS presentationRevision,
103
+ retained_from_revision AS retainedFromRevision
104
+ FROM viewer_runs WHERE run_id = ?`,
105
+ )
106
+ .get(runId);
107
+ if (!isViewerRunRow(row)) {
108
+ throw new Error(`Viewer projection does not exist for workflow run: ${runId}`);
109
+ }
110
+ const revision = row.presentationRevision + 1;
111
+ for (const [index, draft] of drafts.entries()) {
112
+ const patch: ViewerPatchOperation[] = [
113
+ {
114
+ op: "replace",
115
+ path: "/presentationRevision",
116
+ value: revision,
117
+ },
118
+ ...(draft.targetType === "graph"
119
+ ? [
120
+ {
121
+ op: "replace" as const,
122
+ path: "/graphRevision",
123
+ value: revision,
124
+ },
125
+ ]
126
+ : []),
127
+ ...(draft.patch ?? []),
128
+ ];
129
+ const patchHash = state.putJson(patch, now);
130
+ state.connection
131
+ .prepare(
132
+ `INSERT INTO viewer_deltas(
133
+ run_id, presentation_revision, delta_index, target_type,
134
+ target_key, patch_hash, recorded_at
135
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
136
+ )
137
+ .run(runId, revision, index, draft.targetType, draft.targetKey ?? "", patchHash, now);
138
+ }
139
+ const retainedFromRevision = Math.max(1, revision - VIEWER_DELTA_RETENTION + 1);
140
+ const update = state.connection
141
+ .prepare(
142
+ `UPDATE viewer_runs
143
+ SET presentation_revision = ?, retained_from_revision = ?, updated_at = ?
144
+ WHERE run_id = ? AND presentation_revision = ?`,
145
+ )
146
+ .run(revision, retainedFromRevision, now, runId, row.presentationRevision);
147
+ if (update.changes !== 1) {
148
+ throw new Error(`Viewer projection revision conflict for workflow run: ${runId}`);
149
+ }
150
+ const stalePatchHashes = state.connection
151
+ .prepare(
152
+ `SELECT DISTINCT patch_hash AS patchHash FROM viewer_deltas
153
+ WHERE run_id = ? AND presentation_revision < ?`,
154
+ )
155
+ .all(runId, retainedFromRevision)
156
+ .filter(isPatchHashRow)
157
+ .map((value) => value.patchHash);
158
+ state.connection
159
+ .prepare(
160
+ `DELETE FROM viewer_deltas
161
+ WHERE run_id = ? AND presentation_revision < ?`,
162
+ )
163
+ .run(runId, retainedFromRevision);
164
+ const deleteBlob = state.connection.prepare(
165
+ `DELETE FROM blobs WHERE blob_hash = ?
166
+ AND NOT EXISTS (SELECT 1 FROM viewer_deltas WHERE patch_hash = blobs.blob_hash)`,
167
+ );
168
+ for (const hash of stalePatchHashes) {
169
+ try {
170
+ deleteBlob.run(hash);
171
+ } catch (error) {
172
+ if (
173
+ typeof error !== "object" ||
174
+ error === null ||
175
+ !("code" in error) ||
176
+ error.code !== "SQLITE_CONSTRAINT_FOREIGNKEY"
177
+ ) {
178
+ throw error;
179
+ }
180
+ }
181
+ }
182
+ return revision;
183
+ }
184
+
185
+ export function viewerTailPatch(
186
+ oldTotal: number,
187
+ items: readonly ViewerTailItem[],
188
+ ): ViewerPatchOperation[] {
189
+ if (!Number.isInteger(oldTotal) || oldTotal < 0) {
190
+ throw new Error("Viewer page total must be a non-negative integer");
191
+ }
192
+ if (items.length === 0) return [];
193
+ const nextTotal = oldTotal + items.length;
194
+ const previousStart = Math.max(0, oldTotal - VIEWER_PAGE_SIZE);
195
+ const nextStart = Math.max(0, nextTotal - VIEWER_PAGE_SIZE);
196
+ const patch: ViewerPatchOperation[] = [];
197
+ if (items.length >= VIEWER_PAGE_SIZE) {
198
+ patch.push({
199
+ op: "replace",
200
+ path: "/items",
201
+ value: items.slice(-VIEWER_PAGE_SIZE),
202
+ });
203
+ } else {
204
+ const retainedBefore = oldTotal - previousStart;
205
+ const removed = Math.min(nextStart - previousStart, retainedBefore);
206
+ for (let index = 0; index < removed; index += 1) {
207
+ patch.push({ op: "remove", path: "/items/0" });
208
+ }
209
+ patch.push({ op: "append", path: "/items", value: [...items] });
210
+ }
211
+ if (nextStart !== previousStart) {
212
+ patch.push({ op: "replace", path: "/start", value: nextStart });
213
+ }
214
+ patch.push({ op: "replace", path: "/total", value: nextTotal });
215
+ return patch;
216
+ }
217
+
218
+ export function readViewerDeltas(
219
+ state: StateDatabase,
220
+ runId: string,
221
+ afterRevision: number,
222
+ ): ViewerCursorResult {
223
+ if (!Number.isInteger(afterRevision) || afterRevision < 0) {
224
+ throw new Error("Viewer cursor must be a non-negative integer");
225
+ }
226
+ const row = state.connection
227
+ .prepare(
228
+ `SELECT presentation_revision AS presentationRevision,
229
+ retained_from_revision AS retainedFromRevision
230
+ FROM viewer_runs WHERE run_id = ?`,
231
+ )
232
+ .get(runId);
233
+ if (!isViewerRunRow(row)) {
234
+ throw new Error(`Viewer projection does not exist for workflow run: ${runId}`);
235
+ }
236
+ if (
237
+ afterRevision === 0 ||
238
+ afterRevision > row.presentationRevision ||
239
+ afterRevision < row.retainedFromRevision - 1
240
+ ) {
241
+ return {
242
+ kind: "snapshot_required",
243
+ currentRevision: row.presentationRevision,
244
+ retainedFromRevision: row.retainedFromRevision,
245
+ };
246
+ }
247
+ const rows = state.connection
248
+ .prepare(
249
+ `SELECT run_id AS runId, presentation_revision AS presentationRevision,
250
+ delta_index AS deltaIndex, target_type AS targetType,
251
+ target_key AS targetKey, patch_hash AS patchHash,
252
+ recorded_at AS recordedAt
253
+ FROM viewer_deltas
254
+ WHERE run_id = ? AND presentation_revision > ?
255
+ ORDER BY presentation_revision, delta_index`,
256
+ )
257
+ .all(runId, afterRevision);
258
+ const deltas: ViewerDeltaRecord[] = [];
259
+ for (const value of rows) {
260
+ if (!isViewerDeltaRow(value) || !isViewerTargetType(value.targetType)) {
261
+ throw new Error("Viewer delta row is invalid");
262
+ }
263
+ deltas.push({
264
+ runId: value.runId,
265
+ presentationRevision: value.presentationRevision,
266
+ deltaIndex: value.deltaIndex,
267
+ targetType: value.targetType,
268
+ targetKey: value.targetKey,
269
+ patch: parsePatch(state.readJson(value.patchHash)),
270
+ recordedAt: value.recordedAt,
271
+ });
272
+ }
273
+ const revisions = [...new Set(deltas.map((delta) => delta.presentationRevision))];
274
+ const expectedCount = row.presentationRevision - afterRevision;
275
+ if (
276
+ revisions.length !== expectedCount ||
277
+ revisions.some((revision, index) => revision !== afterRevision + index + 1)
278
+ ) {
279
+ return {
280
+ kind: "snapshot_required",
281
+ currentRevision: row.presentationRevision,
282
+ retainedFromRevision: row.retainedFromRevision,
283
+ };
284
+ }
285
+ return {
286
+ kind: "deltas",
287
+ currentRevision: row.presentationRevision,
288
+ retainedFromRevision: row.retainedFromRevision,
289
+ deltas,
290
+ };
291
+ }
292
+
293
+ function parsePatch(value: JsonValue): ViewerPatchOperation[] {
294
+ if (!Array.isArray(value)) throw new Error("Viewer delta patch is not an array");
295
+ const operations: ViewerPatchOperation[] = [];
296
+ for (const item of value) {
297
+ if (!isRecord(item) || typeof item.op !== "string" || typeof item.path !== "string") {
298
+ throw new Error("Viewer delta patch operation is invalid");
299
+ }
300
+ if (item.op === "remove") {
301
+ operations.push({ op: "remove", path: item.path });
302
+ continue;
303
+ }
304
+ if ((item.op === "add" || item.op === "replace") && "value" in item) {
305
+ operations.push({
306
+ op: item.op,
307
+ path: item.path,
308
+ value: parseJson(canonicalJson(item.value)),
309
+ });
310
+ continue;
311
+ }
312
+ if (item.op === "append" && Array.isArray(item.value)) {
313
+ operations.push({
314
+ op: "append",
315
+ path: item.path,
316
+ value: item.value.map((entry) => parseJson(canonicalJson(entry))),
317
+ });
318
+ continue;
319
+ }
320
+ throw new Error("Viewer delta patch operation is unsupported");
321
+ }
322
+ return operations;
323
+ }
324
+
325
+ function isPatchHashRow(value: unknown): value is { patchHash: Buffer } {
326
+ return isRecord(value) && Buffer.isBuffer(value.patchHash);
327
+ }
328
+
329
+ function isViewerRunRow(value: unknown): value is ViewerRunRow {
330
+ return (
331
+ isRecord(value) &&
332
+ typeof value.presentationRevision === "number" &&
333
+ typeof value.retainedFromRevision === "number"
334
+ );
335
+ }
336
+
337
+ function isViewerDeltaRow(value: unknown): value is ViewerDeltaRow {
338
+ return (
339
+ isRecord(value) &&
340
+ typeof value.runId === "string" &&
341
+ typeof value.presentationRevision === "number" &&
342
+ typeof value.deltaIndex === "number" &&
343
+ typeof value.targetType === "string" &&
344
+ typeof value.targetKey === "string" &&
345
+ Buffer.isBuffer(value.patchHash) &&
346
+ typeof value.recordedAt === "number"
347
+ );
348
+ }
349
+
350
+ function isViewerTargetType(value: string): value is ViewerTargetType {
351
+ return ["summary", "graph", "replay", "timeline", "conversation", "inspector"].includes(value);
352
+ }
353
+
354
+ function isRecord(value: unknown): value is Record<string, unknown> {
355
+ return typeof value === "object" && value !== null && !Array.isArray(value);
356
+ }