@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
@@ -1,9 +1,13 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { StateDatabase, workflowStatePath } from "../state/database.js";
3
- import { canonicalJson } from "../state/json.js";
3
+ import { canonicalJson, parseJson } from "../state/json.js";
4
4
  import { StateMutationStore, StaleResourceError, resourceIdFor, tokenHash, } from "../state/mutation.js";
5
+ import { initializeViewerRun, recordViewerDeltas, viewerTailPatch, VIEWER_PAGE_SIZE, } from "../state/viewer.js";
5
6
  import { compositionMetadata } from "./composition.js";
7
+ import { ClaimLostError } from "./errors.js";
8
+ import { HumanDecisionStore } from "./human-decision.js";
6
9
  import { applyJsonPatch, validateJsonPatch } from "./json-patch.js";
10
+ import { reduceSessionEvents, reduceSessionEventsFromCheckpoint, } from "./session-reducer.js";
7
11
  import { applyWorkflowSettingsPatch, workflowSettingsScopeId, } from "./settings.js";
8
12
  import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
9
13
  export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1";
@@ -35,11 +39,13 @@ export class WorkflowRunStore {
35
39
  databasePath;
36
40
  state;
37
41
  authorityProvider;
42
+ snapshotLifecycle;
38
43
  contexts = new Map();
39
44
  ownsState;
40
45
  mutations;
41
46
  constructor(databasePath = workflowStatePath(), options = {}) {
42
47
  this.authorityProvider = options.authorityProvider;
48
+ this.snapshotLifecycle = options.snapshotLifecycle;
43
49
  this.ownsState = options.state === undefined;
44
50
  this.state =
45
51
  options.state ??
@@ -55,7 +61,195 @@ export class WorkflowRunStore {
55
61
  if (this.ownsState)
56
62
  this.state.close();
57
63
  }
64
+ /** Host-only synchronization after a queue lifecycle mutation on the run resource. */
65
+ synchronizeRevision(runId) {
66
+ const revision = this.resourceRevision(runId);
67
+ const context = this.contexts.get(runId);
68
+ if (context === undefined) {
69
+ this.contexts.set(runId, { revision, lock: Promise.resolve() });
70
+ }
71
+ else {
72
+ context.revision = revision;
73
+ }
74
+ return revision;
75
+ }
76
+ async createHumanDecisionRequest(request) {
77
+ return await new HumanDecisionStore(this.databasePath, { state: this.state }).createRequest(request);
78
+ }
79
+ async readResolvedHumanDecision(decisionId) {
80
+ return await new HumanDecisionStore(this.databasePath, { state: this.state }).readResolved(decisionId);
81
+ }
82
+ async reserveEffect(options) {
83
+ return await this.withRunLock(options.runId, async () => this.state.transaction(() => {
84
+ requireBoundedEffectText(options.effectType, "type");
85
+ requireBoundedEffectText(options.idempotencyKey, "idempotency key");
86
+ const context = this.contextFor(options.runId);
87
+ const run = this.requireRunRow(options.runId);
88
+ this.assertWriteAuthority(run, context.revision);
89
+ const authority = this.authorityProvider?.(options.runId);
90
+ const payloadHash = this.state.putJson({ recovery: options.recovery, request: options.request }, Date.now());
91
+ const effectId = workflowEffectId(run.resourceId, options.effectType, options.idempotencyKey);
92
+ const existing = this.state.connection
93
+ .prepare(`SELECT status, payload_hash AS payloadHash, result_hash AS resultHash,
94
+ attempt_count AS attemptCount
95
+ FROM effects WHERE effect_id = ?`)
96
+ .get(effectId);
97
+ if (isWorkflowEffectRow(existing)) {
98
+ if (!existing.payloadHash.equals(payloadHash)) {
99
+ throw new Error("Managed effect key was reused with another request");
100
+ }
101
+ if (existing.status === "applied") {
102
+ return {
103
+ effectId,
104
+ attemptNumber: existing.attemptCount,
105
+ disposition: "adopted",
106
+ ...(existing.resultHash === null
107
+ ? {}
108
+ : { result: this.state.readJson(existing.resultHash) }),
109
+ };
110
+ }
111
+ if (existing.status !== "pending") {
112
+ return {
113
+ effectId,
114
+ attemptNumber: existing.attemptCount,
115
+ disposition: "ambiguous",
116
+ };
117
+ }
118
+ return this.beginEffectAttempt(run, effectId, existing.attemptCount + 1, authority);
119
+ }
120
+ const now = Date.now();
121
+ const effectResourceId = this.mutations.ensureResource("effect", effectId, now);
122
+ this.mutations.mutate({
123
+ resourceId: effectResourceId,
124
+ operation: "effect.reserve",
125
+ actor: authority?.actor ?? { type: "system" },
126
+ expectedRevision: 0,
127
+ }, "effect.reserved", () => {
128
+ this.state.connection
129
+ .prepare(`INSERT INTO effects(
130
+ effect_id, resource_id, source_resource_id, source_revision,
131
+ effect_type, idempotency_key, payload_hash, owner_scope, status,
132
+ attempt_count, created_at, updated_at
133
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'run', 'applying', 1, ?, ?)`)
134
+ .run(effectId, effectResourceId, run.resourceId, context.revision, options.effectType, options.idempotencyKey, payloadHash, now, now);
135
+ this.insertEffectAttempt(effectId, 1, authority, now);
136
+ }, { now, payload: { runId: options.runId, attemptId: options.attemptId } });
137
+ return { effectId, attemptNumber: 1, disposition: "execute" };
138
+ }));
139
+ }
140
+ async settleEffect(options) {
141
+ await this.withRunLock(options.runId, async () => this.state.transaction(() => {
142
+ const context = this.contextFor(options.runId);
143
+ const run = this.requireRunRow(options.runId);
144
+ this.assertWriteAuthority(run, context.revision);
145
+ const row = this.state.connection
146
+ .prepare(`SELECT e.resource_id AS resourceId, e.status
147
+ FROM effects e WHERE e.effect_id = ? AND e.source_resource_id = ?`)
148
+ .get(options.effectId, run.resourceId);
149
+ if (!isWorkflowEffectIdentityRow(row) || row.status !== "applying") {
150
+ throw new Error("Managed effect is not applying");
151
+ }
152
+ const now = Date.now();
153
+ const revision = this.requireResourceRevision(row.resourceId);
154
+ const resultHash = options.result === undefined ? null : this.state.putJson(options.result, now);
155
+ const errorHash = options.error === undefined ? null : this.state.putText(options.error, now);
156
+ this.mutations.mutate({
157
+ resourceId: row.resourceId,
158
+ operation: "effect.settle",
159
+ actor: this.authorityProvider?.(options.runId)?.actor ?? { type: "system" },
160
+ expectedRevision: revision,
161
+ }, `effect.${options.outcome}`, () => {
162
+ const changed = this.state.connection
163
+ .prepare(`UPDATE effects
164
+ SET status = ?, result_hash = ?, error_hash = ?, updated_at = ?, settled_at = ?
165
+ WHERE effect_id = ? AND status = 'applying' AND attempt_count = ?`)
166
+ .run(options.outcome, resultHash, errorHash, now, now, options.effectId, options.attemptNumber);
167
+ /* istanbul ignore if -- serialized effect mutation keeps this attempt current */
168
+ if (changed.changes !== 1)
169
+ throw new Error("Managed effect attempt changed");
170
+ this.state.connection
171
+ .prepare(`UPDATE effect_attempts
172
+ SET finished_at = ?, outcome = ?, result_hash = ?, error_hash = ?
173
+ WHERE effect_id = ? AND attempt_number = ? AND finished_at IS NULL`)
174
+ .run(now, options.outcome === "cancelled" ? "interrupted" : options.outcome, resultHash, errorHash, options.effectId, options.attemptNumber);
175
+ }, { now, payload: { runId: options.runId, outcome: options.outcome } });
176
+ }));
177
+ }
178
+ async recoverApplyingEffects(runId) {
179
+ return await this.withRunLock(runId, async () => this.state.transaction(() => {
180
+ const context = this.contextFor(runId);
181
+ const run = this.requireRunRow(runId);
182
+ this.assertWriteAuthority(run, context.revision);
183
+ const authority = this.authorityProvider?.(runId);
184
+ const rows = this.state.connection
185
+ .prepare(`SELECT effect_id AS effectId, resource_id AS resourceId,
186
+ payload_hash AS payloadHash, attempt_count AS attemptCount
187
+ FROM effects WHERE source_resource_id = ? AND status = 'applying'`)
188
+ .all(run.resourceId)
189
+ .filter(isApplyingEffectRow);
190
+ let ambiguous = false;
191
+ for (const row of rows) {
192
+ const payload = this.state.readJson(row.payloadHash);
193
+ const recovery = effectRecoveryFromPayload(payload);
194
+ const status = recovery === "idempotent" ? "pending" : "ambiguous";
195
+ if (status === "ambiguous")
196
+ ambiguous = true;
197
+ const now = Date.now();
198
+ const revision = this.requireResourceRevision(row.resourceId);
199
+ this.mutations.mutate({
200
+ resourceId: row.resourceId,
201
+ operation: "effect.recover",
202
+ actor: authority?.actor ?? { type: "system" },
203
+ expectedRevision: revision,
204
+ }, status === "pending" ? "effect.retry_ready" : "effect.ambiguous", () => {
205
+ this.state.connection
206
+ .prepare(`UPDATE effects SET status = ?, updated_at = ?, settled_at = ?
207
+ WHERE effect_id = ? AND status = 'applying'`)
208
+ .run(status, now, status === "ambiguous" ? now : null, row.effectId);
209
+ this.state.connection
210
+ .prepare(`UPDATE effect_attempts SET finished_at = ?, outcome = 'interrupted'
211
+ WHERE effect_id = ? AND attempt_number = ? AND finished_at IS NULL`)
212
+ .run(now, row.effectId, row.attemptCount);
213
+ }, { now, payload: { runId, recovery } });
214
+ }
215
+ return ambiguous ? "ambiguous" : "safe";
216
+ }));
217
+ }
218
+ beginEffectAttempt(run, effectId, attemptNumber, authority) {
219
+ const now = Date.now();
220
+ const row = this.state.connection
221
+ .prepare("SELECT resource_id AS resourceId FROM effects WHERE effect_id = ?")
222
+ .get(effectId);
223
+ /* istanbul ignore if -- beginEffectAttempt is called only for a persisted effect */
224
+ if (!isResourceIdRow(row))
225
+ throw new Error("Managed effect resource is missing");
226
+ const revision = this.requireResourceRevision(row.resourceId);
227
+ this.mutations.mutate({
228
+ resourceId: row.resourceId,
229
+ operation: "effect.retry",
230
+ actor: authority?.actor ?? { type: "system" },
231
+ expectedRevision: revision,
232
+ }, "effect.retry_started", () => {
233
+ this.state.connection
234
+ .prepare(`UPDATE effects SET status = 'applying', attempt_count = ?, updated_at = ?,
235
+ settled_at = NULL, error_hash = NULL
236
+ WHERE effect_id = ? AND status = 'pending'`)
237
+ .run(attemptNumber, now, effectId);
238
+ this.insertEffectAttempt(effectId, attemptNumber, authority, now);
239
+ }, { now, payload: { runId: run.runId, attemptNumber } });
240
+ return { effectId, attemptNumber, disposition: "execute" };
241
+ }
242
+ insertEffectAttempt(effectId, attemptNumber, authority, now) {
243
+ this.state.connection
244
+ .prepare(`INSERT INTO effect_attempts(
245
+ effect_id, attempt_number, owner_id, lease_generation, started_at
246
+ ) VALUES (?, ?, ?, ?, ?)`)
247
+ .run(effectId, attemptNumber, authority?.ownerId ?? "system", authority?.generation ?? 1, now);
248
+ }
58
249
  async initializeRun(workflow, state, options = {}) {
250
+ return await this.initializeRunFromSnapshot(createDefinitionSnapshot(workflow), workflow.name, state, options);
251
+ }
252
+ async initializeRunFromSnapshot(snapshot, workflowName, state, options = {}) {
59
253
  assertValidRunId(state.runId);
60
254
  if (!this.contexts.has(state.runId)) {
61
255
  const reserved = this.readRunRow(state.runId);
@@ -68,7 +262,6 @@ export class WorkflowRunStore {
68
262
  let acceptedRevision = 1;
69
263
  const now = Date.now();
70
264
  const at = new Date(now).toISOString();
71
- const snapshot = createDefinitionSnapshot(workflow);
72
265
  const definitionJson = canonicalJson(snapshot);
73
266
  const definitionDigest = createHash("sha256").update(definitionJson).digest();
74
267
  const source = sourceForState(state, definitionDigest);
@@ -89,14 +282,23 @@ export class WorkflowRunStore {
89
282
  acceptedRevision = revision;
90
283
  state.traceSeq = revision;
91
284
  state.updatedAt = at;
92
- insertRunEvent(this.state, reserved.resourceId, revision, at, {
285
+ const traceEvent = {
286
+ seq: revision,
287
+ at,
288
+ runId: state.runId,
93
289
  scope: "run",
94
290
  type: "run_initialized",
95
- payload: { workflowName: workflow.name },
96
- });
291
+ payload: { workflowName },
292
+ };
293
+ insertRunEvent(this.state, reserved.resourceId, revision, at, traceEvent);
97
294
  this.persistRunState(reserved, state, revision, now);
295
+ initializeViewerRun(this.state, state.runId, now);
98
296
  this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
297
+ if (state.parentRunId !== undefined) {
298
+ recordViewerDeltas(this.state, state.parentRunId, this.viewerInspectorTargets(state.parentRunId), now);
299
+ }
99
300
  this.syncNodeAttempts(state, snapshot, now);
301
+ recordViewerDeltas(this.state, state.runId, [...runViewerTargets(state, traceEvent), ...this.viewerInspectorTargets(state.runId)], now);
100
302
  this.syncContinuationIdentity(state);
101
303
  context.revision = revision;
102
304
  return;
@@ -107,7 +309,7 @@ export class WorkflowRunStore {
107
309
  definition_digest, workflow_name, definition_hash, created_at
108
310
  ) VALUES (?, ?, ?, ?)
109
311
  ON CONFLICT(definition_digest) DO NOTHING`)
110
- .run(definitionDigest, workflow.name, definitionHash, now);
312
+ .run(definitionDigest, workflowName, definitionHash, now);
111
313
  this.state.connection
112
314
  .prepare(`INSERT INTO resources(
113
315
  resource_id, resource_type, aggregate_key, revision, created_at, updated_at
@@ -130,14 +332,18 @@ export class WorkflowRunStore {
130
332
  created_at, updated_at, finished_at
131
333
  ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
132
334
  .run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName, launchOptionsHash, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, finalOutputHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
335
+ initializeViewerRun(this.state, state.runId, now);
133
336
  this.insertRunSources(state.runId, source, state.workflowSources ?? []);
134
337
  this.syncContinuationIdentity(state);
135
338
  insertRunEvent(this.state, resourceId, 1, at, {
136
339
  scope: "run",
137
340
  type: "run_created",
138
- payload: { workflowName: workflow.name },
341
+ payload: { workflowName },
139
342
  });
140
343
  this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
344
+ if (state.parentRunId !== undefined) {
345
+ recordViewerDeltas(this.state, state.parentRunId, this.viewerInspectorTargets(state.parentRunId), now);
346
+ }
141
347
  this.syncNodeAttempts(state, snapshot, now);
142
348
  });
143
349
  this.contexts.set(state.runId, { revision: acceptedRevision, lock: Promise.resolve() });
@@ -165,6 +371,7 @@ export class WorkflowRunStore {
165
371
  initial_hash, current_hash, created_at, updated_at
166
372
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
167
373
  .run(scopeId, resourceId, originRunId, options.runId, options.mountPath, options.invocation, settingsHash, settingsHash, now, now);
374
+ recordViewerDeltas(this.state, options.runId, this.viewerInspectorTargets(options.runId), now);
168
375
  return this.settingsScopeRecord(this.requireSettingsScopeRow(scopeId));
169
376
  });
170
377
  }
@@ -295,6 +502,7 @@ export class WorkflowRunStore {
295
502
  database.connection
296
503
  .prepare(`UPDATE workflow_settings SET current_hash = ?, updated_at = ? WHERE scope_id = ?`)
297
504
  .run(savedAfterHash, now, request.scopeId);
505
+ recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
298
506
  return changeId;
299
507
  }, {
300
508
  payload: {
@@ -371,6 +579,7 @@ export class WorkflowRunStore {
371
579
  created_at, updated_at
372
580
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
373
581
  .run(followUpId, resourceId, queue.resourceId, request.runId, request.requestId, order, request.targetSessionId, request.actor.type, request.actor.id ?? null, request.source, promptHash, now, now);
582
+ recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
374
583
  return followUpId;
375
584
  }, { payload: { runId: request.runId, requestId: request.requestId } });
376
585
  return {
@@ -416,6 +625,7 @@ export class WorkflowRunStore {
416
625
  if (update.changes !== 1) {
417
626
  throw new StaleResourceError("Workflow follow-up changed before removal");
418
627
  }
628
+ recordViewerDeltas(this.state, request.runId, this.viewerInspectorTargets(request.runId), now);
419
629
  }, { payload: { followUpId: request.followUpId, source: request.source } });
420
630
  return this.followUpRecord(this.requireFollowUpRow(request.followUpId));
421
631
  }
@@ -503,6 +713,7 @@ export class WorkflowRunStore {
503
713
  .prepare(`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
504
714
  WHERE run_id = ? AND status = 'pending_presentation'`)
505
715
  .run(now, options.runId);
716
+ recordViewerDeltas(this.state, options.runId, this.viewerInspectorTargets(options.runId), now);
506
717
  }, { payload: { runId: options.runId, state: options.state } });
507
718
  }
508
719
  catch (error) {
@@ -569,6 +780,7 @@ export class WorkflowRunStore {
569
780
  if (update.changes !== 1) {
570
781
  throw new StaleResourceError("Workflow follow-up changed before delivery completed");
571
782
  }
783
+ recordViewerDeltas(this.state, row.runId, this.viewerInspectorTargets(row.runId), now);
572
784
  }, { payload: { followUpId, sessionEntryId } });
573
785
  return this.followUpRecord(this.requireFollowUpRow(followUpId));
574
786
  }
@@ -599,14 +811,19 @@ export class WorkflowRunStore {
599
811
  SET status = 'interrupted', updated_at = ?
600
812
  WHERE run_id = ? AND status IN ('pending', 'running', 'waiting')`)
601
813
  .run(now, runId);
602
- insertRunEvent(this.state, row.resourceId, nextRevision, at, {
814
+ const traceEvent = {
815
+ seq: nextRevision,
816
+ at,
817
+ runId,
603
818
  scope: "run",
604
819
  type: "run_resume_prepared",
605
820
  payload: {},
606
- });
821
+ };
822
+ insertRunEvent(this.state, row.resourceId, nextRevision, at, traceEvent);
607
823
  loaded.state.traceSeq = nextRevision;
608
824
  loaded.state.updatedAt = at;
609
825
  this.persistRunState(row, loaded.state, nextRevision, now);
826
+ recordViewerDeltas(this.state, runId, runViewerTargets(loaded.state, traceEvent), now);
610
827
  context.revision = nextRevision;
611
828
  });
612
829
  const prepared = this.readRun(runId, { includeTrace: true });
@@ -625,6 +842,7 @@ export class WorkflowRunStore {
625
842
  delete loaded.state.currentNode;
626
843
  delete loaded.state.currentAttemptId;
627
844
  delete loaded.state.currentNodeStartedAt;
845
+ delete loaded.state.currentNodeDeadlineAt;
628
846
  delete loaded.state.currentSettingsScopeId;
629
847
  delete loaded.state.currentSettingsChangeNumber;
630
848
  delete loaded.state.currentSettingsHash;
@@ -638,68 +856,72 @@ export class WorkflowRunStore {
638
856
  return this.readRun(runId, { includeTrace: true });
639
857
  }
640
858
  async publishUpdate(runId, state, nodeId, attemptId, update, options = {}) {
641
- return await this.withRunLock(runId, async () => {
642
- if (options.signal?.aborted === true) {
643
- throw options.signal.reason ?? new Error("workflow update attempt is no longer active");
644
- }
645
- const exists = (state.updates ?? []).some((record) => record.type === update.type && record.key === update.key);
646
- if (!exists && (state.updates?.length ?? 0) >= MAX_CURRENT_UPDATES) {
647
- throw new Error(`workflow run supports at most ${MAX_CURRENT_UPDATES} current updates`);
648
- }
649
- const data = structuredClone(update.data);
650
- const updateId = createUpdateId();
651
- let acceptedEvent;
652
- let acceptedRecord;
653
- this.state.transaction(() => {
654
- const context = this.contextFor(runId);
655
- const run = this.requireRunRow(runId);
656
- this.assertWriteAuthority(run, context.revision);
657
- const revision = context.revision + 1;
658
- const at = new Date().toISOString();
659
- const event = {
660
- seq: revision,
661
- at,
662
- runId,
663
- scope: "node",
664
- type: "update_published",
665
- nodeId,
666
- attemptId,
667
- payload: { updateId, type: update.type, key: update.key, data },
668
- };
669
- const record = {
670
- updateId,
671
- seq: revision,
672
- at,
673
- runId,
674
- nodeId,
675
- attemptId,
676
- type: update.type,
677
- key: update.key,
678
- data,
679
- };
680
- state.updates = updateProjection(state.updates, record);
681
- state.traceSeq = revision;
682
- state.updatedAt = at;
683
- this.ensureAttempt(state, nodeId, attemptId, Date.now());
684
- const dataHash = this.state.putJson(data);
685
- const nextUpdateSeq = this.nextUpdateSequence(attemptId);
686
- this.state.connection
687
- .prepare(`INSERT INTO workflow_updates(
859
+ return await this.withRunLock(runId, async () => this.publishUpdateSynchronous(runId, state, nodeId, attemptId, update, options));
860
+ }
861
+ /** Host-only synchronous form for an already serialized command transaction. */
862
+ publishUpdateSynchronous(runId, state, nodeId, attemptId, update, options = {}) {
863
+ if (options.signal?.aborted === true) {
864
+ throw options.signal.reason ?? new Error("workflow update attempt is no longer active");
865
+ }
866
+ const exists = (state.updates ?? []).some((record) => record.type === update.type && record.key === update.key);
867
+ if (!exists && (state.updates?.length ?? 0) >= MAX_CURRENT_UPDATES) {
868
+ throw new Error(`workflow run supports at most ${MAX_CURRENT_UPDATES} current updates`);
869
+ }
870
+ const data = structuredClone(update.data);
871
+ const updateId = createUpdateId();
872
+ let acceptedEvent;
873
+ let acceptedRecord;
874
+ this.state.transaction(() => {
875
+ const context = this.contextFor(runId);
876
+ const run = this.requireRunRow(runId);
877
+ this.assertWriteAuthority(run, context.revision);
878
+ const revision = context.revision + 1;
879
+ const at = new Date().toISOString();
880
+ const event = {
881
+ seq: revision,
882
+ at,
883
+ runId,
884
+ scope: "node",
885
+ type: "update_published",
886
+ nodeId,
887
+ attemptId,
888
+ payload: { updateId, type: update.type, key: update.key, data },
889
+ };
890
+ const record = {
891
+ updateId,
892
+ seq: revision,
893
+ at,
894
+ runId,
895
+ nodeId,
896
+ attemptId,
897
+ type: update.type,
898
+ key: update.key,
899
+ data,
900
+ };
901
+ state.updates = updateProjection(state.updates, record);
902
+ state.traceSeq = revision;
903
+ state.updatedAt = at;
904
+ this.ensureAttempt(state, nodeId, attemptId, Date.now());
905
+ const dataHash = this.state.putJson(data);
906
+ const nextUpdateSeq = this.nextUpdateSequence(attemptId);
907
+ this.state.connection
908
+ .prepare(`INSERT INTO workflow_updates(
688
909
  update_id, attempt_id, update_seq, run_revision,
689
910
  update_type, update_key, data_hash, recorded_at
690
911
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
691
- .run(updateId, attemptId, nextUpdateSeq, revision, update.type, update.key, dataHash, Date.now());
692
- insertRunEvent(this.state, run.resourceId, revision, at, event);
693
- this.persistRunState(run, state, revision, Date.now());
694
- context.revision = revision;
695
- acceptedEvent = event;
696
- acceptedRecord = record;
697
- });
698
- if (acceptedEvent === undefined || acceptedRecord === undefined) {
699
- throw new Error("Workflow update transaction did not produce a result");
700
- }
701
- return { event: acceptedEvent, record: acceptedRecord };
912
+ .run(updateId, attemptId, nextUpdateSeq, revision, update.type, update.key, dataHash, Date.now());
913
+ insertRunEvent(this.state, run.resourceId, revision, at, event);
914
+ const committedAt = Date.now();
915
+ this.persistRunState(run, state, revision, committedAt);
916
+ recordViewerDeltas(this.state, runId, runViewerTargets(state, event), committedAt);
917
+ context.revision = revision;
918
+ acceptedEvent = event;
919
+ acceptedRecord = record;
702
920
  });
921
+ if (acceptedEvent === undefined || acceptedRecord === undefined) {
922
+ throw new Error("Workflow update transaction did not produce a result");
923
+ }
924
+ return { event: acceptedEvent, record: acceptedRecord };
703
925
  }
704
926
  async writeSnapshot(runId, state, event) {
705
927
  return await this.withRunLock(runId, async () => {
@@ -720,6 +942,14 @@ export class WorkflowRunStore {
720
942
  this.persistRunState(run, state, revision, now);
721
943
  this.transitionFollowUpsForRunState(state, event, now);
722
944
  this.syncNodeAttempts(state, snapshot, now);
945
+ recordViewerDeltas(this.state, runId, runViewerTargets(state, traceEvent), now);
946
+ this.snapshotLifecycle?.({
947
+ runId,
948
+ state,
949
+ event: traceEvent,
950
+ database: this.state,
951
+ now,
952
+ });
723
953
  context.revision = revision;
724
954
  accepted = traceEvent;
725
955
  });
@@ -747,6 +977,7 @@ export class WorkflowRunStore {
747
977
  const segmentId = segmentIdFor(runId, attemptId);
748
978
  if (this.segmentRow(segmentId) !== undefined)
749
979
  return;
980
+ const hasExistingSession = this.segmentRows(runId).length > 0;
750
981
  this.state.transaction(() => {
751
982
  const run = this.requireRunRow(runId);
752
983
  const context = this.contextFor(runId);
@@ -781,7 +1012,7 @@ export class WorkflowRunStore {
781
1012
  .run(binding.piSessionId, now, runId);
782
1013
  const revision = context.revision + 1;
783
1014
  const at = new Date(now).toISOString();
784
- insertRunEvent(this.state, run.resourceId, revision, at, {
1015
+ const traceEvent = {
785
1016
  seq: revision,
786
1017
  at,
787
1018
  runId,
@@ -791,11 +1022,34 @@ export class WorkflowRunStore {
791
1022
  piSessionId: binding.piSessionId,
792
1023
  ...(attemptId !== undefined ? { captureAttemptId: attemptId } : {}),
793
1024
  },
794
- });
1025
+ };
1026
+ insertRunEvent(this.state, run.resourceId, revision, at, traceEvent);
795
1027
  const current = this.readRunState(run, this.readDefinition(run.definitionHash));
796
1028
  current.traceSeq = revision;
797
1029
  current.updatedAt = at;
798
1030
  this.persistRunState(run, current, revision, now);
1031
+ const targets = runViewerTargets(current, traceEvent);
1032
+ targets[0]?.patch?.push(hasExistingSession
1033
+ ? {
1034
+ op: "add",
1035
+ path: "/session/binding",
1036
+ value: parseJson(canonicalJson(binding)),
1037
+ }
1038
+ : {
1039
+ op: "add",
1040
+ path: "/session",
1041
+ value: parseJson(canonicalJson({
1042
+ binding,
1043
+ presentationRevision: 0,
1044
+ entryPage: { presentationRevision: 0, start: 0, total: 0, items: [] },
1045
+ eventPage: { presentationRevision: 0, start: 0, total: 0, items: [] },
1046
+ eventsMalformed: false,
1047
+ eventsTornTail: false,
1048
+ capture: null,
1049
+ replayCheckpoint: null,
1050
+ })),
1051
+ });
1052
+ recordViewerDeltas(this.state, runId, targets, now);
799
1053
  context.revision = revision;
800
1054
  });
801
1055
  });
@@ -806,18 +1060,51 @@ export class WorkflowRunStore {
806
1060
  this.assertSessionWriteAuthority(runId);
807
1061
  const revision = this.requireResourceRevision(segmentResourceId(segment));
808
1062
  const sequence = segment.entryCount + 1;
1063
+ const runSequenceRow = this.state.connection
1064
+ .prepare(`SELECT COALESCE(MAX(run_seq), 0) + 1 AS count
1065
+ FROM session_entries WHERE run_id = ?`)
1066
+ .get(runId);
1067
+ if (!isCountRow(runSequenceRow)) {
1068
+ throw new Error("Workflow session entry sequence is unavailable");
1069
+ }
1070
+ const runSequence = runSequenceRow.count;
809
1071
  const now = Date.now();
810
1072
  const entryHash = this.state.putJson(entry, now);
811
1073
  const entryId = typeof entry.id === "string" ? entry.id : `entry-${sequence}`;
812
1074
  this.state.connection
813
- .prepare(`INSERT INTO session_entries(segment_id, entry_seq, entry_id, entry_hash, recorded_at)
814
- VALUES (?, ?, ?, ?, ?)`)
815
- .run(segment.segmentId, sequence, entryId, entryHash, now);
1075
+ .prepare(`INSERT INTO session_entries(
1076
+ segment_id, run_id, entry_seq, run_seq, entry_id, entry_hash, recorded_at
1077
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`)
1078
+ .run(segment.segmentId, runId, sequence, runSequence, entryId, entryHash, now);
816
1079
  this.state.connection
817
1080
  .prepare("UPDATE session_segments SET entry_count = ? WHERE segment_id = ?")
818
1081
  .run(sequence, segment.segmentId);
819
1082
  this.bumpResource(segmentResourceId(segment), revision, now);
820
1083
  insertGenericEvent(this.state, segmentResourceId(segment), revision + 1, "session.entry_appended", "session", segment.sessionId, null, now);
1084
+ recordViewerDeltas(this.state, runId, [
1085
+ {
1086
+ targetType: "conversation",
1087
+ targetKey: "entries:tail",
1088
+ patch: viewerTailPatch(runSequence - 1, [
1089
+ parseJson(canonicalJson({
1090
+ seq: runSequence,
1091
+ at: new Date(now).toISOString(),
1092
+ entry,
1093
+ })),
1094
+ ]),
1095
+ },
1096
+ {
1097
+ targetType: "conversation",
1098
+ targetKey: "capture",
1099
+ patch: [
1100
+ {
1101
+ op: "replace",
1102
+ path: "/capture/entryCount",
1103
+ value: runSequence,
1104
+ },
1105
+ ],
1106
+ },
1107
+ ], now);
821
1108
  return sequence;
822
1109
  });
823
1110
  }
@@ -831,6 +1118,16 @@ export class WorkflowRunStore {
831
1118
  if (current.status !== "recording")
832
1119
  throw new Error("Session event capture has stopped");
833
1120
  let expected = current.eventCount + 1;
1121
+ const runSequenceRow = this.state.connection
1122
+ .prepare(`SELECT COALESCE(MAX(run_seq), 0) + 1 AS count
1123
+ FROM session_events WHERE run_id = ?`)
1124
+ .get(runId);
1125
+ if (!isCountRow(runSequenceRow)) {
1126
+ throw new Error("Workflow session event sequence is unavailable");
1127
+ }
1128
+ let runSequence = runSequenceRow.count;
1129
+ const firstRunSequence = runSequence;
1130
+ const projectedRecords = [];
834
1131
  for (const record of records) {
835
1132
  validateSessionEventRecord(record);
836
1133
  if (record.seq !== expected) {
@@ -842,17 +1139,166 @@ export class WorkflowRunStore {
842
1139
  const payloadHash = this.state.putJson(record.payload, Date.parse(record.at));
843
1140
  this.state.connection
844
1141
  .prepare(`INSERT INTO session_events(
845
- segment_id, event_seq, event_type, node_id, attempt_id,
1142
+ segment_id, run_id, event_seq, run_seq, event_type, node_id, attempt_id,
846
1143
  turn_id, message_id, tool_call_id, payload_hash, recorded_at
847
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
848
- .run(current.segmentId, record.seq, record.type, record.nodeId, record.attemptId, record.turnId ?? null, record.messageId ?? null, record.toolCallId ?? null, payloadHash, Date.parse(record.at));
1144
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1145
+ .run(current.segmentId, runId, record.seq, runSequence, record.type, record.nodeId, record.attemptId, record.turnId ?? null, record.messageId ?? null, record.toolCallId ?? null, payloadHash, Date.parse(record.at));
1146
+ const stepIndex = this.state.connection
1147
+ .prepare(`SELECT step_index AS stepIndex
1148
+ FROM run_steps WHERE run_id = ? AND attempt_id = ?`)
1149
+ .get(runId, record.attemptId);
1150
+ projectedRecords.push({
1151
+ ...record,
1152
+ seq: runSequence,
1153
+ ...(isStepIndexRow(stepIndex) ? { stepIndex: stepIndex.stepIndex } : {}),
1154
+ });
849
1155
  expected += 1;
1156
+ runSequence += 1;
850
1157
  }
851
1158
  this.state.connection
852
1159
  .prepare("UPDATE session_segments SET event_count = ? WHERE segment_id = ?")
853
1160
  .run(expected - 1, current.segmentId);
1161
+ const recordedAt = Date.now();
1162
+ this.recordSessionReplayCheckpoints(runId, firstRunSequence, projectedRecords, recordedAt);
1163
+ const projectedValues = projectedRecords.map((record) => parseJson(canonicalJson(record)));
1164
+ const nextEventTotal = firstRunSequence - 1 + projectedValues.length;
1165
+ const previousBlock = Math.floor(Math.max(0, firstRunSequence - 2) / VIEWER_PAGE_SIZE);
1166
+ const nextBlock = Math.floor(Math.max(0, nextEventTotal - 1) / VIEWER_PAGE_SIZE);
1167
+ const crossesPageBoundary = previousBlock !== nextBlock;
1168
+ recordViewerDeltas(this.state, runId, [
1169
+ {
1170
+ targetType: "timeline",
1171
+ targetKey: crossesPageBoundary ? "session:reload" : "session:tail",
1172
+ patch: crossesPageBoundary
1173
+ ? [{ op: "replace", path: "/total", value: nextEventTotal }]
1174
+ : [
1175
+ { op: "append", path: "/items", value: projectedValues },
1176
+ { op: "replace", path: "/total", value: nextEventTotal },
1177
+ ],
1178
+ },
1179
+ {
1180
+ targetType: "conversation",
1181
+ targetKey: "capture",
1182
+ patch: [
1183
+ {
1184
+ op: "replace",
1185
+ path: "/capture/eventCount",
1186
+ value: nextEventTotal,
1187
+ },
1188
+ {
1189
+ op: "replace",
1190
+ path: "/capture/lastEventSeq",
1191
+ value: nextEventTotal,
1192
+ },
1193
+ ],
1194
+ },
1195
+ ], recordedAt);
854
1196
  });
855
1197
  }
1198
+ recordSessionReplayCheckpoints(runId, firstRunSequence, newRecords, now) {
1199
+ const row = this.state.connection
1200
+ .prepare(`SELECT event_seq AS eventSeq, state_hash AS stateHash
1201
+ FROM viewer_session_checkpoints
1202
+ WHERE run_id = ? ORDER BY event_seq DESC LIMIT 1`)
1203
+ .get(runId);
1204
+ let checkpoint = reduceSessionEvents([], [], 0);
1205
+ if (isViewerSessionCheckpointRow(row)) {
1206
+ const saved = this.state.readJson(row.stateHash);
1207
+ if (!isTemporalSessionState(saved) || saved.throughSeq !== row.eventSeq) {
1208
+ throw new Error(`Viewer session checkpoint is invalid for ${runId}`);
1209
+ }
1210
+ checkpoint = saved;
1211
+ }
1212
+ let pending = [
1213
+ ...this.sessionEventRecords(runId, checkpoint.throughSeq, firstRunSequence),
1214
+ ...newRecords,
1215
+ ];
1216
+ const lastSequence = pending.at(-1)?.seq ?? checkpoint.throughSeq;
1217
+ let boundary = (Math.floor(checkpoint.throughSeq / VIEWER_PAGE_SIZE) + 1) * VIEWER_PAGE_SIZE;
1218
+ while (boundary <= lastSequence) {
1219
+ const page = pending.filter((event) => event.seq <= boundary);
1220
+ checkpoint = reduceSessionEventsFromCheckpoint([], page, boundary, checkpoint);
1221
+ checkpoint = boundedTemporalCheckpoint(checkpoint);
1222
+ const stateHash = this.state.putJson(checkpoint, now);
1223
+ this.state.connection
1224
+ .prepare(`INSERT INTO viewer_session_checkpoints(run_id, event_seq, state_hash, recorded_at)
1225
+ VALUES (?, ?, ?, ?)`)
1226
+ .run(runId, boundary, stateHash, now);
1227
+ pending = pending.filter((event) => event.seq > boundary);
1228
+ boundary += VIEWER_PAGE_SIZE;
1229
+ }
1230
+ }
1231
+ sessionEventRecords(runId, afterSequence, beforeSequence) {
1232
+ return this.state.connection
1233
+ .prepare(`SELECT run_seq AS runSeq, event_type AS eventType, node_id AS nodeId,
1234
+ attempt_id AS attemptId, turn_id AS turnId, message_id AS messageId,
1235
+ tool_call_id AS toolCallId, payload_hash AS payloadHash,
1236
+ recorded_at AS recordedAt
1237
+ FROM session_events
1238
+ WHERE run_id = ? AND run_seq > ? AND run_seq < ?
1239
+ ORDER BY run_seq`)
1240
+ .all(runId, afterSequence, beforeSequence)
1241
+ .map((value) => {
1242
+ if (!isViewerSessionEventRow(value)) {
1243
+ throw new Error(`Viewer session event row is invalid for ${runId}`);
1244
+ }
1245
+ const payload = this.state.readJson(value.payloadHash);
1246
+ if (!isRecord(payload)) {
1247
+ throw new Error(`Viewer session event payload is invalid for ${runId}`);
1248
+ }
1249
+ return {
1250
+ seq: value.runSeq,
1251
+ at: new Date(value.recordedAt).toISOString(),
1252
+ nodeId: value.nodeId,
1253
+ attemptId: value.attemptId,
1254
+ type: assertSessionEventType(value.eventType),
1255
+ payload,
1256
+ ...(value.turnId === null ? {} : { turnId: value.turnId }),
1257
+ ...(value.messageId === null ? {} : { messageId: value.messageId }),
1258
+ ...(value.toolCallId === null ? {} : { toolCallId: value.toolCallId }),
1259
+ };
1260
+ });
1261
+ }
1262
+ viewerSessionCapture(runId) {
1263
+ const segments = this.segmentRows(runId);
1264
+ let status = "complete";
1265
+ let failure;
1266
+ let entryCount = 0;
1267
+ let eventCount = 0;
1268
+ for (const segment of segments) {
1269
+ entryCount += segment.entryCount;
1270
+ eventCount += segment.eventCount;
1271
+ if (segment.status === "failed") {
1272
+ status = "failed";
1273
+ if (failure === undefined && segment.failureHash !== null) {
1274
+ const value = this.state.readJson(segment.failureHash);
1275
+ if (!isRecord(value) ||
1276
+ typeof value.failedAt !== "string" ||
1277
+ typeof value.code !== "string" ||
1278
+ typeof value.message !== "string") {
1279
+ throw new Error(`Session capture failure is invalid for ${runId}`);
1280
+ }
1281
+ failure = {
1282
+ failedAt: value.failedAt,
1283
+ code: value.code,
1284
+ message: value.message,
1285
+ };
1286
+ }
1287
+ }
1288
+ else if (segment.status === "recording" && status !== "failed") {
1289
+ status = "recording";
1290
+ }
1291
+ }
1292
+ return {
1293
+ schema: SESSION_CAPTURE_SCHEMA,
1294
+ eventSchema: SESSION_EVENT_SCHEMA,
1295
+ status,
1296
+ eventCount,
1297
+ entryCount,
1298
+ lastEventSeq: eventCount,
1299
+ ...(failure === undefined ? {} : { failure }),
1300
+ };
1301
+ }
856
1302
  async writeSessionCapture(runId, capture, attemptId) {
857
1303
  validateSessionCapture(capture);
858
1304
  const segment = this.requireSegment(segmentIdFor(runId, attemptId));
@@ -875,6 +1321,20 @@ export class WorkflowRunStore {
875
1321
  this.bumpResource(resourceId, revision, now);
876
1322
  const payloadHash = this.state.putJson(capture, now);
877
1323
  insertGenericEvent(this.state, resourceId, revision + 1, "session.capture_updated", "session", segment.sessionId, payloadHash, now);
1324
+ const viewerCapture = this.viewerSessionCapture(runId);
1325
+ recordViewerDeltas(this.state, runId, [
1326
+ {
1327
+ targetType: "conversation",
1328
+ targetKey: "capture",
1329
+ patch: [
1330
+ {
1331
+ op: "add",
1332
+ path: "/capture",
1333
+ value: parseJson(canonicalJson(viewerCapture)),
1334
+ },
1335
+ ],
1336
+ },
1337
+ ], now);
878
1338
  });
879
1339
  }
880
1340
  async sessionCounts(runId, attemptId) {
@@ -982,10 +1442,6 @@ export class WorkflowRunStore {
982
1442
  throw new Error("Resource revision conflict");
983
1443
  }
984
1444
  assertWriteAuthority(run, expectedRevision) {
985
- const actualRevision = this.requireResourceRevision(run.resourceId);
986
- if (actualRevision !== expectedRevision) {
987
- throw new Error(`Workflow run revision conflict: expected ${expectedRevision}, got ${actualRevision}`);
988
- }
989
1445
  const lease = this.state.connection
990
1446
  .prepare(`SELECT generation, owner_type AS ownerType, owner_id AS ownerId,
991
1447
  token_hash AS tokenHash, expires_at AS expiresAt
@@ -994,18 +1450,41 @@ export class WorkflowRunStore {
994
1450
  /* istanbul ignore if -- exact schema and internal query shape */
995
1451
  if (!isLeaseAuthorityRow(lease))
996
1452
  throw new Error("Workflow run lease is missing");
997
- if (lease.ownerId === null)
998
- return;
999
- const authority = this.authorityProvider?.(run.runId);
1000
- if (authority === undefined ||
1001
- authority.ownerType !== lease.ownerType ||
1002
- authority.ownerId !== lease.ownerId ||
1003
- authority.generation !== lease.generation ||
1004
- lease.tokenHash === null ||
1005
- !lease.tokenHash.equals(tokenHash(authority.token)) ||
1006
- lease.expiresAt === null ||
1007
- lease.expiresAt <= Date.now()) {
1008
- throw new Error("Workflow run write rejected because ownership changed");
1453
+ if (lease.ownerId !== null || this.authorityProvider !== undefined) {
1454
+ const authority = this.authorityProvider?.(run.runId);
1455
+ if (authority === undefined) {
1456
+ throw new ClaimLostError(run.runId, "missingAuthority");
1457
+ }
1458
+ const now = Date.now();
1459
+ if (lease.expiresAt === null || lease.expiresAt <= now) {
1460
+ throw new ClaimLostError(run.runId, "expired");
1461
+ }
1462
+ if (authority.ownerType !== lease.ownerType || authority.ownerId !== lease.ownerId) {
1463
+ throw new ClaimLostError(run.runId, "ownerChanged");
1464
+ }
1465
+ if (lease.tokenHash === null || !lease.tokenHash.equals(tokenHash(authority.token))) {
1466
+ throw new ClaimLostError(run.runId, "tokenChanged");
1467
+ }
1468
+ if (authority.generation !== lease.generation) {
1469
+ throw new ClaimLostError(run.runId, "generationChanged");
1470
+ }
1471
+ const leaseMs = authority.leaseMs ?? 30_000;
1472
+ if (!Number.isInteger(leaseMs) || leaseMs <= 0) {
1473
+ throw new Error("Workflow run authority lease duration must be a positive integer");
1474
+ }
1475
+ const renewal = this.state.connection
1476
+ .prepare(`UPDATE leases SET heartbeat_at = ?, expires_at = ?
1477
+ WHERE resource_id = ? AND owner_type = ? AND owner_id = ?
1478
+ AND token_hash = ? AND generation = ? AND expires_at > ?`)
1479
+ .run(now, now + leaseMs, run.resourceId, authority.ownerType, authority.ownerId, tokenHash(authority.token), authority.generation, now);
1480
+ /* istanbul ignore if -- BEGIN IMMEDIATE keeps the checked lease stable */
1481
+ if (renewal.changes !== 1) {
1482
+ throw new ClaimLostError(run.runId, "expired");
1483
+ }
1484
+ }
1485
+ const actualRevision = this.requireResourceRevision(run.resourceId);
1486
+ if (actualRevision !== expectedRevision) {
1487
+ throw new Error(`Workflow run revision conflict: expected ${expectedRevision}, got ${actualRevision}`);
1009
1488
  }
1010
1489
  }
1011
1490
  persistRunState(run, state, expectedRevision, now) {
@@ -1058,6 +1537,72 @@ export class WorkflowRunStore {
1058
1537
  .run(effectId, effectResourceId, runResourceId, sourceRevision, effectType, idempotencyKey, payloadHash, now, now);
1059
1538
  insertGenericEvent(this.state, effectResourceId, 1, "effect.created", "system", null, payloadHash, now);
1060
1539
  }
1540
+ viewerInspectorTargets(runId) {
1541
+ const settings = this.state.connection
1542
+ .prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
1543
+ s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
1544
+ s.mount_path AS mountPath, s.invocation,
1545
+ s.current_hash AS currentHash, r.revision,
1546
+ s.created_at AS createdAt, s.updated_at AS updatedAt
1547
+ FROM workflow_settings s
1548
+ JOIN resources r ON r.resource_id = s.resource_id
1549
+ WHERE s.active_run_id = ?
1550
+ ORDER BY s.mount_path, s.invocation`)
1551
+ .all(runId)
1552
+ .filter(isSettingsScopeRow)
1553
+ .map((row) => ({
1554
+ scopeId: row.scopeId,
1555
+ mountPath: row.mountPath,
1556
+ invocation: row.invocation,
1557
+ changeNumber: row.revision,
1558
+ settingsHash: row.currentHash.toString("hex"),
1559
+ }));
1560
+ const settingsStart = Math.max(0, settings.length - VIEWER_PAGE_SIZE);
1561
+ const followUps = this.state.connection
1562
+ .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
1563
+ .all(runId)
1564
+ .filter(isFollowUpRow)
1565
+ .map((row) => ({
1566
+ followUpId: row.followUpId,
1567
+ order: row.orderNumber,
1568
+ state: row.status,
1569
+ source: row.sourceType,
1570
+ sessionEntryId: row.sessionEntryId,
1571
+ }));
1572
+ const followUpStart = Math.max(0, followUps.length - VIEWER_PAGE_SIZE);
1573
+ const queue = this.requireFollowUpQueueRow(runId);
1574
+ return [
1575
+ {
1576
+ targetType: "inspector",
1577
+ targetKey: "settings",
1578
+ patch: [
1579
+ {
1580
+ op: "replace",
1581
+ path: "/settingsScopes",
1582
+ value: parseJson(canonicalJson(settings.slice(settingsStart))),
1583
+ },
1584
+ { op: "replace", path: "/settingsStart", value: settingsStart },
1585
+ { op: "replace", path: "/settingsTotal", value: settings.length },
1586
+ ],
1587
+ },
1588
+ {
1589
+ targetType: "inspector",
1590
+ targetKey: "follow-ups",
1591
+ patch: [
1592
+ {
1593
+ op: "replace",
1594
+ path: "/followUpQueue",
1595
+ value: parseJson(canonicalJson({
1596
+ presentationState: queue.presentationState,
1597
+ items: followUps.slice(followUpStart),
1598
+ })),
1599
+ },
1600
+ { op: "replace", path: "/followUpStart", value: followUpStart },
1601
+ { op: "replace", path: "/followUpTotal", value: followUps.length },
1602
+ ],
1603
+ },
1604
+ ];
1605
+ }
1061
1606
  initializeRunSettingsAndFollowUps(state, initialSettings, now) {
1062
1607
  const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
1063
1608
  if (state.parentRunId !== undefined) {
@@ -1424,6 +1969,17 @@ export class WorkflowRunStore {
1424
1969
  }
1425
1970
  if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
1426
1971
  this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
1972
+ if (state.currentNodeDeadlineAt !== undefined) {
1973
+ const deadlineAt = state.currentNodeDeadlineAt === null ? null : Date.parse(state.currentNodeDeadlineAt);
1974
+ if (deadlineAt !== null && !Number.isFinite(deadlineAt)) {
1975
+ throw new Error("Workflow node deadline is invalid");
1976
+ }
1977
+ this.state.connection
1978
+ .prepare(`UPDATE node_attempts SET deadline_at = ?, updated_at = ?
1979
+ WHERE attempt_id = ? AND run_id = ?
1980
+ AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
1981
+ .run(deadlineAt, now, state.currentAttemptId, state.runId);
1982
+ }
1427
1983
  }
1428
1984
  this.syncRunSteps(state, now);
1429
1985
  }
@@ -1686,6 +2242,13 @@ export class WorkflowRunStore {
1686
2242
  ...(activeAttempt?.startedAt === null || activeAttempt === undefined
1687
2243
  ? {}
1688
2244
  : { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
2245
+ ...(activeAttempt === undefined
2246
+ ? {}
2247
+ : {
2248
+ currentNodeDeadlineAt: activeAttempt.deadlineAt === null
2249
+ ? null
2250
+ : new Date(activeAttempt.deadlineAt).toISOString(),
2251
+ }),
1689
2252
  ...savedCurrentSettingsBinding(activeAttempt?.settingsScopeId ?? null, activeAttempt?.settingsChangeNumber ?? null, activeAttempt?.settingsHash ?? null),
1690
2253
  ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
1691
2254
  ...(humanDecision === undefined ? {} : { humanDecision }),
@@ -1803,7 +2366,7 @@ export class WorkflowRunStore {
1803
2366
  readActiveAttempt(runId) {
1804
2367
  const row = this.state.connection
1805
2368
  .prepare(`SELECT attempt_id AS attemptId, node_id AS nodeId, started_at AS startedAt,
1806
- settings_scope_id AS settingsScopeId,
2369
+ deadline_at AS deadlineAt, settings_scope_id AS settingsScopeId,
1807
2370
  settings_change_number AS settingsChangeNumber,
1808
2371
  settings_hash AS settingsHash
1809
2372
  FROM node_attempts
@@ -2046,10 +2609,148 @@ export class WorkflowRunStore {
2046
2609
  this.bumpResource(resourceId, revision, now);
2047
2610
  const payloadHash = this.state.putJson(capture, now);
2048
2611
  insertGenericEvent(this.state, resourceId, revision + 1, "session.capture_failed", "system", null, payloadHash, now);
2612
+ const viewerCapture = this.viewerSessionCapture(runId);
2613
+ recordViewerDeltas(this.state, runId, [
2614
+ {
2615
+ targetType: "conversation",
2616
+ targetKey: "capture",
2617
+ patch: [
2618
+ {
2619
+ op: "add",
2620
+ path: "/capture",
2621
+ value: parseJson(canonicalJson(viewerCapture)),
2622
+ },
2623
+ ],
2624
+ },
2625
+ ], now);
2049
2626
  });
2050
2627
  }
2051
2628
  }
2052
2629
  }
2630
+ function runViewerTargets(state, traceEvent) {
2631
+ const stepTotal = state.steps.length;
2632
+ const stepStart = Math.max(0, stepTotal - VIEWER_PAGE_SIZE);
2633
+ const updateTotal = state.updates?.length ?? 0;
2634
+ const updateStart = Math.max(0, updateTotal - VIEWER_PAGE_SIZE);
2635
+ const compactStep = (step) => ({
2636
+ attemptId: step.attemptId,
2637
+ nodeId: step.nodeId,
2638
+ nodeType: step.nodeType,
2639
+ outcome: step.outcome,
2640
+ startedAt: step.startedAt,
2641
+ finishedAt: step.finishedAt,
2642
+ prompt: null,
2643
+ output: null,
2644
+ ...(step.settingsScopeId === undefined ? {} : { settingsScopeId: step.settingsScopeId }),
2645
+ ...(step.settingsChangeNumber === undefined
2646
+ ? {}
2647
+ : { settingsChangeNumber: step.settingsChangeNumber }),
2648
+ ...(step.settingsHash === undefined ? {} : { settingsHash: step.settingsHash }),
2649
+ });
2650
+ const latestStepByNode = new Map();
2651
+ for (const step of state.steps)
2652
+ latestStepByNode.set(step.nodeId, step);
2653
+ const graphSteps = state.steps
2654
+ .filter((step) => latestStepByNode.get(step.nodeId) === step)
2655
+ .map(compactStep);
2656
+ const transitions = new Set();
2657
+ for (let index = 1; index < state.steps.length; index += 1) {
2658
+ const previous = state.steps[index - 1];
2659
+ const current = state.steps[index];
2660
+ if (previous !== undefined && current !== undefined) {
2661
+ transitions.add(`${previous.nodeId}->${current.nodeId}`);
2662
+ }
2663
+ }
2664
+ const graphPatch = [
2665
+ { op: "replace", path: "/state/traceSeq", value: state.traceSeq },
2666
+ { op: "replace", path: "/state/updatedAt", value: state.updatedAt },
2667
+ { op: "replace", path: "/state/status", value: state.status },
2668
+ {
2669
+ op: "add",
2670
+ path: "/state/updates",
2671
+ value: parseJson(canonicalJson(state.updates?.slice(updateStart) ?? [])),
2672
+ },
2673
+ { op: "add", path: "/state/finishedAt", value: state.finishedAt ?? null },
2674
+ { op: "add", path: "/state/currentNode", value: state.currentNode ?? null },
2675
+ { op: "add", path: "/state/currentAttemptId", value: state.currentAttemptId ?? null },
2676
+ {
2677
+ op: "add",
2678
+ path: "/state/currentNodeStartedAt",
2679
+ value: state.currentNodeStartedAt ?? null,
2680
+ },
2681
+ {
2682
+ op: "add",
2683
+ path: "/state/currentNodeDeadlineAt",
2684
+ value: state.currentNodeDeadlineAt ?? null,
2685
+ },
2686
+ {
2687
+ op: "add",
2688
+ path: "/state/currentSettingsScopeId",
2689
+ value: state.currentSettingsScopeId ?? null,
2690
+ },
2691
+ {
2692
+ op: "add",
2693
+ path: "/state/currentSettingsChangeNumber",
2694
+ value: state.currentSettingsChangeNumber ?? null,
2695
+ },
2696
+ {
2697
+ op: "add",
2698
+ path: "/state/currentSettingsHash",
2699
+ value: state.currentSettingsHash ?? null,
2700
+ },
2701
+ { op: "add", path: "/state/statusDetail", value: state.statusDetail ?? null },
2702
+ { op: "add", path: "/state/paused", value: state.paused ?? false },
2703
+ { op: "add", path: "/state/waitingOn", value: state.waitingOn ?? null },
2704
+ {
2705
+ op: "add",
2706
+ path: "/state/humanDecision",
2707
+ value: parseJson(canonicalJson(state.humanDecision ?? null)),
2708
+ },
2709
+ {
2710
+ op: "replace",
2711
+ path: "/graphSteps",
2712
+ value: parseJson(canonicalJson(graphSteps)),
2713
+ },
2714
+ {
2715
+ op: "replace",
2716
+ path: "/takenTransitions",
2717
+ value: [...transitions].sort(),
2718
+ },
2719
+ { op: "replace", path: "/graphCursor", value: Math.max(0, stepTotal - 1) },
2720
+ { op: "replace", path: "/stepStart", value: stepStart },
2721
+ { op: "replace", path: "/stepTotal", value: stepTotal },
2722
+ { op: "replace", path: "/updateStart", value: updateStart },
2723
+ { op: "replace", path: "/updateTotal", value: updateTotal },
2724
+ { op: "replace", path: "/manifest/status", value: state.status },
2725
+ { op: "add", path: "/manifest/finishedAt", value: state.finishedAt ?? null },
2726
+ {
2727
+ op: "replace",
2728
+ path: "/live",
2729
+ value: state.status === "running" || state.status === "waiting",
2730
+ },
2731
+ ];
2732
+ const targets = [
2733
+ { targetType: "graph", patch: graphPatch },
2734
+ {
2735
+ targetType: "timeline",
2736
+ targetKey: "trace:tail",
2737
+ patch: viewerTailPatch(traceEvent.seq - 1, [
2738
+ parseJson(canonicalJson({
2739
+ ...traceEvent,
2740
+ payload: compactTracePayload(traceEvent.type, traceEvent.payload),
2741
+ })),
2742
+ ]),
2743
+ },
2744
+ ];
2745
+ if (traceEvent.type === "node_finished" || traceEvent.type === "node_failed") {
2746
+ targets.push({
2747
+ targetType: "replay",
2748
+ targetKey: "steps:reload",
2749
+ patch: [{ op: "replace", path: "/stepTotal", value: stepTotal }],
2750
+ });
2751
+ }
2752
+ return targets;
2753
+ }
2053
2754
  export function readWorkflowRun(runId, options = {}) {
2054
2755
  const store = new WorkflowRunStore(options.databasePath ?? workflowStatePath(), {
2055
2756
  readOnly: true,
@@ -2249,6 +2950,20 @@ function emptySegment() {
2249
2950
  integrity: { status: "unavailable", diagnostics: [] },
2250
2951
  };
2251
2952
  }
2953
+ function assertSessionEventType(value) {
2954
+ switch (value) {
2955
+ case "turn_started":
2956
+ case "turn_finished":
2957
+ case "message_started":
2958
+ case "assistant_event":
2959
+ case "message_finished":
2960
+ case "tool_execution_started":
2961
+ case "tool_execution_finished":
2962
+ return value;
2963
+ default:
2964
+ throw new Error(`Unknown session event type: ${value}`);
2965
+ }
2966
+ }
2252
2967
  function validateSessionEventRecord(record) {
2253
2968
  if (!Number.isSafeInteger(record.seq) || record.seq < 1) {
2254
2969
  throw new Error("Session event seq must be a positive safe integer");
@@ -2416,12 +3131,51 @@ function isStatusRow(value) {
2416
3131
  function isCountRow(value) {
2417
3132
  return isRecord(value) && typeof value.count === "number";
2418
3133
  }
3134
+ function isStepIndexRow(value) {
3135
+ return isRecord(value) && typeof value.stepIndex === "number";
3136
+ }
2419
3137
  function isLeaseExpiryRow(value) {
2420
3138
  return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
2421
3139
  }
2422
3140
  function isNextOrderRow(value) {
2423
3141
  return isRecord(value) && typeof value.nextOrder === "number";
2424
3142
  }
3143
+ function isViewerSessionCheckpointRow(value) {
3144
+ return isRecord(value) && typeof value.eventSeq === "number" && Buffer.isBuffer(value.stateHash);
3145
+ }
3146
+ function isViewerSessionEventRow(value) {
3147
+ return (isRecord(value) &&
3148
+ typeof value.runSeq === "number" &&
3149
+ typeof value.eventType === "string" &&
3150
+ typeof value.nodeId === "string" &&
3151
+ typeof value.attemptId === "string" &&
3152
+ (typeof value.turnId === "string" || value.turnId === null) &&
3153
+ (typeof value.messageId === "string" || value.messageId === null) &&
3154
+ (typeof value.toolCallId === "string" || value.toolCallId === null) &&
3155
+ Buffer.isBuffer(value.payloadHash) &&
3156
+ typeof value.recordedAt === "number");
3157
+ }
3158
+ function isTemporalSessionState(value) {
3159
+ return (isRecord(value) &&
3160
+ typeof value.throughSeq === "number" &&
3161
+ Array.isArray(value.messages) &&
3162
+ Array.isArray(value.tools) &&
3163
+ Array.isArray(value.settledEntryIds) &&
3164
+ value.settledEntryIds.every((item) => typeof item === "string") &&
3165
+ Array.isArray(value.diagnostics) &&
3166
+ value.diagnostics.every((item) => typeof item === "string"));
3167
+ }
3168
+ function boundedTemporalCheckpoint(state) {
3169
+ const messages = state.messages.filter((message) => message.status === "streaming");
3170
+ const activeMessages = new Set(messages.map((message) => message.messageId));
3171
+ return {
3172
+ ...state,
3173
+ messages,
3174
+ tools: state.tools.filter((tool) => tool.status === "running" && activeMessages.has(tool.messageId)),
3175
+ settledEntryIds: [],
3176
+ diagnostics: ["earlier session messages are outside this page"],
3177
+ };
3178
+ }
2425
3179
  function assertValidRunId(runId) {
2426
3180
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
2427
3181
  throw new Error(`Invalid workflow run id: ${JSON.stringify(runId)}`);
@@ -2514,6 +3268,46 @@ function isSessionEventRow(value) {
2514
3268
  function isLeaseAuthorityRow(value) {
2515
3269
  return isRecord(value);
2516
3270
  }
3271
+ function isApplyingEffectRow(value) {
3272
+ return (isRecord(value) &&
3273
+ typeof value.effectId === "string" &&
3274
+ typeof value.resourceId === "string" &&
3275
+ Buffer.isBuffer(value.payloadHash) &&
3276
+ typeof value.attemptCount === "number");
3277
+ }
3278
+ function effectRecoveryFromPayload(value) {
3279
+ return typeof value === "object" &&
3280
+ value !== null &&
3281
+ !Array.isArray(value) &&
3282
+ value.recovery === "idempotent"
3283
+ ? "idempotent"
3284
+ : "manual";
3285
+ }
3286
+ function isWorkflowEffectRow(value) {
3287
+ return (isRecord(value) &&
3288
+ typeof value.status === "string" &&
3289
+ Buffer.isBuffer(value.payloadHash) &&
3290
+ (value.resultHash === null || Buffer.isBuffer(value.resultHash)) &&
3291
+ typeof value.attemptCount === "number");
3292
+ }
3293
+ function isWorkflowEffectIdentityRow(value) {
3294
+ return (isRecord(value) && typeof value.resourceId === "string" && typeof value.status === "string");
3295
+ }
3296
+ function isResourceIdRow(value) {
3297
+ return isRecord(value) && typeof value.resourceId === "string";
3298
+ }
3299
+ function workflowEffectId(sourceResourceId, effectType, idempotencyKey) {
3300
+ const digest = createHash("sha256")
3301
+ .update(`${sourceResourceId}\0${effectType}\0${idempotencyKey}`)
3302
+ .digest("hex")
3303
+ .slice(0, 40);
3304
+ return `effect-${digest}`;
3305
+ }
3306
+ function requireBoundedEffectText(value, label) {
3307
+ if (typeof value !== "string" || value.length === 0 || value.length > 256) {
3308
+ throw new Error(`Managed effect ${label} must be nonempty text of at most 256 characters`);
3309
+ }
3310
+ }
2517
3311
  export function createDefinitionSnapshot(workflow) {
2518
3312
  const metadata = compositionMetadata(workflow);
2519
3313
  const composition = metadata?.snapshot;
@@ -2590,8 +3384,13 @@ function snapshotNode(workflow, nodeId, node) {
2590
3384
  ...(typeof node.humanDecision.onTimeout === "function" ? { dynamicTimeout: true } : {}),
2591
3385
  };
2592
3386
  }
2593
- if (node.nodeType === "action")
3387
+ if (node.nodeType === "action") {
2594
3388
  common.actionExecution = "exec" in node ? "shell" : "function";
3389
+ /* istanbul ignore else -- validated action nodes always declare a managed effect */
3390
+ if (node.effect !== undefined) {
3391
+ common.effect = { type: node.effect.type, recovery: node.effect.recovery };
3392
+ }
3393
+ }
2595
3394
  return common;
2596
3395
  }
2597
3396
  function snapshotSettings(settings) {