@osolmaz/pi-workflows 0.13.1 → 0.13.3

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 (152) hide show
  1. package/README.md +9 -7
  2. package/dist/builtins/autodoc.workflow.d.ts +5 -5
  3. package/dist/builtins/autoimplement.workflow.d.ts +69 -61
  4. package/dist/builtins/autoimplement.workflow.js +47 -8
  5. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  6. package/dist/builtins/autoplan.workflow.d.ts +11 -5
  7. package/dist/builtins/autoplan.workflow.js +68 -32
  8. package/dist/builtins/autoplan.workflow.js.map +1 -1
  9. package/dist/builtins/catalog.js +4 -4
  10. package/dist/builtins/change-verification.workflow.d.ts +1 -1
  11. package/dist/builtins/plain-summary.workflow.d.ts +1 -1
  12. package/dist/builtins/plain-summary.workflow.js +35 -24
  13. package/dist/builtins/plain-summary.workflow.js.map +1 -1
  14. package/dist/builtins/plan-approval.workflow.d.ts +1 -1
  15. package/dist/builtins/plan-change.workflow.d.ts +25 -23
  16. package/dist/builtins/sanity-check.workflow.d.ts +3 -3
  17. package/dist/builtins/sanity-check.workflow.js +19 -22
  18. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  19. package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
  20. package/dist/controllers/index.d.ts +2 -2
  21. package/dist/controllers/index.js.map +1 -1
  22. package/dist/controllers/sqlite.js +88 -61
  23. package/dist/controllers/sqlite.js.map +1 -1
  24. package/dist/controllers/types.d.ts +34 -0
  25. package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
  26. package/dist/controllers/workflow-engine-scheduler.js +83 -1
  27. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  28. package/dist/controllers/workflows.d.ts +8 -1
  29. package/dist/controllers/workflows.js +36 -0
  30. package/dist/controllers/workflows.js.map +1 -1
  31. package/dist/extension/controller-host.d.ts +2 -1
  32. package/dist/extension/controller-host.js +1 -1
  33. package/dist/extension/controller-host.js.map +1 -1
  34. package/dist/extension/decision-channels.js +45 -7
  35. package/dist/extension/decision-channels.js.map +1 -1
  36. package/dist/extension/follow-up-coordinator.d.ts +27 -0
  37. package/dist/extension/follow-up-coordinator.js +131 -0
  38. package/dist/extension/follow-up-coordinator.js.map +1 -0
  39. package/dist/extension/index.d.ts +9 -0
  40. package/dist/extension/index.js +462 -76
  41. package/dist/extension/index.js.map +1 -1
  42. package/dist/extension/recorder.d.ts +1 -1
  43. package/dist/extension/recorder.js +8 -8
  44. package/dist/extension/recorder.js.map +1 -1
  45. package/dist/extension/session-events.d.ts +2 -3
  46. package/dist/extension/session-events.js +11 -10
  47. package/dist/extension/session-events.js.map +1 -1
  48. package/dist/extension/widget.js +9 -0
  49. package/dist/extension/widget.js.map +1 -1
  50. package/dist/state/database.d.ts +2 -1
  51. package/dist/state/database.js +11 -13
  52. package/dist/state/database.js.map +1 -1
  53. package/dist/state/json.js +6 -1
  54. package/dist/state/json.js.map +1 -1
  55. package/dist/state/mutation.d.ts +1 -1
  56. package/dist/state/mutation.js.map +1 -1
  57. package/dist/state/prune.d.ts +18 -0
  58. package/dist/state/prune.js +373 -0
  59. package/dist/state/prune.js.map +1 -0
  60. package/dist/state/schema.d.ts +1 -1
  61. package/dist/state/schema.js +130 -13
  62. package/dist/state/schema.js.map +1 -1
  63. package/dist/viewer/cli.d.ts +3 -1
  64. package/dist/viewer/cli.js +55 -4
  65. package/dist/viewer/cli.js.map +1 -1
  66. package/dist/viewer/render.js +19 -1
  67. package/dist/viewer/render.js.map +1 -1
  68. package/dist/viewer/session-reducer.d.ts +0 -1
  69. package/dist/viewer/session-reducer.js +2 -24
  70. package/dist/viewer/session-reducer.js.map +1 -1
  71. package/dist/workflows/composition.d.ts +2 -0
  72. package/dist/workflows/composition.js +15 -0
  73. package/dist/workflows/composition.js.map +1 -1
  74. package/dist/workflows/definition.d.ts +6 -3
  75. package/dist/workflows/definition.js +3 -0
  76. package/dist/workflows/definition.js.map +1 -1
  77. package/dist/workflows/engine.d.ts +6 -1
  78. package/dist/workflows/engine.js +303 -36
  79. package/dist/workflows/engine.js.map +1 -1
  80. package/dist/workflows/human-decision.d.ts +1 -0
  81. package/dist/workflows/human-decision.js +25 -9
  82. package/dist/workflows/human-decision.js.map +1 -1
  83. package/dist/workflows/index.d.ts +3 -1
  84. package/dist/workflows/index.js +2 -0
  85. package/dist/workflows/index.js.map +1 -1
  86. package/dist/workflows/json-patch.d.ts +47 -0
  87. package/dist/workflows/json-patch.js +298 -0
  88. package/dist/workflows/json-patch.js.map +1 -0
  89. package/dist/workflows/schema.js +29 -1
  90. package/dist/workflows/schema.js.map +1 -1
  91. package/dist/workflows/settings.d.ts +128 -0
  92. package/dist/workflows/settings.js +199 -0
  93. package/dist/workflows/settings.js.map +1 -0
  94. package/dist/workflows/store.d.ts +81 -3
  95. package/dist/workflows/store.js +1563 -50
  96. package/dist/workflows/store.js.map +1 -1
  97. package/dist/workflows/tool-input.d.ts +22 -0
  98. package/dist/workflows/tool-input.js +43 -0
  99. package/dist/workflows/tool-input.js.map +1 -1
  100. package/dist/workflows/types.d.ts +41 -6
  101. package/docs/2026-08-25-workflow-follow-ups.md +132 -0
  102. package/docs/2026-08-25-workflow-settings.md +169 -0
  103. package/docs/DEFERRED_TURNS.md +6 -0
  104. package/docs/DESIGN_PHILOSOPHY.md +2 -0
  105. package/docs/SQLITE_STATE.md +36 -17
  106. package/docs/WORKFLOW_COMPOSITION.md +15 -0
  107. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
  108. package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
  109. package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
  110. package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
  111. package/docs/session-event-journal.md +2 -3
  112. package/docs/tui-viewer.md +7 -6
  113. package/docs/workflows.md +58 -10
  114. package/examples/workflows/live-settings.workflow.ts +93 -0
  115. package/herdr-plugin.toml +1 -1
  116. package/package.json +1 -1
  117. package/src/builtins/autoimplement.workflow.ts +56 -8
  118. package/src/builtins/autoplan.workflow.ts +80 -32
  119. package/src/builtins/catalog.ts +4 -4
  120. package/src/builtins/plain-summary.workflow.ts +38 -40
  121. package/src/builtins/sanity-check.workflow.ts +19 -22
  122. package/src/controllers/index.ts +6 -0
  123. package/src/controllers/sqlite.ts +132 -72
  124. package/src/controllers/types.ts +40 -0
  125. package/src/controllers/workflow-engine-scheduler.ts +103 -1
  126. package/src/controllers/workflows.ts +68 -0
  127. package/src/extension/controller-host.ts +6 -1
  128. package/src/extension/decision-channels.ts +47 -7
  129. package/src/extension/follow-up-coordinator.ts +151 -0
  130. package/src/extension/index.ts +540 -89
  131. package/src/extension/recorder.ts +10 -8
  132. package/src/extension/session-events.ts +15 -13
  133. package/src/extension/widget.ts +8 -0
  134. package/src/state/database.ts +12 -12
  135. package/src/state/json.ts +6 -1
  136. package/src/state/mutation.ts +4 -1
  137. package/src/state/prune.ts +502 -0
  138. package/src/state/schema.ts +130 -13
  139. package/src/viewer/cli.ts +52 -5
  140. package/src/viewer/render.ts +43 -1
  141. package/src/viewer/session-reducer.ts +2 -29
  142. package/src/workflows/composition.ts +19 -0
  143. package/src/workflows/definition.ts +19 -5
  144. package/src/workflows/engine.ts +365 -36
  145. package/src/workflows/human-decision.ts +41 -11
  146. package/src/workflows/index.ts +43 -0
  147. package/src/workflows/json-patch.ts +375 -0
  148. package/src/workflows/schema.ts +31 -1
  149. package/src/workflows/settings.ts +430 -0
  150. package/src/workflows/store.ts +2268 -121
  151. package/src/workflows/tool-input.ts +58 -0
  152. package/src/workflows/types.ts +43 -5
@@ -1,8 +1,10 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { StateDatabase, workflowStatePath } from "../state/database.js";
3
3
  import { canonicalJson } from "../state/json.js";
4
- import { resourceIdFor, tokenHash } from "../state/mutation.js";
4
+ import { StateMutationStore, StaleResourceError, resourceIdFor, tokenHash, } from "../state/mutation.js";
5
5
  import { compositionMetadata } from "./composition.js";
6
+ import { applyJsonPatch, validateJsonPatch } from "./json-patch.js";
7
+ import { applyWorkflowSettingsPatch, workflowSettingsScopeId, } from "./settings.js";
6
8
  import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
7
9
  export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1";
8
10
  export const DEFINITION_SNAPSHOT_SCHEMA = "pi-workflows.definition-snapshot.v1";
@@ -10,6 +12,18 @@ export const SESSION_BINDING_SCHEMA = "pi-workflows.session-binding.v1";
10
12
  export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1";
11
13
  export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1";
12
14
  export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
15
+ const FOLLOW_UP_ROW_SELECT = `
16
+ SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
17
+ f.queue_resource_id AS queueResourceId, f.run_id AS runId,
18
+ f.request_id AS requestId, f.order_number AS orderNumber,
19
+ f.target_session_id AS targetSessionId, f.actor_type AS actorType,
20
+ f.actor_id AS actorId, f.source_type AS sourceType,
21
+ f.prompt_hash AS promptHash, f.status,
22
+ f.session_entry_id AS sessionEntryId, f.reason_hash AS reasonHash,
23
+ f.created_at AS createdAt, f.updated_at AS updatedAt, f.sent_at AS sentAt,
24
+ r.revision
25
+ FROM workflow_follow_ups f
26
+ JOIN resources r ON r.resource_id = f.resource_id`;
13
27
  export function workflowStateDatabasePath(homeDir) {
14
28
  return workflowStatePath(homeDir);
15
29
  }
@@ -23,6 +37,7 @@ export class WorkflowRunStore {
23
37
  authorityProvider;
24
38
  contexts = new Map();
25
39
  ownsState;
40
+ mutations;
26
41
  constructor(databasePath = workflowStatePath(), options = {}) {
27
42
  this.authorityProvider = options.authorityProvider;
28
43
  this.ownsState = options.state === undefined;
@@ -34,12 +49,13 @@ export class WorkflowRunStore {
34
49
  checkLegacyState: databasePath === workflowStatePath(),
35
50
  });
36
51
  this.databasePath = this.state.filePath;
52
+ this.mutations = new StateMutationStore(this.state);
37
53
  }
38
54
  close() {
39
55
  if (this.ownsState)
40
56
  this.state.close();
41
57
  }
42
- async initializeRun(workflow, state) {
58
+ async initializeRun(workflow, state, options = {}) {
43
59
  assertValidRunId(state.runId);
44
60
  if (!this.contexts.has(state.runId)) {
45
61
  const reserved = this.readRunRow(state.runId);
@@ -79,7 +95,9 @@ export class WorkflowRunStore {
79
95
  payload: { workflowName: workflow.name },
80
96
  });
81
97
  this.persistRunState(reserved, state, revision, now);
98
+ this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
82
99
  this.syncNodeAttempts(state, snapshot, now);
100
+ this.syncContinuationIdentity(state);
83
101
  context.revision = revision;
84
102
  return;
85
103
  }
@@ -101,30 +119,463 @@ export class WorkflowRunStore {
101
119
  state.traceSeq = 1;
102
120
  state.updatedAt = at;
103
121
  const inputHash = this.state.putJson(state.input, now);
104
- const workflowSourceHash = this.state.putJson(state.workflowSource ?? source, now);
105
122
  const launchOptionsHash = this.state.putJson({}, now);
106
- const stateHash = this.state.putJson(state, now);
123
+ const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
107
124
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
108
125
  this.state.connection
109
126
  .prepare(`INSERT INTO runs(
110
127
  run_id, resource_id, project_id, parent_run_id, definition_digest,
111
- workflow_ref, workflow_source_hash, launch_options_hash,
112
- source_type, source_ref, source_revision, title, status, paused,
113
- status_detail, input_hash, output_hash, error_hash,
128
+ workflow_ref, launch_options_hash, title, status, paused,
129
+ status_detail, input_hash, final_output_hash, error_hash,
114
130
  created_at, updated_at, finished_at
115
- ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
116
- .run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName, workflowSourceHash, launchOptionsHash, source.type, source.ref, source.revision, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, stateHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
131
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
132
+ .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));
133
+ this.insertRunSources(state.runId, source, state.workflowSources ?? []);
134
+ this.syncContinuationIdentity(state);
117
135
  insertRunEvent(this.state, resourceId, 1, at, {
118
136
  scope: "run",
119
137
  type: "run_created",
120
138
  payload: { workflowName: workflow.name },
121
139
  });
140
+ this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
122
141
  this.syncNodeAttempts(state, snapshot, now);
123
142
  });
124
143
  this.contexts.set(state.runId, { revision: acceptedRevision, lock: Promise.resolve() });
125
144
  return state.runId;
126
145
  });
127
146
  }
147
+ ensureSettingsScope(options) {
148
+ const now = Date.now();
149
+ return this.state.transaction(() => {
150
+ this.requireRunAcceptsSettings(options.runId, false);
151
+ const originRunId = this.logicalOriginRunId(options.runId);
152
+ const scopeId = workflowSettingsScopeId(originRunId, options.mountPath, options.invocation);
153
+ const existing = this.settingsScopeRow(scopeId);
154
+ if (existing !== undefined) {
155
+ if (existing.activeRunId !== options.runId) {
156
+ throw new Error(`Workflow settings scope belongs to another active run: ${scopeId}`);
157
+ }
158
+ return this.settingsScopeRecord(existing);
159
+ }
160
+ const resourceId = this.mutations.ensureResource("settings", scopeId, now);
161
+ const settingsHash = this.state.putJson(options.settings, now);
162
+ this.state.connection
163
+ .prepare(`INSERT INTO workflow_settings(
164
+ scope_id, resource_id, origin_run_id, active_run_id, mount_path, invocation,
165
+ initial_hash, current_hash, created_at, updated_at
166
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
167
+ .run(scopeId, resourceId, originRunId, options.runId, options.mountPath, options.invocation, settingsHash, settingsHash, now, now);
168
+ return this.settingsScopeRecord(this.requireSettingsScopeRow(scopeId));
169
+ });
170
+ }
171
+ getSettingsScope(scopeId) {
172
+ const row = this.settingsScopeRow(scopeId);
173
+ return row === undefined ? undefined : this.settingsScopeRecord(row);
174
+ }
175
+ getSettingsScopeAtChange(scopeId, changeNumber) {
176
+ if (!Number.isInteger(changeNumber) || changeNumber < 0) {
177
+ throw new Error("Workflow settings change number must be a non-negative integer");
178
+ }
179
+ const row = this.settingsScopeRow(scopeId);
180
+ if (row === undefined)
181
+ return undefined;
182
+ if (changeNumber > row.revision) {
183
+ throw new Error(`Workflow settings change ${changeNumber} does not exist for ${scopeId}`);
184
+ }
185
+ if (changeNumber === row.revision)
186
+ return this.settingsScopeRecord(row);
187
+ const saved = changeNumber === 0
188
+ ? this.state.connection
189
+ .prepare("SELECT initial_hash AS settingsHash FROM workflow_settings WHERE scope_id = ?")
190
+ .get(scopeId)
191
+ : this.state.connection
192
+ .prepare(`SELECT after_hash AS settingsHash FROM workflow_setting_changes
193
+ WHERE scope_id = ? AND change_number = ?`)
194
+ .get(scopeId, changeNumber);
195
+ if (!isRecord(saved) || !Buffer.isBuffer(saved.settingsHash)) {
196
+ throw new Error(`Workflow settings change ${changeNumber} is missing for ${scopeId}`);
197
+ }
198
+ return this.settingsScopeRecord(row, changeNumber, saved.settingsHash);
199
+ }
200
+ listSettingsScopes(runId) {
201
+ return this.state.connection
202
+ .prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
203
+ s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
204
+ s.mount_path AS mountPath, s.invocation,
205
+ s.current_hash AS currentHash, r.revision,
206
+ s.created_at AS createdAt, s.updated_at AS updatedAt
207
+ FROM workflow_settings s
208
+ JOIN resources r ON r.resource_id = s.resource_id
209
+ WHERE s.active_run_id = ?
210
+ ORDER BY s.mount_path, s.invocation`)
211
+ .all(runId)
212
+ .filter(isSettingsScopeRow)
213
+ .map((row) => this.settingsScopeRecord(row));
214
+ }
215
+ settingsScopesForRunView(runId) {
216
+ const originRunId = this.logicalOriginRunId(runId);
217
+ return this.state.connection
218
+ .prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
219
+ s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
220
+ s.mount_path AS mountPath, s.invocation,
221
+ s.current_hash AS currentHash, r.revision,
222
+ s.created_at AS createdAt, s.updated_at AS updatedAt
223
+ FROM workflow_settings s
224
+ JOIN resources r ON r.resource_id = s.resource_id
225
+ WHERE s.origin_run_id = ? OR s.active_run_id = ?
226
+ ORDER BY s.mount_path, s.invocation`)
227
+ .all(originRunId, runId)
228
+ .filter(isSettingsScopeRow)
229
+ .map((row) => this.settingsScopeRecord(row));
230
+ }
231
+ findSettingsScope(runId, mountPath, invocation) {
232
+ const row = this.state.connection
233
+ .prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
234
+ s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
235
+ s.mount_path AS mountPath, s.invocation,
236
+ s.current_hash AS currentHash, r.revision,
237
+ s.created_at AS createdAt, s.updated_at AS updatedAt
238
+ FROM workflow_settings s
239
+ JOIN resources r ON r.resource_id = s.resource_id
240
+ WHERE s.active_run_id = ? AND s.mount_path = ? AND s.invocation = ?`)
241
+ .get(runId, mountPath, invocation);
242
+ return isSettingsScopeRow(row) ? this.settingsScopeRecord(row) : undefined;
243
+ }
244
+ async changeSettings(definition, request) {
245
+ assertRequestId(request.requestId);
246
+ assertSourceType(request.source);
247
+ const patch = validateJsonPatch(request.patch);
248
+ const patchJson = canonicalJson(patch);
249
+ const patchDigest = createHash("sha256").update(patchJson).digest();
250
+ for (let attempt = 0; attempt < 8; attempt += 1) {
251
+ const prior = this.settingsChangeRow(request.scopeId, request.requestId);
252
+ if (prior !== undefined) {
253
+ this.assertSameSettingsChange(prior, request, patchDigest);
254
+ return {
255
+ scope: this.settingsScopeRecord(this.requireSettingsScopeRow(request.scopeId)),
256
+ change: this.settingsChangeRecord(prior),
257
+ adopted: true,
258
+ };
259
+ }
260
+ const scope = this.requireSettingsScopeRow(request.scopeId);
261
+ if (scope.activeRunId !== request.runId) {
262
+ throw new Error(`Workflow settings scope is not active for run ${request.runId}`);
263
+ }
264
+ this.requireRunAcceptsSettings(request.runId, true);
265
+ if (request.expectedChangeNumber !== undefined &&
266
+ request.expectedChangeNumber !== scope.revision) {
267
+ throw new StaleResourceError(`Workflow settings changed: expected ${request.expectedChangeNumber}, current ${scope.revision}`);
268
+ }
269
+ const before = this.state.readJson(scope.currentHash);
270
+ const applied = await applyWorkflowSettingsPatch(definition, before, patch, request.actor, request.source);
271
+ const beforeHash = scope.currentHash;
272
+ const afterHash = createHash("sha256").update(canonicalJson(applied.json)).digest();
273
+ try {
274
+ const mutation = this.mutations.mutate({
275
+ resourceId: scope.resourceId,
276
+ operation: "settings.change",
277
+ actor: request.actor,
278
+ expectedRevision: scope.revision,
279
+ }, "settings.changed", ({ database, nextRevision, now }) => {
280
+ this.requireRunAcceptsSettings(request.runId, true);
281
+ const current = this.requireSettingsScopeRow(request.scopeId);
282
+ if (current.activeRunId !== request.runId || !current.currentHash.equals(beforeHash)) {
283
+ throw new StaleResourceError("Workflow settings changed before the patch committed");
284
+ }
285
+ const patchHash = database.putJson(applied.patch, now);
286
+ const savedAfterHash = database.putJson(applied.json, now);
287
+ const changeId = `setting-change-${randomUUID()}`;
288
+ database.connection
289
+ .prepare(`INSERT INTO workflow_setting_changes(
290
+ change_id, scope_id, request_id, change_number,
291
+ actor_type, actor_id, source_type, patch_hash,
292
+ before_hash, after_hash, accepted_at
293
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
294
+ .run(changeId, request.scopeId, request.requestId, nextRevision, request.actor.type, request.actor.id ?? null, request.source, patchHash, beforeHash, savedAfterHash, now);
295
+ database.connection
296
+ .prepare(`UPDATE workflow_settings SET current_hash = ?, updated_at = ? WHERE scope_id = ?`)
297
+ .run(savedAfterHash, now, request.scopeId);
298
+ return changeId;
299
+ }, {
300
+ payload: {
301
+ scopeId: request.scopeId,
302
+ requestId: request.requestId,
303
+ source: request.source,
304
+ beforeHash: beforeHash.toString("hex"),
305
+ afterHash: afterHash.toString("hex"),
306
+ },
307
+ });
308
+ const row = this.settingsChangeRow(request.scopeId, request.requestId);
309
+ if (row === undefined || row.changeId !== mutation.value) {
310
+ throw new Error("Accepted workflow settings change is missing");
311
+ }
312
+ return {
313
+ scope: this.settingsScopeRecord(this.requireSettingsScopeRow(request.scopeId)),
314
+ change: this.settingsChangeRecord(row),
315
+ adopted: false,
316
+ };
317
+ }
318
+ catch (error) {
319
+ if (!(error instanceof StaleResourceError) || request.expectedChangeNumber !== undefined) {
320
+ throw error;
321
+ }
322
+ }
323
+ }
324
+ throw new StaleResourceError("Workflow settings kept changing; retry with an expected number");
325
+ }
326
+ queueFollowUp(request) {
327
+ assertRequestId(request.requestId);
328
+ assertSourceType(request.source);
329
+ if (typeof request.prompt !== "string" || request.prompt.trim().length === 0) {
330
+ throw new Error("Workflow follow-up prompt must be a non-empty string");
331
+ }
332
+ if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
333
+ throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
334
+ }
335
+ for (let attempt = 0; attempt < 8; attempt += 1) {
336
+ const prior = this.followUpRowByRequest(request.runId, request.requestId);
337
+ if (prior !== undefined) {
338
+ this.assertSameFollowUp(prior, request);
339
+ return { followUp: this.followUpRecord(prior), adopted: true };
340
+ }
341
+ this.requireRunAcceptsSettings(request.runId, true);
342
+ const queue = this.requireFollowUpQueueRow(request.runId);
343
+ try {
344
+ const result = this.mutations.mutate({
345
+ resourceId: queue.resourceId,
346
+ operation: "follow-up.queue",
347
+ actor: request.actor,
348
+ expectedRevision: queue.revision,
349
+ }, "follow-up.queued", ({ database, now }) => {
350
+ this.requireRunAcceptsSettings(request.runId, true);
351
+ const currentQueue = this.requireFollowUpQueueRow(request.runId);
352
+ if (currentQueue.originSessionId !== null &&
353
+ currentQueue.originSessionId !== request.targetSessionId) {
354
+ throw new Error("Workflow follow-ups must target the run's origin Pi session");
355
+ }
356
+ if (currentQueue.originSessionId === null) {
357
+ database.connection
358
+ .prepare(`UPDATE workflow_follow_up_queues
359
+ SET origin_session_id = ?, updated_at = ? WHERE run_id = ?`)
360
+ .run(request.targetSessionId, now, request.runId);
361
+ }
362
+ const order = this.nextFollowUpOrder(request.runId);
363
+ const followUpId = followUpIdFor(request.runId, request.requestId);
364
+ const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
365
+ const promptHash = database.putText(request.prompt, now);
366
+ database.connection
367
+ .prepare(`INSERT INTO workflow_follow_ups(
368
+ follow_up_id, resource_id, queue_resource_id, run_id,
369
+ request_id, order_number, target_session_id,
370
+ actor_type, actor_id, source_type, prompt_hash, status,
371
+ created_at, updated_at
372
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
373
+ .run(followUpId, resourceId, queue.resourceId, request.runId, request.requestId, order, request.targetSessionId, request.actor.type, request.actor.id ?? null, request.source, promptHash, now, now);
374
+ return followUpId;
375
+ }, { payload: { runId: request.runId, requestId: request.requestId } });
376
+ return {
377
+ followUp: this.followUpRecord(this.requireFollowUpRow(result.value)),
378
+ adopted: false,
379
+ };
380
+ }
381
+ catch (error) {
382
+ if (!(error instanceof StaleResourceError))
383
+ throw error;
384
+ }
385
+ }
386
+ throw new StaleResourceError("Workflow follow-up queue kept changing; retry the request");
387
+ }
388
+ removeFollowUp(request) {
389
+ assertSourceType(request.source);
390
+ const row = this.requireFollowUpRow(request.followUpId);
391
+ if (row.runId !== request.runId) {
392
+ throw new Error(`Workflow follow-up is not part of run ${request.runId}`);
393
+ }
394
+ if (row.status === "removed")
395
+ return this.followUpRecord(row);
396
+ if (row.status === "sent" || row.status === "cancelled") {
397
+ throw new Error(`Workflow follow-up cannot be removed after it is ${row.status}`);
398
+ }
399
+ this.assertFollowUpRemovalAuthority(row, request.actor, request.source);
400
+ if (this.hasLiveLease(row.resourceId)) {
401
+ throw new Error("Workflow follow-up is being delivered and cannot be removed");
402
+ }
403
+ const reason = "Removed before delivery";
404
+ this.mutations.mutate({
405
+ resourceId: row.resourceId,
406
+ operation: "follow-up.remove",
407
+ actor: request.actor,
408
+ expectedRevision: row.revision,
409
+ }, "follow-up.removed", ({ database, now }) => {
410
+ const reasonHash = database.putText(reason, now);
411
+ const update = database.connection
412
+ .prepare(`UPDATE workflow_follow_ups
413
+ SET status = 'removed', reason_hash = ?, updated_at = ?
414
+ WHERE follow_up_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`)
415
+ .run(reasonHash, now, request.followUpId);
416
+ if (update.changes !== 1) {
417
+ throw new StaleResourceError("Workflow follow-up changed before removal");
418
+ }
419
+ }, { payload: { followUpId: request.followUpId, source: request.source } });
420
+ return this.followUpRecord(this.requireFollowUpRow(request.followUpId));
421
+ }
422
+ originSessionId(runId) {
423
+ const row = this.state.connection
424
+ .prepare(`SELECT COALESCE(q.origin_session_id, b.origin_session_id) AS originSessionId
425
+ FROM runs r
426
+ LEFT JOIN workflow_follow_up_queues q ON q.run_id = r.run_id
427
+ LEFT JOIN run_bindings b ON b.run_id = r.run_id
428
+ WHERE r.run_id = ?`)
429
+ .get(runId);
430
+ return isRecord(row) && typeof row.originSessionId === "string"
431
+ ? row.originSessionId
432
+ : undefined;
433
+ }
434
+ readFollowUpQueue(runId) {
435
+ const queue = this.followUpQueueRow(runId);
436
+ if (queue === undefined)
437
+ return undefined;
438
+ const followUps = this.state.connection
439
+ .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
440
+ .all(runId)
441
+ .filter(isFollowUpRow)
442
+ .map((row) => this.followUpRecord(row));
443
+ return {
444
+ runId,
445
+ ...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
446
+ presentationState: assertPresentationState(queue.presentationState),
447
+ ...(queue.presentationEntryId !== null
448
+ ? { presentationEntryId: queue.presentationEntryId }
449
+ : {}),
450
+ ...(queue.presentationAssistantEntryId !== null
451
+ ? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
452
+ : {}),
453
+ ...(queue.presentationReasonHash !== null
454
+ ? { presentationReason: this.readText(queue.presentationReasonHash) }
455
+ : {}),
456
+ followUps,
457
+ };
458
+ }
459
+ listPendingPresentations(targetSessionId) {
460
+ return this.state.connection
461
+ .prepare(`SELECT run_id AS runId FROM workflow_follow_up_queues
462
+ WHERE origin_session_id = ? AND presentation_state = 'pending'
463
+ ORDER BY created_at`)
464
+ .all(targetSessionId)
465
+ .flatMap((row) => (isRecord(row) && typeof row.runId === "string" ? [row.runId] : []));
466
+ }
467
+ settleFollowUpPresentation(options) {
468
+ const queue = this.requireFollowUpQueueRow(options.runId);
469
+ if (queue.presentationState === options.state) {
470
+ return this.readFollowUpQueue(options.runId);
471
+ }
472
+ if (queue.presentationState !== "pending") {
473
+ throw new Error(`Workflow presentation is ${queue.presentationState}, not pending for ${options.runId}`);
474
+ }
475
+ if (options.state === "settled" &&
476
+ (options.presentationEntryId === undefined || options.assistantEntryId === undefined)) {
477
+ throw new Error("Settled workflow presentation requires both session entry IDs");
478
+ }
479
+ if (options.state === "unavailable" && !options.reason?.trim()) {
480
+ throw new Error("Unavailable workflow presentation requires a reason");
481
+ }
482
+ try {
483
+ this.mutations.mutate({
484
+ resourceId: queue.resourceId,
485
+ operation: "follow-up.presentation",
486
+ actor: {
487
+ type: "session",
488
+ ...(queue.originSessionId !== null ? { id: queue.originSessionId } : {}),
489
+ },
490
+ expectedRevision: queue.revision,
491
+ }, `follow-up.presentation-${options.state}`, ({ database, now }) => {
492
+ const reasonHash = options.state === "unavailable"
493
+ ? database.putText(options.reason, now)
494
+ : null;
495
+ database.connection
496
+ .prepare(`UPDATE workflow_follow_up_queues
497
+ SET presentation_state = ?, presentation_entry_id = ?,
498
+ presentation_assistant_entry_id = ?, presentation_reason_hash = ?,
499
+ presentation_updated_at = ?, updated_at = ?
500
+ WHERE run_id = ? AND presentation_state = 'pending'`)
501
+ .run(options.state, options.presentationEntryId ?? null, options.assistantEntryId ?? null, reasonHash, now, now, options.runId);
502
+ database.connection
503
+ .prepare(`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
504
+ WHERE run_id = ? AND status = 'pending_presentation'`)
505
+ .run(now, options.runId);
506
+ }, { payload: { runId: options.runId, state: options.state } });
507
+ }
508
+ catch (error) {
509
+ if (error instanceof StaleResourceError) {
510
+ const current = this.requireFollowUpQueueRow(options.runId);
511
+ if (current.presentationState === options.state) {
512
+ return this.readFollowUpQueue(options.runId);
513
+ }
514
+ }
515
+ throw error;
516
+ }
517
+ return this.readFollowUpQueue(options.runId);
518
+ }
519
+ claimNextFollowUp(targetSessionId, ownerId, leaseMs = 30_000) {
520
+ const rows = this.state.connection
521
+ .prepare(`${FOLLOW_UP_ROW_SELECT}
522
+ WHERE f.target_session_id = ? AND f.status = 'ready'
523
+ ORDER BY f.created_at, f.order_number LIMIT 16`)
524
+ .all(targetSessionId)
525
+ .filter(isFollowUpRow);
526
+ for (const row of rows) {
527
+ let claim;
528
+ try {
529
+ claim = this.mutations.claim({
530
+ resourceId: row.resourceId,
531
+ ownerType: "session",
532
+ ownerId,
533
+ expectedRevision: row.revision,
534
+ leaseMs,
535
+ });
536
+ }
537
+ catch (error) {
538
+ if (error instanceof StaleResourceError)
539
+ continue;
540
+ throw error;
541
+ }
542
+ if (claim === undefined)
543
+ continue;
544
+ const current = this.requireFollowUpRow(row.followUpId);
545
+ if (current.status !== "ready") {
546
+ this.mutations.release(claim, claim.resourceRevision);
547
+ continue;
548
+ }
549
+ return { followUp: this.followUpRecord(current), claim };
550
+ }
551
+ return undefined;
552
+ }
553
+ markFollowUpSent(followUpId, claim, sessionEntryId) {
554
+ const row = this.requireFollowUpRow(followUpId);
555
+ if (row.status === "sent")
556
+ return this.followUpRecord(row);
557
+ this.mutations.mutate({
558
+ resourceId: row.resourceId,
559
+ operation: "follow-up.sent",
560
+ actor: { type: "session", id: claim.ownerId },
561
+ expectedRevision: claim.resourceRevision,
562
+ lease: claim,
563
+ }, "follow-up.sent", ({ database, now }) => {
564
+ const update = database.connection
565
+ .prepare(`UPDATE workflow_follow_ups
566
+ SET status = 'sent', session_entry_id = ?, sent_at = ?, updated_at = ?
567
+ WHERE follow_up_id = ? AND status = 'ready'`)
568
+ .run(sessionEntryId, now, now, followUpId);
569
+ if (update.changes !== 1) {
570
+ throw new StaleResourceError("Workflow follow-up changed before delivery completed");
571
+ }
572
+ }, { payload: { followUpId, sessionEntryId } });
573
+ return this.followUpRecord(this.requireFollowUpRow(followUpId));
574
+ }
575
+ releaseFollowUpClaim(claim) {
576
+ const revision = this.requireResourceRevision(claim.resourceId);
577
+ this.mutations.release(claim, revision);
578
+ }
128
579
  async prepareRunResume(runId) {
129
580
  const loaded = this.readRun(runId, { includeTrace: true });
130
581
  if (loaded === null)
@@ -132,6 +583,7 @@ export class WorkflowRunStore {
132
583
  if (loaded.state.status !== "running") {
133
584
  throw new Error(`Cannot resume workflow run ${runId} with status ${loaded.state.status}`);
134
585
  }
586
+ this.verifySettingsProjections(runId);
135
587
  const revision = this.resourceRevision(runId);
136
588
  this.contexts.set(runId, { revision, lock: Promise.resolve() });
137
589
  this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
@@ -144,9 +596,9 @@ export class WorkflowRunStore {
144
596
  const at = new Date(now).toISOString();
145
597
  this.state.connection
146
598
  .prepare(`UPDATE node_attempts
147
- SET status = 'cancelled', finished_at = ?, updated_at = ?
599
+ SET status = 'interrupted', updated_at = ?
148
600
  WHERE run_id = ? AND status IN ('pending', 'running', 'waiting')`)
149
- .run(now, now, runId);
601
+ .run(now, runId);
150
602
  insertRunEvent(this.state, row.resourceId, nextRevision, at, {
151
603
  scope: "run",
152
604
  type: "run_resume_prepared",
@@ -173,6 +625,9 @@ export class WorkflowRunStore {
173
625
  delete loaded.state.currentNode;
174
626
  delete loaded.state.currentAttemptId;
175
627
  delete loaded.state.currentNodeStartedAt;
628
+ delete loaded.state.currentSettingsScopeId;
629
+ delete loaded.state.currentSettingsChangeNumber;
630
+ delete loaded.state.currentSettingsHash;
176
631
  delete loaded.state.statusDetail;
177
632
  delete loaded.state.paused;
178
633
  await this.writeSnapshot(runId, loaded.state, {
@@ -230,9 +685,10 @@ export class WorkflowRunStore {
230
685
  const nextUpdateSeq = this.nextUpdateSequence(attemptId);
231
686
  this.state.connection
232
687
  .prepare(`INSERT INTO workflow_updates(
233
- update_id, attempt_id, update_seq, update_type, update_key, data_hash, recorded_at
234
- ) VALUES (?, ?, ?, ?, ?, ?, ?)`)
235
- .run(updateId, attemptId, nextUpdateSeq, update.type, update.key, dataHash, Date.now());
688
+ update_id, attempt_id, update_seq, run_revision,
689
+ update_type, update_key, data_hash, recorded_at
690
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
691
+ .run(updateId, attemptId, nextUpdateSeq, revision, update.type, update.key, dataHash, Date.now());
236
692
  insertRunEvent(this.state, run.resourceId, revision, at, event);
237
693
  this.persistRunState(run, state, revision, Date.now());
238
694
  context.revision = revision;
@@ -255,12 +711,14 @@ export class WorkflowRunStore {
255
711
  const revision = context.revision + 1;
256
712
  const now = Date.now();
257
713
  const at = new Date(now).toISOString();
714
+ const snapshot = this.readDefinition(run.definitionHash);
715
+ this.assertSettingsRouteCurrent(event, snapshot);
258
716
  const traceEvent = { seq: revision, at, runId, ...event };
259
717
  state.traceSeq = revision;
260
718
  state.updatedAt = at;
261
719
  insertRunEvent(this.state, run.resourceId, revision, at, traceEvent);
262
720
  this.persistRunState(run, state, revision, now);
263
- const snapshot = this.readDefinition(run.definitionHash);
721
+ this.transitionFollowUpsForRunState(state, event, now);
264
722
  this.syncNodeAttempts(state, snapshot, now);
265
723
  context.revision = revision;
266
724
  accepted = traceEvent;
@@ -316,6 +774,11 @@ export class WorkflowRunStore {
316
774
  VALUES (?, ?, 'interactive', ?)
317
775
  ON CONFLICT(run_id) DO NOTHING`)
318
776
  .run(runId, binding.piSessionId, now);
777
+ this.state.connection
778
+ .prepare(`UPDATE workflow_follow_up_queues
779
+ SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
780
+ WHERE run_id = ?`)
781
+ .run(binding.piSessionId, now, runId);
319
782
  const revision = context.revision + 1;
320
783
  const at = new Date(now).toISOString();
321
784
  insertRunEvent(this.state, run.resourceId, revision, at, {
@@ -329,7 +792,7 @@ export class WorkflowRunStore {
329
792
  ...(attemptId !== undefined ? { captureAttemptId: attemptId } : {}),
330
793
  },
331
794
  });
332
- const current = this.readState(run.stateHash);
795
+ const current = this.readRunState(run, this.readDefinition(run.definitionHash));
333
796
  current.traceSeq = revision;
334
797
  current.updatedAt = at;
335
798
  this.persistRunState(run, current, revision, now);
@@ -354,7 +817,7 @@ export class WorkflowRunStore {
354
817
  .prepare("UPDATE session_segments SET entry_count = ? WHERE segment_id = ?")
355
818
  .run(sequence, segment.segmentId);
356
819
  this.bumpResource(segmentResourceId(segment), revision, now);
357
- insertGenericEvent(this.state, segmentResourceId(segment), revision + 1, "session.entry_appended", "session", segment.sessionId, entryHash, now);
820
+ insertGenericEvent(this.state, segmentResourceId(segment), revision + 1, "session.entry_appended", "session", segment.sessionId, null, now);
358
821
  return sequence;
359
822
  });
360
823
  }
@@ -385,15 +848,9 @@ export class WorkflowRunStore {
385
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));
386
849
  expected += 1;
387
850
  }
388
- const now = Date.now();
389
851
  this.state.connection
390
852
  .prepare("UPDATE session_segments SET event_count = ? WHERE segment_id = ?")
391
853
  .run(expected - 1, current.segmentId);
392
- const resourceId = segmentResourceId(current);
393
- const revision = this.requireResourceRevision(resourceId);
394
- this.bumpResource(resourceId, revision, now);
395
- const payloadHash = this.state.putJson({ count: records.length, lastSeq: expected - 1 }, now);
396
- insertGenericEvent(this.state, resourceId, revision + 1, "session.events_appended", "session", current.sessionId, payloadHash, now);
397
854
  });
398
855
  }
399
856
  async writeSessionCapture(runId, capture, attemptId) {
@@ -432,8 +889,8 @@ export class WorkflowRunStore {
432
889
  const row = this.readRunRow(runId);
433
890
  if (row === undefined)
434
891
  return null;
435
- const state = this.readState(row.stateHash);
436
892
  const snapshot = this.readDefinition(row.definitionHash);
893
+ const state = this.readRunState(row, snapshot);
437
894
  const segments = this.segmentRows(runId).map((segment) => this.loadSegment(segment, state));
438
895
  const flat = segments.find((segment) => segment.attemptId === "") ?? emptySegment();
439
896
  return {
@@ -447,6 +904,8 @@ export class WorkflowRunStore {
447
904
  sessionCapture: flat.capture,
448
905
  sessionIntegrity: flat.integrity,
449
906
  sessionSegments: segments.filter((segment) => segment.attemptId !== ""),
907
+ settingsScopes: this.settingsScopesForRunView(runId),
908
+ followUpQueue: this.readFollowUpQueue(runId) ?? null,
450
909
  };
451
910
  }
452
911
  listRuns(options = {}) {
@@ -551,20 +1010,19 @@ export class WorkflowRunStore {
551
1010
  }
552
1011
  persistRunState(run, state, expectedRevision, now) {
553
1012
  this.bumpResource(run.resourceId, expectedRevision - 1, now);
554
- const stateHash = this.state.putJson(state, now);
1013
+ const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
555
1014
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
556
1015
  const update = this.state.connection
557
1016
  .prepare(`UPDATE runs
558
- SET title = ?, status = ?, paused = ?, status_detail = ?, output_hash = ?,
559
- error_hash = ?, updated_at = ?, finished_at = ?
1017
+ SET title = ?, status = ?, paused = ?, status_detail = ?,
1018
+ final_output_hash = ?, error_hash = ?, updated_at = ?, finished_at = ?
560
1019
  WHERE run_id = ?`)
561
- .run(state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, stateHash, errorHash, now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt), state.runId);
1020
+ .run(state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, finalOutputHash, errorHash, now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt), state.runId);
562
1021
  if (update.changes !== 1)
563
1022
  throw new Error(`Workflow run is missing: ${state.runId}`);
564
1023
  if (state.status !== "running") {
565
1024
  this.enqueueRunSettlementEffect(run.resourceId, expectedRevision, state, now);
566
1025
  }
567
- run.stateHash = stateHash;
568
1026
  }
569
1027
  enqueueRunSettlementEffect(runResourceId, sourceRevision, state, now) {
570
1028
  if (this.state.connection.prepare("SELECT 1 FROM run_queue WHERE run_id = ?").get(state.runId) ===
@@ -600,12 +1058,341 @@ export class WorkflowRunStore {
600
1058
  .run(effectId, effectResourceId, runResourceId, sourceRevision, effectType, idempotencyKey, payloadHash, now, now);
601
1059
  insertGenericEvent(this.state, effectResourceId, 1, "effect.created", "system", null, payloadHash, now);
602
1060
  }
1061
+ initializeRunSettingsAndFollowUps(state, initialSettings, now) {
1062
+ const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
1063
+ if (state.parentRunId !== undefined) {
1064
+ const parentQueue = this.followUpQueueRow(state.parentRunId);
1065
+ if (parentQueue !== undefined) {
1066
+ this.state.connection
1067
+ .prepare(`UPDATE workflow_follow_up_queues
1068
+ SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
1069
+ WHERE run_id = ?`)
1070
+ .run(parentQueue.originSessionId, now, state.runId);
1071
+ this.state.connection
1072
+ .prepare(`UPDATE workflow_follow_ups
1073
+ SET run_id = ?, queue_resource_id = ?, updated_at = ?
1074
+ WHERE run_id = ? AND status = 'queued'`)
1075
+ .run(state.runId, childQueueResourceId, now, state.parentRunId);
1076
+ }
1077
+ this.state.connection
1078
+ .prepare(`UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`)
1079
+ .run(state.runId, now, state.parentRunId);
1080
+ }
1081
+ const existing = this.state.connection
1082
+ .prepare("SELECT COUNT(*) AS count FROM workflow_settings WHERE active_run_id = ?")
1083
+ .get(state.runId);
1084
+ const hasSettings = isCountRow(existing) && existing.count > 0;
1085
+ if (hasSettings)
1086
+ return;
1087
+ for (const scope of initialSettings) {
1088
+ const originRunId = this.logicalOriginRunId(state.runId);
1089
+ const scopeId = workflowSettingsScopeId(originRunId, scope.mountPath, scope.invocation);
1090
+ const resourceId = this.mutations.ensureResource("settings", scopeId, now);
1091
+ const settingsHash = this.state.putJson(scope.settings, now);
1092
+ this.state.connection
1093
+ .prepare(`INSERT INTO workflow_settings(
1094
+ scope_id, resource_id, origin_run_id, active_run_id, mount_path, invocation,
1095
+ initial_hash, current_hash, created_at, updated_at
1096
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1097
+ ON CONFLICT(scope_id) DO NOTHING`)
1098
+ .run(scopeId, resourceId, originRunId, state.runId, scope.mountPath, scope.invocation, settingsHash, settingsHash, now, now);
1099
+ }
1100
+ }
1101
+ ensureFollowUpQueue(runId, now) {
1102
+ const resourceId = this.mutations.ensureResource("follow_up_queue", runId, now);
1103
+ this.state.connection
1104
+ .prepare(`INSERT INTO workflow_follow_up_queues(
1105
+ run_id, resource_id, created_at, updated_at
1106
+ ) VALUES (?, ?, ?, ?)
1107
+ ON CONFLICT(run_id) DO NOTHING`)
1108
+ .run(runId, resourceId, now, now);
1109
+ return resourceId;
1110
+ }
1111
+ logicalOriginRunId(runId) {
1112
+ let current = runId;
1113
+ for (let depth = 0; depth < 100; depth += 1) {
1114
+ const row = this.state.connection
1115
+ .prepare("SELECT parent_run_id AS parentRunId FROM runs WHERE run_id = ?")
1116
+ .get(current);
1117
+ if (!isParentRunRow(row))
1118
+ return current;
1119
+ if (row.parentRunId === null)
1120
+ return current;
1121
+ current = row.parentRunId;
1122
+ }
1123
+ throw new Error(`Workflow continuation chain is too deep for run ${runId}`);
1124
+ }
1125
+ requireRunAcceptsSettings(runId, allowWaiting) {
1126
+ const row = this.state.connection
1127
+ .prepare("SELECT status FROM runs WHERE run_id = ?")
1128
+ .get(runId);
1129
+ if (!isStatusRow(row))
1130
+ throw new Error(`Workflow run is missing: ${runId}`);
1131
+ if (row.status !== "running" && !(allowWaiting && row.status === "waiting")) {
1132
+ throw new Error(`Workflow run ${runId} does not accept changes with status ${row.status}`);
1133
+ }
1134
+ }
1135
+ verifySettingsProjections(runId) {
1136
+ const scopes = this.state.connection
1137
+ .prepare(`SELECT s.scope_id AS scopeId, s.initial_hash AS initialHash,
1138
+ s.current_hash AS currentHash, r.revision
1139
+ FROM workflow_settings s
1140
+ JOIN resources r ON r.resource_id = s.resource_id
1141
+ WHERE s.active_run_id = ?`)
1142
+ .all(runId);
1143
+ for (const value of scopes) {
1144
+ if (!isSettingsProjectionRow(value)) {
1145
+ throw new Error("Workflow settings projection row is invalid");
1146
+ }
1147
+ let current = this.state.readJson(value.initialHash);
1148
+ let currentHash = value.initialHash;
1149
+ const changes = this.state.connection
1150
+ .prepare(`SELECT change_number AS changeNumber, patch_hash AS patchHash,
1151
+ before_hash AS beforeHash, after_hash AS afterHash
1152
+ FROM workflow_setting_changes WHERE scope_id = ? ORDER BY change_number`)
1153
+ .all(value.scopeId);
1154
+ let expected = 1;
1155
+ for (const change of changes) {
1156
+ if (!isSettingsProjectionChangeRow(change) || change.changeNumber !== expected) {
1157
+ throw new Error(`Workflow settings changes are not contiguous for ${value.scopeId}`);
1158
+ }
1159
+ if (!change.beforeHash.equals(currentHash)) {
1160
+ throw new Error(`Workflow settings change has a wrong old value for ${value.scopeId}`);
1161
+ }
1162
+ current = applyJsonPatch(current, this.state.readJson(change.patchHash));
1163
+ currentHash = createHash("sha256").update(canonicalJson(current)).digest();
1164
+ if (!currentHash.equals(change.afterHash)) {
1165
+ throw new Error(`Workflow settings change has a wrong new value for ${value.scopeId}`);
1166
+ }
1167
+ expected += 1;
1168
+ }
1169
+ if (value.revision !== expected - 1 || !value.currentHash.equals(currentHash)) {
1170
+ throw new Error(`Workflow settings projection does not match saved changes for ${value.scopeId}`);
1171
+ }
1172
+ }
1173
+ }
1174
+ settingsScopeRow(scopeId) {
1175
+ const row = this.state.connection
1176
+ .prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
1177
+ s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
1178
+ s.mount_path AS mountPath, s.invocation,
1179
+ s.current_hash AS currentHash, r.revision,
1180
+ s.created_at AS createdAt, s.updated_at AS updatedAt
1181
+ FROM workflow_settings s
1182
+ JOIN resources r ON r.resource_id = s.resource_id
1183
+ WHERE s.scope_id = ?`)
1184
+ .get(scopeId);
1185
+ return isSettingsScopeRow(row) ? row : undefined;
1186
+ }
1187
+ requireSettingsScopeRow(scopeId) {
1188
+ const row = this.settingsScopeRow(scopeId);
1189
+ if (row === undefined)
1190
+ throw new Error(`Workflow settings scope not found: ${scopeId}`);
1191
+ return row;
1192
+ }
1193
+ settingsScopeRecord(row, changeNumber = row.revision, settingsHash = row.currentHash) {
1194
+ return {
1195
+ scopeId: row.scopeId,
1196
+ originRunId: row.originRunId,
1197
+ activeRunId: row.activeRunId,
1198
+ mountPath: row.mountPath,
1199
+ invocation: row.invocation,
1200
+ changeNumber,
1201
+ settings: this.state.readJson(settingsHash),
1202
+ settingsHash: settingsHash.toString("hex"),
1203
+ createdAt: new Date(row.createdAt).toISOString(),
1204
+ updatedAt: new Date(row.updatedAt).toISOString(),
1205
+ };
1206
+ }
1207
+ settingsChangeRow(scopeId, requestId) {
1208
+ const row = this.state.connection
1209
+ .prepare(`SELECT change_id AS changeId, scope_id AS scopeId, request_id AS requestId,
1210
+ change_number AS changeNumber, actor_type AS actorType, actor_id AS actorId,
1211
+ source_type AS sourceType, patch_hash AS patchHash,
1212
+ before_hash AS beforeHash, after_hash AS afterHash,
1213
+ accepted_at AS acceptedAt
1214
+ FROM workflow_setting_changes WHERE scope_id = ? AND request_id = ?`)
1215
+ .get(scopeId, requestId);
1216
+ return isSettingsChangeRow(row) ? row : undefined;
1217
+ }
1218
+ settingsChangeRecord(row) {
1219
+ return {
1220
+ changeId: row.changeId,
1221
+ scopeId: row.scopeId,
1222
+ requestId: row.requestId,
1223
+ changeNumber: row.changeNumber,
1224
+ actor: {
1225
+ type: assertActorType(row.actorType),
1226
+ ...(row.actorId !== null ? { id: row.actorId } : {}),
1227
+ },
1228
+ source: row.sourceType,
1229
+ patch: validateJsonPatch(this.state.readJson(row.patchHash)),
1230
+ beforeHash: row.beforeHash.toString("hex"),
1231
+ afterHash: row.afterHash.toString("hex"),
1232
+ acceptedAt: new Date(row.acceptedAt).toISOString(),
1233
+ };
1234
+ }
1235
+ assertSameSettingsChange(row, request, patchDigest) {
1236
+ if (!row.patchHash.equals(patchDigest) ||
1237
+ row.actorType !== request.actor.type ||
1238
+ row.actorId !== (request.actor.id ?? null) ||
1239
+ row.sourceType !== request.source) {
1240
+ throw new Error(`Workflow settings request ID was reused with different content`);
1241
+ }
1242
+ }
1243
+ followUpQueueRow(runId) {
1244
+ const row = this.state.connection
1245
+ .prepare(`SELECT q.run_id AS runId, q.resource_id AS resourceId,
1246
+ q.origin_session_id AS originSessionId,
1247
+ q.presentation_state AS presentationState,
1248
+ q.presentation_entry_id AS presentationEntryId,
1249
+ q.presentation_assistant_entry_id AS presentationAssistantEntryId,
1250
+ q.presentation_reason_hash AS presentationReasonHash,
1251
+ r.revision
1252
+ FROM workflow_follow_up_queues q
1253
+ JOIN resources r ON r.resource_id = q.resource_id
1254
+ WHERE q.run_id = ?`)
1255
+ .get(runId);
1256
+ return isFollowUpQueueRow(row) ? row : undefined;
1257
+ }
1258
+ requireFollowUpQueueRow(runId) {
1259
+ const row = this.followUpQueueRow(runId);
1260
+ if (row === undefined)
1261
+ throw new Error(`Workflow follow-up queue not found: ${runId}`);
1262
+ return row;
1263
+ }
1264
+ followUpRowByRequest(runId, requestId) {
1265
+ const row = this.state.connection
1266
+ .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
1267
+ .get(runId, requestId);
1268
+ return isFollowUpRow(row) ? row : undefined;
1269
+ }
1270
+ followUpRow(followUpId) {
1271
+ const row = this.state.connection
1272
+ .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.follow_up_id = ?`)
1273
+ .get(followUpId);
1274
+ return isFollowUpRow(row) ? row : undefined;
1275
+ }
1276
+ requireFollowUpRow(followUpId) {
1277
+ const row = this.followUpRow(followUpId);
1278
+ if (row === undefined)
1279
+ throw new Error(`Workflow follow-up not found: ${followUpId}`);
1280
+ return row;
1281
+ }
1282
+ followUpRecord(row) {
1283
+ return {
1284
+ followUpId: row.followUpId,
1285
+ runId: row.runId,
1286
+ requestId: row.requestId,
1287
+ order: row.orderNumber,
1288
+ targetSessionId: row.targetSessionId,
1289
+ actor: {
1290
+ type: assertActorType(row.actorType),
1291
+ ...(row.actorId !== null ? { id: row.actorId } : {}),
1292
+ },
1293
+ source: row.sourceType,
1294
+ prompt: this.readText(row.promptHash),
1295
+ state: assertFollowUpState(row.status),
1296
+ ...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
1297
+ ...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
1298
+ createdAt: new Date(row.createdAt).toISOString(),
1299
+ updatedAt: new Date(row.updatedAt).toISOString(),
1300
+ ...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
1301
+ };
1302
+ }
1303
+ assertSameFollowUp(row, request) {
1304
+ const promptHash = createHash("sha256").update(request.prompt).digest();
1305
+ if (!row.promptHash.equals(promptHash) ||
1306
+ row.targetSessionId !== request.targetSessionId ||
1307
+ row.actorType !== request.actor.type ||
1308
+ row.actorId !== (request.actor.id ?? null) ||
1309
+ row.sourceType !== request.source) {
1310
+ throw new Error("Workflow follow-up request ID was reused with different content");
1311
+ }
1312
+ }
1313
+ assertFollowUpRemovalAuthority(row, actor, source) {
1314
+ if (actor.type === "human")
1315
+ return;
1316
+ if ((actor.type === "session" || actor.type === "controller") &&
1317
+ row.actorType === actor.type &&
1318
+ row.actorId === (actor.id ?? null) &&
1319
+ row.sourceType === source) {
1320
+ return;
1321
+ }
1322
+ throw new Error("Workflow follow-up can be removed only by its source or a verified human");
1323
+ }
1324
+ hasLiveLease(resourceId) {
1325
+ const row = this.state.connection
1326
+ .prepare("SELECT expires_at AS expiresAt FROM leases WHERE resource_id = ?")
1327
+ .get(resourceId);
1328
+ return isLeaseExpiryRow(row) && row.expiresAt !== null && row.expiresAt > Date.now();
1329
+ }
1330
+ nextFollowUpOrder(runId) {
1331
+ const row = this.state.connection
1332
+ .prepare(`SELECT COALESCE(MAX(order_number), 0) + 1 AS nextOrder
1333
+ FROM workflow_follow_ups WHERE run_id = ?`)
1334
+ .get(runId);
1335
+ if (!isNextOrderRow(row))
1336
+ throw new Error("Could not allocate workflow follow-up order");
1337
+ return row.nextOrder;
1338
+ }
1339
+ assertSettingsRouteCurrent(event, snapshot) {
1340
+ if (event.type !== "node_finished" || event.nodeId === undefined)
1341
+ return;
1342
+ if (snapshot.nodes[event.nodeId]?.settingsRoute !== true)
1343
+ return;
1344
+ const scopeId = event.payload.settingsScopeId;
1345
+ const changeNumber = event.payload.settingsChangeNumber;
1346
+ if (typeof scopeId !== "string" || typeof changeNumber !== "number") {
1347
+ throw new Error(`Settings route ${event.nodeId} has no saved settings binding`);
1348
+ }
1349
+ const scope = this.requireSettingsScopeRow(scopeId);
1350
+ if (scope.revision !== changeNumber) {
1351
+ throw new StaleResourceError(`Settings route ${event.nodeId} used change ${changeNumber}, current change is ${scope.revision}`);
1352
+ }
1353
+ }
1354
+ transitionFollowUpsForRunState(state, event, now) {
1355
+ if (state.status === "running" || state.status === "waiting")
1356
+ return;
1357
+ const queue = this.followUpQueueRow(state.runId);
1358
+ if (queue === undefined || queue.presentationState !== "none")
1359
+ return;
1360
+ let presentationState = "not-needed";
1361
+ let followUpState = "cancelled";
1362
+ let reasonHash = null;
1363
+ if (state.status === "completed") {
1364
+ const required = event.payload.presentationRequired === true;
1365
+ presentationState = required ? "pending" : "not-needed";
1366
+ followUpState = required ? "pending_presentation" : "ready";
1367
+ }
1368
+ else {
1369
+ reasonHash = this.state.putText(state.error ?? `Workflow ended with status ${state.status}`, now);
1370
+ }
1371
+ this.state.connection
1372
+ .prepare(`UPDATE workflow_follow_up_queues
1373
+ SET presentation_state = ?, presentation_updated_at = ?, updated_at = ?
1374
+ WHERE run_id = ? AND presentation_state = 'none'`)
1375
+ .run(presentationState, now, now, state.runId);
1376
+ this.state.connection
1377
+ .prepare(`UPDATE workflow_follow_ups
1378
+ SET status = ?, reason_hash = ?, updated_at = ?
1379
+ WHERE run_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`)
1380
+ .run(followUpState, reasonHash, now, state.runId);
1381
+ const revision = this.requireResourceRevision(queue.resourceId);
1382
+ this.bumpResource(queue.resourceId, revision, now);
1383
+ const payloadHash = this.state.putJson({ runId: state.runId, state: presentationState, followUps: followUpState }, now);
1384
+ insertGenericEvent(this.state, queue.resourceId, revision + 1, "follow-up.run-finished", "system", null, payloadHash, now);
1385
+ }
603
1386
  syncNodeAttempts(state, snapshot, now) {
604
1387
  for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
605
- const resultHash = this.state.putJson(step, now);
1388
+ const promptEntryId = this.findAttemptPromptEntry(state.runId, step.attemptId);
1389
+ const promptHash = promptEntryId === undefined && step.prompt !== null
1390
+ ? this.state.putText(step.prompt, now)
1391
+ : null;
1392
+ const receipt = attemptReceipt(step);
1393
+ const receiptHash = Object.keys(receipt).length === 0 ? null : this.state.putJson(receipt, now);
606
1394
  const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
607
1395
  const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
608
- const promptHash = step.prompt === null ? null : this.state.putJson(step.prompt, now);
609
1396
  const existing = this.state.connection
610
1397
  .prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
611
1398
  .get(step.attemptId);
@@ -614,23 +1401,133 @@ export class WorkflowRunStore {
614
1401
  this.state.connection
615
1402
  .prepare(`INSERT INTO node_attempts(
616
1403
  attempt_id, run_id, node_id, attempt_number, node_type, status,
617
- presentation_hash, output_hash, result_hash, error_hash,
1404
+ prompt_hash, output_hash, receipt_hash, error_hash,
1405
+ settings_scope_id, settings_change_number, settings_hash,
618
1406
  started_at, finished_at, created_at, updated_at
619
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
620
- .run(step.attemptId, state.runId, step.nodeId, attemptNumber, step.nodeType, outcomeStatus(step.outcome), promptHash, outputHash, resultHash, errorHash, Date.parse(step.startedAt), Date.parse(step.finishedAt), Date.parse(step.startedAt), now);
1407
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1408
+ .run(step.attemptId, state.runId, step.nodeId, attemptNumber, step.nodeType, outcomeStatus(step.outcome), promptHash, outputHash, receiptHash, errorHash, step.settingsScopeId ?? null, step.settingsChangeNumber ?? null, step.settingsHash === undefined ? null : Buffer.from(step.settingsHash, "hex"), Date.parse(step.startedAt), Date.parse(step.finishedAt), Date.parse(step.startedAt), now);
621
1409
  }
622
1410
  else {
623
1411
  this.state.connection
624
1412
  .prepare(`UPDATE node_attempts
625
- SET status = ?, presentation_hash = ?, output_hash = ?, result_hash = ?,
626
- error_hash = ?, finished_at = ?, updated_at = ?
1413
+ SET status = ?, prompt_hash = ?, output_hash = ?, receipt_hash = ?, error_hash = ?,
1414
+ settings_scope_id = ?, settings_change_number = ?,
1415
+ settings_hash = ?, finished_at = ?, updated_at = ?
627
1416
  WHERE attempt_id = ?`)
628
- .run(outcomeStatus(step.outcome), promptHash, outputHash, resultHash, errorHash, Date.parse(step.finishedAt), now, step.attemptId);
1417
+ .run(outcomeStatus(step.outcome), promptHash, outputHash, receiptHash, errorHash, step.settingsScopeId ?? null, step.settingsChangeNumber ?? null, step.settingsHash === undefined ? null : Buffer.from(step.settingsHash, "hex"), Date.parse(step.finishedAt), now, step.attemptId);
1418
+ }
1419
+ if (this.syncAttemptEntries(state.runId, step, promptEntryId)) {
1420
+ this.state.connection
1421
+ .prepare("UPDATE node_attempts SET output_hash = NULL WHERE attempt_id = ?")
1422
+ .run(step.attemptId);
629
1423
  }
630
1424
  }
631
1425
  if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
632
1426
  this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
633
1427
  }
1428
+ this.syncRunSteps(state, now);
1429
+ }
1430
+ syncRunSteps(state, now) {
1431
+ const existingRows = this.state.connection
1432
+ .prepare(`SELECT step_index AS stepIndex, attempt_id AS attemptId
1433
+ FROM run_steps WHERE run_id = ? ORDER BY step_index`)
1434
+ .all(state.runId)
1435
+ .filter(isRunStepIdentityRow);
1436
+ if (existingRows.length > state.steps.length) {
1437
+ throw new Error(`Workflow run steps cannot shrink: ${state.runId}`);
1438
+ }
1439
+ for (const existing of existingRows) {
1440
+ if (state.steps[existing.stepIndex]?.attemptId !== existing.attemptId) {
1441
+ throw new Error(`Workflow run step history changed at index ${existing.stepIndex}`);
1442
+ }
1443
+ }
1444
+ for (let stepIndex = existingRows.length; stepIndex < state.steps.length; stepIndex += 1) {
1445
+ const step = state.steps[stepIndex];
1446
+ /* istanbul ignore if -- array index follows a checked bound */
1447
+ if (step === undefined)
1448
+ throw new Error("Workflow run step became unavailable");
1449
+ const attemptOutput = this.readAttemptOutput(step.attemptId);
1450
+ const carried = stepIndex < (state.carriedStepCount ?? 0);
1451
+ const outputOverrideHash = carried && canonicalJson(attemptOutput) !== canonicalJson(step.output)
1452
+ ? this.state.putJson(step.output, now)
1453
+ : null;
1454
+ this.state.connection
1455
+ .prepare(`INSERT INTO run_steps(run_id, step_index, attempt_id, output_override_hash)
1456
+ VALUES (?, ?, ?, ?)`)
1457
+ .run(state.runId, stepIndex, step.attemptId, outputOverrideHash);
1458
+ }
1459
+ }
1460
+ syncAttemptEntries(runId, step, promptEntryId) {
1461
+ const links = [];
1462
+ if (promptEntryId !== undefined)
1463
+ links.push(["prompt", promptEntryId]);
1464
+ if (step.conversation !== undefined) {
1465
+ links.push(["first", step.conversation.firstEntryId], ["last", step.conversation.lastEntryId]);
1466
+ }
1467
+ if (step.assistantMessage?.entryId !== undefined) {
1468
+ links.push(["response", step.assistantMessage.entryId]);
1469
+ }
1470
+ let responseLinked = false;
1471
+ for (const [role, entryId] of links) {
1472
+ const row = this.state.connection
1473
+ .prepare(`SELECT e.segment_id AS segmentId, e.entry_hash AS entryHash
1474
+ FROM session_entries e
1475
+ JOIN session_segments s ON s.segment_id = e.segment_id
1476
+ WHERE s.run_id = ? AND e.entry_id = ?
1477
+ ORDER BY e.recorded_at DESC LIMIT 1`)
1478
+ .get(runId, entryId);
1479
+ if (!isSegmentIdentityRow(row))
1480
+ continue;
1481
+ if (role === "response") {
1482
+ const output = assistantOutputFromEntry(this.state.readJson(row.entryHash), step.assistantMessage?.maxChars);
1483
+ if (canonicalJson(output) !== canonicalJson(step.output) ||
1484
+ createHash("sha256").update(output).digest("hex") !== step.assistantMessage?.sha256) {
1485
+ throw new Error(`Assistant response entry conflicts with attempt ${step.attemptId}`);
1486
+ }
1487
+ }
1488
+ this.state.connection
1489
+ .prepare(`INSERT INTO attempt_entries(attempt_id, role, segment_id, entry_id)
1490
+ VALUES (?, ?, ?, ?)
1491
+ ON CONFLICT(attempt_id, role) DO UPDATE SET
1492
+ segment_id = excluded.segment_id, entry_id = excluded.entry_id`)
1493
+ .run(step.attemptId, role, row.segmentId, entryId);
1494
+ if (role === "response")
1495
+ responseLinked = true;
1496
+ }
1497
+ return responseLinked;
1498
+ }
1499
+ findAttemptPromptEntry(runId, attemptId) {
1500
+ const row = this.state.connection
1501
+ .prepare(`SELECT e.entry_id AS entryId
1502
+ FROM session_entries e
1503
+ JOIN session_segments s ON s.segment_id = e.segment_id
1504
+ JOIN blobs b ON b.blob_hash = e.entry_hash
1505
+ WHERE s.run_id = ?
1506
+ AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-agent-step'
1507
+ AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
1508
+ ORDER BY e.recorded_at DESC LIMIT 1`)
1509
+ .get(runId, attemptId);
1510
+ return isEntryIdentityRow(row) ? row.entryId : undefined;
1511
+ }
1512
+ readAttemptOutput(attemptId) {
1513
+ const row = this.state.connection
1514
+ .prepare(`SELECT a.output_hash AS outputHash, a.receipt_hash AS receiptHash,
1515
+ e.entry_hash AS responseEntryHash
1516
+ FROM node_attempts a
1517
+ LEFT JOIN attempt_entries l ON l.attempt_id = a.attempt_id AND l.role = 'response'
1518
+ LEFT JOIN session_entries e
1519
+ ON e.segment_id = l.segment_id AND e.entry_id = l.entry_id
1520
+ WHERE a.attempt_id = ?`)
1521
+ .get(attemptId);
1522
+ if (!isAttemptValueRow(row)) {
1523
+ throw new Error(`Workflow node attempt is missing: ${attemptId}`);
1524
+ }
1525
+ if (row.outputHash !== null)
1526
+ return this.state.readJson(row.outputHash);
1527
+ if (row.responseEntryHash === null)
1528
+ return null;
1529
+ const receipt = row.receiptHash === null ? {} : this.readJsonAs(row.receiptHash);
1530
+ return assistantOutputFromEntry(this.state.readJson(row.responseEntryHash), receipt.assistantMessage?.maxChars);
634
1531
  }
635
1532
  ensureAttempt(state, nodeId, attemptId, now, snapshot) {
636
1533
  const existing = this.state.connection
@@ -639,18 +1536,31 @@ export class WorkflowRunStore {
639
1536
  const status = state.status === "waiting" ? "waiting" : "running";
640
1537
  if (existing !== undefined) {
641
1538
  this.state.connection
642
- .prepare("UPDATE node_attempts SET status = ?, updated_at = ? WHERE attempt_id = ?")
643
- .run(status, now, attemptId);
1539
+ .prepare(`UPDATE node_attempts
1540
+ SET status = ?, settings_scope_id = ?, settings_change_number = ?,
1541
+ settings_hash = ?, updated_at = ?
1542
+ WHERE attempt_id = ?`)
1543
+ .run(status, state.currentSettingsScopeId ?? null, state.currentSettingsChangeNumber ?? null, state.currentSettingsHash === undefined
1544
+ ? null
1545
+ : Buffer.from(state.currentSettingsHash, "hex"), now, attemptId);
644
1546
  return;
645
1547
  }
646
1548
  const definition = snapshot ?? this.readDefinition(this.requireRunRow(state.runId).definitionHash);
647
1549
  const nodeType = definition.nodes[nodeId]?.nodeType ?? "agent";
1550
+ this.state.connection
1551
+ .prepare(`UPDATE node_attempts
1552
+ SET status = 'cancelled', finished_at = COALESCE(finished_at, ?), updated_at = ?
1553
+ WHERE run_id = ? AND status = 'interrupted'`)
1554
+ .run(now, now, state.runId);
648
1555
  this.state.connection
649
1556
  .prepare(`INSERT INTO node_attempts(
650
1557
  attempt_id, run_id, node_id, attempt_number, node_type, status,
1558
+ settings_scope_id, settings_change_number, settings_hash,
651
1559
  started_at, created_at, updated_at
652
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
653
- .run(attemptId, state.runId, nodeId, this.nextAttemptNumber(state.runId, nodeId), nodeType, status, state.currentNodeStartedAt === undefined ? now : Date.parse(state.currentNodeStartedAt), now, now);
1560
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1561
+ .run(attemptId, state.runId, nodeId, this.nextAttemptNumber(state.runId, nodeId), nodeType, status, state.currentSettingsScopeId ?? null, state.currentSettingsChangeNumber ?? null, state.currentSettingsHash === undefined
1562
+ ? null
1563
+ : Buffer.from(state.currentSettingsHash, "hex"), state.currentNodeStartedAt === undefined ? now : Date.parse(state.currentNodeStartedAt), now, now);
654
1564
  }
655
1565
  nextAttemptNumber(runId, nodeId) {
656
1566
  const row = this.state.connection
@@ -672,11 +1582,46 @@ export class WorkflowRunStore {
672
1582
  throw new Error("Could not allocate workflow update sequence");
673
1583
  return row.updateSeq;
674
1584
  }
1585
+ insertRunSources(runId, root, mounted) {
1586
+ const insert = this.state.connection.prepare(`INSERT INTO run_sources(run_id, mount_path, source_type, source_ref, source_revision)
1587
+ VALUES (?, ?, ?, ?, ?)`);
1588
+ insert.run(runId, "", root.type, root.ref, root.revision);
1589
+ for (const item of mounted) {
1590
+ const source = sourceParts(item.source);
1591
+ insert.run(runId, item.mountPath.join("/"), source.type, source.ref, source.revision);
1592
+ }
1593
+ }
1594
+ syncContinuationIdentity(state) {
1595
+ if (state.parentRunId === undefined || state.humanDecision === undefined)
1596
+ return;
1597
+ const createdAt = Date.parse(state.humanDecision.acceptedAt);
1598
+ this.state.connection
1599
+ .prepare(`INSERT INTO continuations(
1600
+ decision_id, parent_run_id, continuation_run_id, created_at
1601
+ ) VALUES (?, ?, ?, ?)
1602
+ ON CONFLICT(decision_id) DO NOTHING`)
1603
+ .run(state.humanDecision.decisionId, state.parentRunId, state.runId, createdAt);
1604
+ const row = this.state.connection
1605
+ .prepare(`SELECT parent_run_id AS parentRunId, continuation_run_id AS continuationRunId,
1606
+ created_at AS createdAt
1607
+ FROM continuations WHERE decision_id = ?`)
1608
+ .get(state.humanDecision.decisionId);
1609
+ if (!isContinuationIdentityRow(row) ||
1610
+ row.parentRunId !== state.parentRunId ||
1611
+ row.continuationRunId !== state.runId ||
1612
+ row.createdAt !== createdAt) {
1613
+ throw new Error("Immutable human decision continuation conflicts");
1614
+ }
1615
+ }
675
1616
  readRunRow(runId) {
676
1617
  const row = this.state.connection
677
1618
  .prepare(`SELECT r.run_id AS runId, r.resource_id AS resourceId,
678
- r.output_hash AS stateHash, d.definition_hash AS definitionHash,
679
- r.status
1619
+ d.definition_hash AS definitionHash, r.definition_digest AS definitionDigest,
1620
+ d.workflow_name AS workflowRef, r.parent_run_id AS parentRunId,
1621
+ r.title, r.status, r.paused, r.status_detail AS statusDetail,
1622
+ r.input_hash AS inputHash, r.final_output_hash AS finalOutputHash,
1623
+ r.error_hash AS errorHash,
1624
+ r.created_at AS createdAt, r.updated_at AS updatedAt, r.finished_at AS finishedAt
680
1625
  FROM runs r
681
1626
  JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
682
1627
  WHERE r.run_id = ?`)
@@ -689,12 +1634,259 @@ export class WorkflowRunStore {
689
1634
  throw new Error(`Workflow run is missing: ${runId}`);
690
1635
  return row;
691
1636
  }
692
- readState(hash) {
693
- const value = this.state.readJson(hash);
694
- if (!isRecord(value) || value.schema !== RUN_STATE_SCHEMA) {
695
- throw new Error("Workflow run state has an incompatible schema");
1637
+ readRunState(row, snapshot) {
1638
+ const steps = this.readSteps(row.runId);
1639
+ const sources = this.readRunSources(row.runId, snapshot);
1640
+ const carriedStepCount = this.carriedStepCount(row.runId);
1641
+ const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
1642
+ const humanDecision = this.readHumanDecisionReceipt(row.runId);
1643
+ const outputs = {};
1644
+ const results = {};
1645
+ for (const step of steps) {
1646
+ const result = resultForStep(step);
1647
+ results[step.nodeId] = result;
1648
+ if (step.outcome === "ok")
1649
+ outputs[step.nodeId] = step.output;
1650
+ else
1651
+ delete outputs[step.nodeId];
1652
+ const node = snapshot.nodes[step.nodeId];
1653
+ const mountPath = node?.includeTransition === "exit" && node.mountPath !== undefined
1654
+ ? node.mountPath.join("/")
1655
+ : undefined;
1656
+ if (mountPath !== undefined && step.outcome === "ok") {
1657
+ outputs[mountPath] = step.output;
1658
+ results[mountPath] = { ...result, nodeId: mountPath };
1659
+ }
696
1660
  }
697
- return value;
1661
+ const revision = this.requireResourceRevision(row.resourceId);
1662
+ return {
1663
+ schema: RUN_STATE_SCHEMA,
1664
+ traceSeq: revision,
1665
+ runId: row.runId,
1666
+ workflowName: row.workflowRef,
1667
+ ...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
1668
+ ...(carriedStepCount === 0 ? {} : { carriedStepCount }),
1669
+ ...(row.title === null ? {} : { runTitle: row.title }),
1670
+ ...(sources.root === undefined ? {} : { workflowSource: sources.root }),
1671
+ ...(sources.mounted.length === 0 ? {} : { workflowSources: sources.mounted }),
1672
+ ...(sources.mounted.length !== 0 || snapshot.composition?.mounts.length
1673
+ ? { definitionDigest: `sha256:${row.definitionDigest.toString("hex")}` }
1674
+ : {}),
1675
+ startedAt: new Date(row.createdAt).toISOString(),
1676
+ ...(row.finishedAt === null ? {} : { finishedAt: new Date(row.finishedAt).toISOString() }),
1677
+ updatedAt: new Date(row.updatedAt).toISOString(),
1678
+ status: row.status,
1679
+ input: this.state.readJson(row.inputHash),
1680
+ outputs,
1681
+ results,
1682
+ steps,
1683
+ ...this.readUpdates(row.runId),
1684
+ ...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
1685
+ ...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
1686
+ ...(activeAttempt?.startedAt === null || activeAttempt === undefined
1687
+ ? {}
1688
+ : { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
1689
+ ...savedCurrentSettingsBinding(activeAttempt?.settingsScopeId ?? null, activeAttempt?.settingsChangeNumber ?? null, activeAttempt?.settingsHash ?? null),
1690
+ ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
1691
+ ...(humanDecision === undefined ? {} : { humanDecision }),
1692
+ ...(row.paused === 0 ? {} : { paused: true }),
1693
+ ...(row.status === "waiting" && steps.at(-1) !== undefined
1694
+ ? { waitingOn: steps.at(-1)?.nodeId }
1695
+ : {}),
1696
+ ...(row.finalOutputHash === null
1697
+ ? {}
1698
+ : { finalOutput: this.state.readJson(row.finalOutputHash) }),
1699
+ ...(row.errorHash === null ? {} : { error: this.readText(row.errorHash) }),
1700
+ };
1701
+ }
1702
+ readSteps(runId) {
1703
+ const rows = this.state.connection
1704
+ .prepare(`SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
1705
+ a.node_type AS nodeType, a.status,
1706
+ a.prompt_hash AS promptHash, a.output_hash AS outputHash,
1707
+ s.output_override_hash AS outputOverrideHash,
1708
+ a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
1709
+ prompt_entry.entry_hash AS promptEntryHash,
1710
+ response_entry.entry_hash AS responseEntryHash,
1711
+ first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
1712
+ a.settings_scope_id AS settingsScopeId,
1713
+ a.settings_change_number AS settingsChangeNumber,
1714
+ a.settings_hash AS settingsHash,
1715
+ a.started_at AS startedAt, a.finished_at AS finishedAt
1716
+ FROM run_steps s
1717
+ JOIN node_attempts a ON a.attempt_id = s.attempt_id
1718
+ LEFT JOIN attempt_entries prompt_link
1719
+ ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
1720
+ LEFT JOIN session_entries prompt_entry
1721
+ ON prompt_entry.segment_id = prompt_link.segment_id
1722
+ AND prompt_entry.entry_id = prompt_link.entry_id
1723
+ LEFT JOIN attempt_entries response_link
1724
+ ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
1725
+ LEFT JOIN session_entries response_entry
1726
+ ON response_entry.segment_id = response_link.segment_id
1727
+ AND response_entry.entry_id = response_link.entry_id
1728
+ LEFT JOIN attempt_entries first_link
1729
+ ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
1730
+ LEFT JOIN attempt_entries last_link
1731
+ ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'
1732
+ WHERE s.run_id = ? ORDER BY s.step_index`)
1733
+ .all(runId)
1734
+ .filter(isStepRow);
1735
+ return rows.map((row, index) => {
1736
+ if (row.stepIndex !== index)
1737
+ throw new Error(`Workflow run step sequence has a gap: ${runId}`);
1738
+ const receipt = row.receiptHash === null ? {} : this.readJsonAs(row.receiptHash);
1739
+ const prompt = row.promptEntryHash !== null
1740
+ ? promptFromEntry(this.state.readJson(row.promptEntryHash))
1741
+ : row.promptHash === null
1742
+ ? null
1743
+ : this.readText(row.promptHash);
1744
+ const outputHash = row.outputOverrideHash ?? row.outputHash;
1745
+ const output = outputHash !== null
1746
+ ? this.state.readJson(outputHash)
1747
+ : row.responseEntryHash === null
1748
+ ? null
1749
+ : assistantOutputFromEntry(this.state.readJson(row.responseEntryHash), receipt.assistantMessage?.maxChars);
1750
+ const error = row.errorHash === null ? undefined : this.readText(row.errorHash);
1751
+ const conversation = row.firstEntryId === null || row.lastEntryId === null
1752
+ ? undefined
1753
+ : { firstEntryId: row.firstEntryId, lastEntryId: row.lastEntryId };
1754
+ return {
1755
+ attemptId: row.attemptId,
1756
+ nodeId: row.nodeId,
1757
+ nodeType: row.nodeType,
1758
+ outcome: outcomeForStatus(row.status),
1759
+ startedAt: new Date(row.startedAt).toISOString(),
1760
+ finishedAt: new Date(row.finishedAt).toISOString(),
1761
+ prompt,
1762
+ output,
1763
+ ...(error === undefined ? {} : { error }),
1764
+ ...savedStepSettingsBinding(row.settingsScopeId, row.settingsChangeNumber, row.settingsHash),
1765
+ ...receipt,
1766
+ ...(conversation === undefined ? {} : { conversation }),
1767
+ };
1768
+ });
1769
+ }
1770
+ readRunSources(runId, snapshot) {
1771
+ const rows = this.state.connection
1772
+ .prepare(`SELECT mount_path AS mountPath, source_type AS sourceType,
1773
+ source_ref AS sourceRef, source_revision AS sourceRevision
1774
+ FROM run_sources WHERE run_id = ? ORDER BY mount_path`)
1775
+ .all(runId)
1776
+ .filter(isRunSourceRow);
1777
+ const rootRow = rows.find((row) => row.mountPath === "");
1778
+ const root = rootRow === undefined ||
1779
+ (rootRow.sourceType === "file" && rootRow.sourceRef.startsWith("inline:"))
1780
+ ? undefined
1781
+ : workflowSourceFromRow(rootRow);
1782
+ const mountNames = new Map((snapshot.composition?.mounts ?? []).map((mount) => [
1783
+ mount.mountPath.join("/"),
1784
+ mount.workflowName,
1785
+ ]));
1786
+ const mounted = rows
1787
+ .filter((row) => row.mountPath !== "")
1788
+ .map((row) => ({
1789
+ mountPath: row.mountPath.split("/"),
1790
+ workflowName: mountNames.get(row.mountPath) ?? row.mountPath,
1791
+ source: workflowSourceFromRow(row),
1792
+ }));
1793
+ return { ...(root === undefined ? {} : { root }), mounted };
1794
+ }
1795
+ carriedStepCount(runId) {
1796
+ const row = this.state.connection
1797
+ .prepare(`SELECT count(*) AS count
1798
+ FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
1799
+ WHERE s.run_id = ? AND a.run_id <> s.run_id`)
1800
+ .get(runId);
1801
+ return isCountRow(row) ? row.count : 0;
1802
+ }
1803
+ readActiveAttempt(runId) {
1804
+ const row = this.state.connection
1805
+ .prepare(`SELECT attempt_id AS attemptId, node_id AS nodeId, started_at AS startedAt,
1806
+ settings_scope_id AS settingsScopeId,
1807
+ settings_change_number AS settingsChangeNumber,
1808
+ settings_hash AS settingsHash
1809
+ FROM node_attempts
1810
+ WHERE run_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
1811
+ .get(runId);
1812
+ return isActiveAttemptRow(row) ? row : undefined;
1813
+ }
1814
+ readHumanDecisionReceipt(runId) {
1815
+ const row = this.state.connection
1816
+ .prepare(`SELECT d.request_hash AS requestHash, r.response_hash AS responseHash
1817
+ FROM continuations c
1818
+ JOIN human_decisions d ON d.decision_id = c.decision_id
1819
+ JOIN human_decision_resolutions r ON r.decision_id = c.decision_id
1820
+ WHERE c.continuation_run_id = ? AND r.outcome = 'accepted'`)
1821
+ .get(runId);
1822
+ if (!isDecisionReceiptRow(row))
1823
+ return undefined;
1824
+ const request = this.readJsonAs(row.requestHash);
1825
+ const decision = this.readJsonAs(row.responseHash);
1826
+ if (typeof request.decisionId !== "string" ||
1827
+ typeof request.requestDigest !== "string" ||
1828
+ typeof request.nodeId !== "string" ||
1829
+ typeof decision.provenance !== "string" ||
1830
+ typeof decision.acceptedAt !== "string" ||
1831
+ typeof decision.answerDigest !== "string" ||
1832
+ typeof decision.subjectDigest !== "string" ||
1833
+ typeof decision.presentationDigest !== "string" ||
1834
+ typeof decision.revision !== "number" ||
1835
+ decision.response === undefined) {
1836
+ throw new Error(`Human decision receipt is invalid for run ${runId}`);
1837
+ }
1838
+ return {
1839
+ schema: "pi-workflows.human-decision-receipt.v1",
1840
+ decisionId: request.decisionId,
1841
+ requestDigest: request.requestDigest,
1842
+ nodeId: request.nodeId,
1843
+ response: decision.response,
1844
+ provenance: decision.provenance === "timeout" ? "timeout" : "human",
1845
+ acceptedAt: decision.acceptedAt,
1846
+ answerDigest: decision.answerDigest,
1847
+ subjectDigest: decision.subjectDigest,
1848
+ presentationDigest: decision.presentationDigest,
1849
+ revision: decision.revision,
1850
+ };
1851
+ }
1852
+ readUpdates(runId) {
1853
+ const rows = this.state.connection
1854
+ .prepare(`SELECT u.update_id AS updateId, u.run_revision AS runRevision,
1855
+ a.node_id AS nodeId, u.attempt_id AS attemptId,
1856
+ u.update_type AS updateType, u.update_key AS updateKey,
1857
+ u.data_hash AS dataHash, u.recorded_at AS recordedAt
1858
+ FROM workflow_updates u
1859
+ JOIN node_attempts a ON a.attempt_id = u.attempt_id
1860
+ WHERE a.run_id = ? ORDER BY u.run_revision`)
1861
+ .all(runId)
1862
+ .filter(isUpdateRow);
1863
+ if (rows.length === 0)
1864
+ return {};
1865
+ let updates;
1866
+ for (const row of rows) {
1867
+ updates = updateProjection(updates, {
1868
+ updateId: row.updateId,
1869
+ seq: row.runRevision,
1870
+ at: new Date(row.recordedAt).toISOString(),
1871
+ runId,
1872
+ nodeId: row.nodeId,
1873
+ attemptId: row.attemptId,
1874
+ type: row.updateType,
1875
+ key: row.updateKey,
1876
+ data: this.readJsonAs(row.dataHash),
1877
+ });
1878
+ }
1879
+ return updates === undefined ? {} : { updates };
1880
+ }
1881
+ readJsonAs(hash) {
1882
+ return this.state.readJson(hash);
1883
+ }
1884
+ readText(hash) {
1885
+ const blob = this.state.readBlob(hash);
1886
+ if (blob === undefined || blob.mediaType !== "text/plain") {
1887
+ throw new Error("Text blob is missing or has the wrong media type");
1888
+ }
1889
+ return blob.content.toString("utf8");
698
1890
  }
699
1891
  readDefinition(hash) {
700
1892
  const value = this.state.readJson(hash);
@@ -892,14 +2084,38 @@ export function readLastTraceEvent(runId, options = {}) {
892
2084
  }
893
2085
  }
894
2086
  function insertRunEvent(state, resourceId, revision, at, event) {
2087
+ const payload = compactTracePayload(event.type, event.payload);
895
2088
  const payloadHash = state.putJson({
896
2089
  scope: event.scope,
897
2090
  ...(event.nodeId === undefined ? {} : { nodeId: event.nodeId }),
898
2091
  ...(event.attemptId === undefined ? {} : { attemptId: event.attemptId }),
899
- payload: event.payload,
2092
+ payload,
900
2093
  }, Date.parse(at));
901
2094
  insertGenericEvent(state, resourceId, revision, event.type, "system", null, payloadHash, Date.parse(at));
902
2095
  }
2096
+ function compactTracePayload(eventType, payload) {
2097
+ if (eventType === "agent_prompt_sent" && Object.hasOwn(payload, "prompt")) {
2098
+ const { prompt: _prompt, ...rest } = payload;
2099
+ return { ...rest, promptStored: true };
2100
+ }
2101
+ if (eventType === "node_finished" || eventType === "node_failed") {
2102
+ const { output: _output, action: _action, assistantMessage: _assistantMessage, conversation: _conversation, error: _error, ...rest } = payload;
2103
+ return { ...rest, ...(Object.hasOwn(payload, "output") ? { outputStored: true } : {}) };
2104
+ }
2105
+ if (eventType === "include_exited" && Object.hasOwn(payload, "output")) {
2106
+ const { output: _output, ...rest } = payload;
2107
+ return { ...rest, outputStored: true };
2108
+ }
2109
+ if (eventType === "run_started" && Object.hasOwn(payload, "input")) {
2110
+ const { input: _input, ...rest } = payload;
2111
+ return { ...rest, inputStored: true };
2112
+ }
2113
+ if (Object.hasOwn(payload, "finalOutput")) {
2114
+ const { finalOutput: _finalOutput, ...rest } = payload;
2115
+ return { ...rest, finalOutputStored: true };
2116
+ }
2117
+ return payload;
2118
+ }
903
2119
  function insertGenericEvent(state, resourceId, revision, eventType, actorType, actorId, payloadHash, now) {
904
2120
  state.connection
905
2121
  .prepare(`INSERT INTO events(
@@ -920,6 +2136,16 @@ function sourceForState(state, definitionDigest) {
920
2136
  revision: definitionDigest.toString("hex"),
921
2137
  };
922
2138
  }
2139
+ function sourceParts(source) {
2140
+ return source.kind === "builtin"
2141
+ ? { type: "builtin", ref: source.id, revision: source.revision }
2142
+ : { type: "file", ref: source.path, revision: source.hash };
2143
+ }
2144
+ function workflowSourceFromRow(row) {
2145
+ return row.sourceType === "builtin"
2146
+ ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
2147
+ : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
2148
+ }
923
2149
  function segmentIdFor(runId, attemptId) {
924
2150
  return `segment-${createHash("sha256")
925
2151
  .update(`${runId}\0${attemptId ?? ""}`)
@@ -941,6 +2167,73 @@ function outcomeStatus(outcome) {
941
2167
  return "failed";
942
2168
  }
943
2169
  }
2170
+ function outcomeForStatus(status) {
2171
+ switch (status) {
2172
+ case "completed":
2173
+ return "ok";
2174
+ case "timed_out":
2175
+ return "timed_out";
2176
+ case "cancelled":
2177
+ return "cancelled";
2178
+ case "failed":
2179
+ return "failed";
2180
+ default:
2181
+ throw new Error(`Workflow step has nonterminal status: ${status}`);
2182
+ }
2183
+ }
2184
+ function attemptReceipt(step) {
2185
+ return {
2186
+ ...(step.action === undefined ? {} : { action: step.action }),
2187
+ ...(step.assistantMessage === undefined ? {} : { assistantMessage: step.assistantMessage }),
2188
+ };
2189
+ }
2190
+ function promptFromEntry(value) {
2191
+ if (!isRecord(value))
2192
+ return null;
2193
+ const content = value.content;
2194
+ if (typeof content === "string")
2195
+ return content;
2196
+ if (!Array.isArray(content))
2197
+ return null;
2198
+ return content
2199
+ .filter(isRecord)
2200
+ .map((part) => (typeof part.text === "string" ? part.text : ""))
2201
+ .filter((text) => text.length !== 0)
2202
+ .join("\n");
2203
+ }
2204
+ function assistantOutputFromEntry(value, maxChars) {
2205
+ if (!isRecord(value) || !isRecord(value.message)) {
2206
+ throw new Error("Assistant response entry is invalid");
2207
+ }
2208
+ const message = value.message;
2209
+ if (message.role !== "assistant" || !Array.isArray(message.content)) {
2210
+ throw new Error("Assistant response entry does not contain an assistant message");
2211
+ }
2212
+ const text = message.content
2213
+ .filter(isRecord)
2214
+ .filter((part) => part.type === "text" && typeof part.text === "string")
2215
+ .map((part) => part.text)
2216
+ .join("\n");
2217
+ if (text.trim().length === 0)
2218
+ throw new Error("Assistant response entry has no visible text");
2219
+ if (maxChars !== undefined && text.length > maxChars) {
2220
+ throw new Error(`Assistant response exceeds its stored limit of ${maxChars} characters`);
2221
+ }
2222
+ return text;
2223
+ }
2224
+ function resultForStep(step) {
2225
+ return {
2226
+ attemptId: step.attemptId,
2227
+ nodeId: step.nodeId,
2228
+ nodeType: step.nodeType,
2229
+ outcome: step.outcome,
2230
+ startedAt: step.startedAt,
2231
+ finishedAt: step.finishedAt,
2232
+ durationMs: Date.parse(step.finishedAt) - Date.parse(step.startedAt),
2233
+ ...(step.outcome === "ok" ? { output: step.output } : {}),
2234
+ ...(step.error === undefined ? {} : { error: step.error }),
2235
+ };
2236
+ }
944
2237
  function traceScope(value) {
945
2238
  return value === "node" || value === "agent" || value === "action" || value === "session"
946
2239
  ? value
@@ -968,6 +2261,10 @@ function validateSessionEventRecord(record) {
968
2261
  Array.isArray(record.payload)) {
969
2262
  throw new Error("Session event is missing required envelope fields");
970
2263
  }
2264
+ if (record.type === "assistant_event" &&
2265
+ ["text_delta", "thinking_delta", "toolcall_delta"].includes(String(record.payload.type))) {
2266
+ throw new Error("Incremental assistant events are not durable session facts");
2267
+ }
971
2268
  }
972
2269
  function validateSessionCapture(capture) {
973
2270
  if (capture.schema !== SESSION_CAPTURE_SCHEMA ||
@@ -988,6 +2285,143 @@ function validateSessionCapture(capture) {
988
2285
  throw new Error("Only failed session capture may contain failure details");
989
2286
  }
990
2287
  }
2288
+ function followUpIdFor(runId, requestId) {
2289
+ const digest = createHash("sha256").update(`${runId}\0${requestId}`).digest("hex").slice(0, 40);
2290
+ return `follow-up-${digest}`;
2291
+ }
2292
+ function assertRequestId(requestId) {
2293
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u.test(requestId)) {
2294
+ throw new Error("Workflow request ID must be a stable identifier of at most 512 characters");
2295
+ }
2296
+ }
2297
+ function assertSourceType(source) {
2298
+ if (typeof source !== "string" || source.trim().length === 0 || source.length > 128) {
2299
+ throw new Error("Workflow change source must be a non-empty string of at most 128 characters");
2300
+ }
2301
+ }
2302
+ function assertActorType(value) {
2303
+ if (value !== "session" &&
2304
+ value !== "host" &&
2305
+ value !== "controller" &&
2306
+ value !== "channel" &&
2307
+ value !== "human" &&
2308
+ value !== "policy" &&
2309
+ value !== "control" &&
2310
+ value !== "system") {
2311
+ throw new Error(`Unknown saved actor type: ${value}`);
2312
+ }
2313
+ return value;
2314
+ }
2315
+ function assertFollowUpState(value) {
2316
+ if (value !== "queued" &&
2317
+ value !== "pending_presentation" &&
2318
+ value !== "ready" &&
2319
+ value !== "sent" &&
2320
+ value !== "removed" &&
2321
+ value !== "cancelled") {
2322
+ throw new Error(`Unknown workflow follow-up state: ${value}`);
2323
+ }
2324
+ return value;
2325
+ }
2326
+ function assertPresentationState(value) {
2327
+ if (value !== "none" &&
2328
+ value !== "not-needed" &&
2329
+ value !== "pending" &&
2330
+ value !== "settled" &&
2331
+ value !== "unavailable") {
2332
+ throw new Error(`Unknown workflow presentation state: ${value}`);
2333
+ }
2334
+ return value;
2335
+ }
2336
+ function isSettingsScopeRow(value) {
2337
+ return (isRecord(value) &&
2338
+ typeof value.scopeId === "string" &&
2339
+ typeof value.resourceId === "string" &&
2340
+ typeof value.originRunId === "string" &&
2341
+ typeof value.activeRunId === "string" &&
2342
+ typeof value.mountPath === "string" &&
2343
+ typeof value.invocation === "number" &&
2344
+ Buffer.isBuffer(value.currentHash) &&
2345
+ typeof value.revision === "number" &&
2346
+ typeof value.createdAt === "number" &&
2347
+ typeof value.updatedAt === "number");
2348
+ }
2349
+ function isSettingsProjectionRow(value) {
2350
+ return (isRecord(value) &&
2351
+ typeof value.scopeId === "string" &&
2352
+ Buffer.isBuffer(value.initialHash) &&
2353
+ Buffer.isBuffer(value.currentHash) &&
2354
+ typeof value.revision === "number");
2355
+ }
2356
+ function isSettingsProjectionChangeRow(value) {
2357
+ return (isRecord(value) &&
2358
+ typeof value.changeNumber === "number" &&
2359
+ Buffer.isBuffer(value.patchHash) &&
2360
+ Buffer.isBuffer(value.beforeHash) &&
2361
+ Buffer.isBuffer(value.afterHash));
2362
+ }
2363
+ function isSettingsChangeRow(value) {
2364
+ return (isRecord(value) &&
2365
+ typeof value.changeId === "string" &&
2366
+ typeof value.scopeId === "string" &&
2367
+ typeof value.requestId === "string" &&
2368
+ typeof value.changeNumber === "number" &&
2369
+ typeof value.actorType === "string" &&
2370
+ (typeof value.actorId === "string" || value.actorId === null) &&
2371
+ typeof value.sourceType === "string" &&
2372
+ Buffer.isBuffer(value.patchHash) &&
2373
+ Buffer.isBuffer(value.beforeHash) &&
2374
+ Buffer.isBuffer(value.afterHash) &&
2375
+ typeof value.acceptedAt === "number");
2376
+ }
2377
+ function isFollowUpQueueRow(value) {
2378
+ return (isRecord(value) &&
2379
+ typeof value.runId === "string" &&
2380
+ typeof value.resourceId === "string" &&
2381
+ (typeof value.originSessionId === "string" || value.originSessionId === null) &&
2382
+ typeof value.presentationState === "string" &&
2383
+ (typeof value.presentationEntryId === "string" || value.presentationEntryId === null) &&
2384
+ (typeof value.presentationAssistantEntryId === "string" ||
2385
+ value.presentationAssistantEntryId === null) &&
2386
+ (Buffer.isBuffer(value.presentationReasonHash) || value.presentationReasonHash === null) &&
2387
+ typeof value.revision === "number");
2388
+ }
2389
+ function isFollowUpRow(value) {
2390
+ return (isRecord(value) &&
2391
+ typeof value.followUpId === "string" &&
2392
+ typeof value.resourceId === "string" &&
2393
+ typeof value.queueResourceId === "string" &&
2394
+ typeof value.runId === "string" &&
2395
+ typeof value.requestId === "string" &&
2396
+ typeof value.orderNumber === "number" &&
2397
+ typeof value.targetSessionId === "string" &&
2398
+ typeof value.actorType === "string" &&
2399
+ (typeof value.actorId === "string" || value.actorId === null) &&
2400
+ typeof value.sourceType === "string" &&
2401
+ Buffer.isBuffer(value.promptHash) &&
2402
+ typeof value.status === "string" &&
2403
+ (typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
2404
+ (Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
2405
+ typeof value.createdAt === "number" &&
2406
+ typeof value.updatedAt === "number" &&
2407
+ (typeof value.sentAt === "number" || value.sentAt === null) &&
2408
+ typeof value.revision === "number");
2409
+ }
2410
+ function isParentRunRow(value) {
2411
+ return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
2412
+ }
2413
+ function isStatusRow(value) {
2414
+ return isRecord(value) && typeof value.status === "string";
2415
+ }
2416
+ function isCountRow(value) {
2417
+ return isRecord(value) && typeof value.count === "number";
2418
+ }
2419
+ function isLeaseExpiryRow(value) {
2420
+ return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
2421
+ }
2422
+ function isNextOrderRow(value) {
2423
+ return isRecord(value) && typeof value.nextOrder === "number";
2424
+ }
991
2425
  function assertValidRunId(runId) {
992
2426
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
993
2427
  throw new Error(`Invalid workflow run id: ${JSON.stringify(runId)}`);
@@ -999,9 +2433,63 @@ function isRecord(value) {
999
2433
  function isRunRow(value) {
1000
2434
  return isRecord(value);
1001
2435
  }
2436
+ function savedCurrentSettingsBinding(scopeId, changeNumber, settingsHash) {
2437
+ if (scopeId === null && changeNumber === null && settingsHash === null)
2438
+ return {};
2439
+ if (scopeId === null || changeNumber === null || settingsHash === null) {
2440
+ throw new Error("Saved current workflow settings binding is incomplete");
2441
+ }
2442
+ return {
2443
+ currentSettingsScopeId: scopeId,
2444
+ currentSettingsChangeNumber: changeNumber,
2445
+ currentSettingsHash: settingsHash.toString("hex"),
2446
+ };
2447
+ }
2448
+ function savedStepSettingsBinding(scopeId, changeNumber, settingsHash) {
2449
+ if (scopeId === null && changeNumber === null && settingsHash === null)
2450
+ return {};
2451
+ if (scopeId === null || changeNumber === null || settingsHash === null) {
2452
+ throw new Error("Saved workflow step settings binding is incomplete");
2453
+ }
2454
+ return {
2455
+ settingsScopeId: scopeId,
2456
+ settingsChangeNumber: changeNumber,
2457
+ settingsHash: settingsHash.toString("hex"),
2458
+ };
2459
+ }
1002
2460
  function isRunIdRow(value) {
1003
2461
  return isRecord(value);
1004
2462
  }
2463
+ function isRunStepIdentityRow(value) {
2464
+ return isRecord(value);
2465
+ }
2466
+ function isStepRow(value) {
2467
+ return isRecord(value);
2468
+ }
2469
+ function isRunSourceRow(value) {
2470
+ return isRecord(value);
2471
+ }
2472
+ function isSegmentIdentityRow(value) {
2473
+ return isRecord(value) && typeof value.segmentId === "string" && Buffer.isBuffer(value.entryHash);
2474
+ }
2475
+ function isEntryIdentityRow(value) {
2476
+ return isRecord(value) && typeof value.entryId === "string";
2477
+ }
2478
+ function isAttemptValueRow(value) {
2479
+ return isRecord(value);
2480
+ }
2481
+ function isActiveAttemptRow(value) {
2482
+ return isRecord(value);
2483
+ }
2484
+ function isDecisionReceiptRow(value) {
2485
+ return (isRecord(value) && Buffer.isBuffer(value.requestHash) && Buffer.isBuffer(value.responseHash));
2486
+ }
2487
+ function isContinuationIdentityRow(value) {
2488
+ return isRecord(value);
2489
+ }
2490
+ function isUpdateRow(value) {
2491
+ return isRecord(value);
2492
+ }
1005
2493
  function isRevisionRow(value) {
1006
2494
  return isRecord(value);
1007
2495
  }
@@ -1027,7 +2515,15 @@ function isLeaseAuthorityRow(value) {
1027
2515
  return isRecord(value);
1028
2516
  }
1029
2517
  export function createDefinitionSnapshot(workflow) {
1030
- const composition = compositionMetadata(workflow)?.snapshot;
2518
+ const metadata = compositionMetadata(workflow);
2519
+ const composition = metadata?.snapshot;
2520
+ const settingsScopes = Object.values(metadata?.scopes ?? {})
2521
+ .filter((scope) => scope.path !== "" && scope.settings !== undefined)
2522
+ .sort((left, right) => left.path.localeCompare(right.path))
2523
+ .map((scope) => ({
2524
+ mountPath: scope.path.split("/"),
2525
+ settings: snapshotSettings(scope.settings),
2526
+ }));
1031
2527
  return {
1032
2528
  schema: DEFINITION_SNAPSHOT_SCHEMA,
1033
2529
  name: workflow.name,
@@ -1039,6 +2535,8 @@ export function createDefinitionSnapshot(workflow) {
1039
2535
  ])),
1040
2536
  edges: structuredClone(workflow.edges),
1041
2537
  ...(composition !== undefined ? { composition: structuredClone(composition) } : {}),
2538
+ ...(workflow.settings !== undefined ? { settings: snapshotSettings(workflow.settings) } : {}),
2539
+ ...(settingsScopes.length > 0 ? { settingsScopes } : {}),
1042
2540
  };
1043
2541
  }
1044
2542
  function snapshotNode(workflow, nodeId, node) {
@@ -1073,6 +2571,9 @@ function snapshotNode(workflow, nodeId, node) {
1073
2571
  if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
1074
2572
  common.expectedOutput = node.expectedOutput;
1075
2573
  }
2574
+ if (node.nodeType === "compute" && node.settingsRoute === true) {
2575
+ common.settingsRoute = true;
2576
+ }
1076
2577
  if (node.nodeType === "notify")
1077
2578
  common.summary = node.kind ?? "progress";
1078
2579
  if (node.nodeType === "checkpoint" && node.summary !== undefined)
@@ -1093,4 +2594,16 @@ function snapshotNode(workflow, nodeId, node) {
1093
2594
  common.actionExecution = "exec" in node ? "shell" : "function";
1094
2595
  return common;
1095
2596
  }
2597
+ function snapshotSettings(settings) {
2598
+ return {
2599
+ ...(settings.description !== undefined ? { description: settings.description } : {}),
2600
+ paths: settings.paths.map((rule) => ({
2601
+ path: rule.path,
2602
+ permissions: Object.fromEntries(Object.entries(rule.permissions).map(([permission, actors]) => [
2603
+ permission,
2604
+ actors === undefined ? [] : [...actors],
2605
+ ])),
2606
+ })),
2607
+ };
2608
+ }
1096
2609
  //# sourceMappingURL=store.js.map