@osolmaz/pi-workflows 0.13.2 → 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 (130) hide show
  1. package/README.md +5 -3
  2. package/dist/builtins/autodoc.workflow.d.ts +5 -5
  3. package/dist/builtins/autoimplement.workflow.d.ts +65 -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 +5 -5
  7. package/dist/builtins/catalog.js +1 -1
  8. package/dist/builtins/change-verification.workflow.d.ts +1 -1
  9. package/dist/builtins/plain-summary.workflow.d.ts +1 -1
  10. package/dist/builtins/plan-approval.workflow.d.ts +1 -1
  11. package/dist/builtins/plan-change.workflow.d.ts +23 -23
  12. package/dist/builtins/sanity-check.workflow.d.ts +3 -3
  13. package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
  14. package/dist/controllers/index.d.ts +2 -2
  15. package/dist/controllers/index.js.map +1 -1
  16. package/dist/controllers/sqlite.js +85 -23
  17. package/dist/controllers/sqlite.js.map +1 -1
  18. package/dist/controllers/types.d.ts +34 -0
  19. package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
  20. package/dist/controllers/workflow-engine-scheduler.js +83 -1
  21. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  22. package/dist/controllers/workflows.d.ts +8 -1
  23. package/dist/controllers/workflows.js +36 -0
  24. package/dist/controllers/workflows.js.map +1 -1
  25. package/dist/extension/controller-host.d.ts +2 -1
  26. package/dist/extension/controller-host.js +1 -1
  27. package/dist/extension/controller-host.js.map +1 -1
  28. package/dist/extension/follow-up-coordinator.d.ts +27 -0
  29. package/dist/extension/follow-up-coordinator.js +131 -0
  30. package/dist/extension/follow-up-coordinator.js.map +1 -0
  31. package/dist/extension/index.d.ts +9 -0
  32. package/dist/extension/index.js +460 -75
  33. package/dist/extension/index.js.map +1 -1
  34. package/dist/extension/session-events.d.ts +2 -3
  35. package/dist/extension/session-events.js +11 -10
  36. package/dist/extension/session-events.js.map +1 -1
  37. package/dist/extension/widget.js +9 -0
  38. package/dist/extension/widget.js.map +1 -1
  39. package/dist/state/database.d.ts +2 -1
  40. package/dist/state/database.js +11 -13
  41. package/dist/state/database.js.map +1 -1
  42. package/dist/state/json.js +6 -1
  43. package/dist/state/json.js.map +1 -1
  44. package/dist/state/mutation.d.ts +1 -1
  45. package/dist/state/mutation.js.map +1 -1
  46. package/dist/state/prune.d.ts +18 -0
  47. package/dist/state/prune.js +373 -0
  48. package/dist/state/prune.js.map +1 -0
  49. package/dist/state/schema.d.ts +1 -1
  50. package/dist/state/schema.js +116 -18
  51. package/dist/state/schema.js.map +1 -1
  52. package/dist/viewer/cli.d.ts +3 -1
  53. package/dist/viewer/cli.js +55 -4
  54. package/dist/viewer/cli.js.map +1 -1
  55. package/dist/viewer/render.js +19 -1
  56. package/dist/viewer/render.js.map +1 -1
  57. package/dist/workflows/composition.d.ts +2 -0
  58. package/dist/workflows/composition.js +15 -0
  59. package/dist/workflows/composition.js.map +1 -1
  60. package/dist/workflows/definition.d.ts +6 -3
  61. package/dist/workflows/definition.js +3 -0
  62. package/dist/workflows/definition.js.map +1 -1
  63. package/dist/workflows/engine.d.ts +6 -1
  64. package/dist/workflows/engine.js +301 -35
  65. package/dist/workflows/engine.js.map +1 -1
  66. package/dist/workflows/human-decision.d.ts +1 -0
  67. package/dist/workflows/human-decision.js +25 -9
  68. package/dist/workflows/human-decision.js.map +1 -1
  69. package/dist/workflows/index.d.ts +3 -1
  70. package/dist/workflows/index.js +2 -0
  71. package/dist/workflows/index.js.map +1 -1
  72. package/dist/workflows/json-patch.d.ts +47 -0
  73. package/dist/workflows/json-patch.js +298 -0
  74. package/dist/workflows/json-patch.js.map +1 -0
  75. package/dist/workflows/schema.js +29 -1
  76. package/dist/workflows/schema.js.map +1 -1
  77. package/dist/workflows/settings.d.ts +128 -0
  78. package/dist/workflows/settings.js +199 -0
  79. package/dist/workflows/settings.js.map +1 -0
  80. package/dist/workflows/store.d.ts +75 -2
  81. package/dist/workflows/store.js +1349 -89
  82. package/dist/workflows/store.js.map +1 -1
  83. package/dist/workflows/tool-input.d.ts +22 -0
  84. package/dist/workflows/tool-input.js +43 -0
  85. package/dist/workflows/tool-input.js.map +1 -1
  86. package/dist/workflows/types.d.ts +40 -5
  87. package/docs/2026-08-25-workflow-follow-ups.md +132 -0
  88. package/docs/2026-08-25-workflow-settings.md +169 -0
  89. package/docs/DEFERRED_TURNS.md +6 -0
  90. package/docs/DESIGN_PHILOSOPHY.md +2 -0
  91. package/docs/SQLITE_STATE.md +31 -18
  92. package/docs/WORKFLOW_COMPOSITION.md +15 -0
  93. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
  94. package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
  95. package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
  96. package/docs/tui-viewer.md +7 -6
  97. package/docs/workflows.md +22 -1
  98. package/examples/workflows/live-settings.workflow.ts +93 -0
  99. package/herdr-plugin.toml +1 -1
  100. package/package.json +1 -1
  101. package/src/builtins/autoimplement.workflow.ts +56 -8
  102. package/src/builtins/catalog.ts +1 -1
  103. package/src/controllers/index.ts +6 -0
  104. package/src/controllers/sqlite.ts +129 -35
  105. package/src/controllers/types.ts +40 -0
  106. package/src/controllers/workflow-engine-scheduler.ts +103 -1
  107. package/src/controllers/workflows.ts +68 -0
  108. package/src/extension/controller-host.ts +6 -1
  109. package/src/extension/follow-up-coordinator.ts +151 -0
  110. package/src/extension/index.ts +538 -88
  111. package/src/extension/session-events.ts +15 -13
  112. package/src/extension/widget.ts +8 -0
  113. package/src/state/database.ts +12 -12
  114. package/src/state/json.ts +6 -1
  115. package/src/state/mutation.ts +4 -1
  116. package/src/state/prune.ts +502 -0
  117. package/src/state/schema.ts +116 -18
  118. package/src/viewer/cli.ts +52 -5
  119. package/src/viewer/render.ts +43 -1
  120. package/src/workflows/composition.ts +19 -0
  121. package/src/workflows/definition.ts +19 -5
  122. package/src/workflows/engine.ts +363 -35
  123. package/src/workflows/human-decision.ts +41 -11
  124. package/src/workflows/index.ts +43 -0
  125. package/src/workflows/json-patch.ts +375 -0
  126. package/src/workflows/schema.ts +31 -1
  127. package/src/workflows/settings.ts +430 -0
  128. package/src/workflows/store.ts +1977 -184
  129. package/src/workflows/tool-input.ts +58 -0
  130. package/src/workflows/types.ts +43 -4
@@ -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,38 +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 workflowSourcesHash = state.workflowSources === undefined
107
- ? null
108
- : this.state.putJson(state.workflowSources, now);
109
- const humanDecisionHash = state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
110
123
  const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
111
124
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
112
125
  this.state.connection
113
126
  .prepare(`INSERT INTO runs(
114
127
  run_id, resource_id, project_id, parent_run_id, definition_digest,
115
- workflow_ref, workflow_source_hash, launch_options_hash,
116
- source_type, source_ref, source_revision, title, status, paused,
117
- status_detail, input_hash, workflow_sources_hash, human_decision_hash,
118
- final_output_hash, error_hash, carried_step_count, current_node,
119
- current_attempt_id, current_node_started_at, waiting_on,
128
+ workflow_ref, launch_options_hash, title, status, paused,
129
+ status_detail, input_hash, final_output_hash, error_hash,
120
130
  created_at, updated_at, finished_at
121
- ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
122
- .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, workflowSourcesHash, humanDecisionHash, finalOutputHash, errorHash, state.carriedStepCount ?? 0, state.currentNode ?? null, state.currentAttemptId ?? null, state.currentNodeStartedAt === undefined
123
- ? null
124
- : Date.parse(state.currentNodeStartedAt), state.waitingOn ?? null, 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);
125
135
  insertRunEvent(this.state, resourceId, 1, at, {
126
136
  scope: "run",
127
137
  type: "run_created",
128
138
  payload: { workflowName: workflow.name },
129
139
  });
140
+ this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
130
141
  this.syncNodeAttempts(state, snapshot, now);
131
142
  });
132
143
  this.contexts.set(state.runId, { revision: acceptedRevision, lock: Promise.resolve() });
133
144
  return state.runId;
134
145
  });
135
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
+ }
136
579
  async prepareRunResume(runId) {
137
580
  const loaded = this.readRun(runId, { includeTrace: true });
138
581
  if (loaded === null)
@@ -140,6 +583,7 @@ export class WorkflowRunStore {
140
583
  if (loaded.state.status !== "running") {
141
584
  throw new Error(`Cannot resume workflow run ${runId} with status ${loaded.state.status}`);
142
585
  }
586
+ this.verifySettingsProjections(runId);
143
587
  const revision = this.resourceRevision(runId);
144
588
  this.contexts.set(runId, { revision, lock: Promise.resolve() });
145
589
  this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
@@ -152,9 +596,9 @@ export class WorkflowRunStore {
152
596
  const at = new Date(now).toISOString();
153
597
  this.state.connection
154
598
  .prepare(`UPDATE node_attempts
155
- SET status = 'cancelled', finished_at = ?, updated_at = ?
599
+ SET status = 'interrupted', updated_at = ?
156
600
  WHERE run_id = ? AND status IN ('pending', 'running', 'waiting')`)
157
- .run(now, now, runId);
601
+ .run(now, runId);
158
602
  insertRunEvent(this.state, row.resourceId, nextRevision, at, {
159
603
  scope: "run",
160
604
  type: "run_resume_prepared",
@@ -181,6 +625,9 @@ export class WorkflowRunStore {
181
625
  delete loaded.state.currentNode;
182
626
  delete loaded.state.currentAttemptId;
183
627
  delete loaded.state.currentNodeStartedAt;
628
+ delete loaded.state.currentSettingsScopeId;
629
+ delete loaded.state.currentSettingsChangeNumber;
630
+ delete loaded.state.currentSettingsHash;
184
631
  delete loaded.state.statusDetail;
185
632
  delete loaded.state.paused;
186
633
  await this.writeSnapshot(runId, loaded.state, {
@@ -264,12 +711,14 @@ export class WorkflowRunStore {
264
711
  const revision = context.revision + 1;
265
712
  const now = Date.now();
266
713
  const at = new Date(now).toISOString();
714
+ const snapshot = this.readDefinition(run.definitionHash);
715
+ this.assertSettingsRouteCurrent(event, snapshot);
267
716
  const traceEvent = { seq: revision, at, runId, ...event };
268
717
  state.traceSeq = revision;
269
718
  state.updatedAt = at;
270
719
  insertRunEvent(this.state, run.resourceId, revision, at, traceEvent);
271
720
  this.persistRunState(run, state, revision, now);
272
- const snapshot = this.readDefinition(run.definitionHash);
721
+ this.transitionFollowUpsForRunState(state, event, now);
273
722
  this.syncNodeAttempts(state, snapshot, now);
274
723
  context.revision = revision;
275
724
  accepted = traceEvent;
@@ -325,6 +774,11 @@ export class WorkflowRunStore {
325
774
  VALUES (?, ?, 'interactive', ?)
326
775
  ON CONFLICT(run_id) DO NOTHING`)
327
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);
328
782
  const revision = context.revision + 1;
329
783
  const at = new Date(now).toISOString();
330
784
  insertRunEvent(this.state, run.resourceId, revision, at, {
@@ -363,7 +817,7 @@ export class WorkflowRunStore {
363
817
  .prepare("UPDATE session_segments SET entry_count = ? WHERE segment_id = ?")
364
818
  .run(sequence, segment.segmentId);
365
819
  this.bumpResource(segmentResourceId(segment), revision, now);
366
- 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);
367
821
  return sequence;
368
822
  });
369
823
  }
@@ -450,6 +904,8 @@ export class WorkflowRunStore {
450
904
  sessionCapture: flat.capture,
451
905
  sessionIntegrity: flat.integrity,
452
906
  sessionSegments: segments.filter((segment) => segment.attemptId !== ""),
907
+ settingsScopes: this.settingsScopesForRunView(runId),
908
+ followUpQueue: this.readFollowUpQueue(runId) ?? null,
453
909
  };
454
910
  }
455
911
  listRuns(options = {}) {
@@ -554,18 +1010,14 @@ export class WorkflowRunStore {
554
1010
  }
555
1011
  persistRunState(run, state, expectedRevision, now) {
556
1012
  this.bumpResource(run.resourceId, expectedRevision - 1, now);
557
- const workflowSourcesHash = state.workflowSources === undefined ? null : this.state.putJson(state.workflowSources, now);
558
- const humanDecisionHash = state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
559
1013
  const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
560
1014
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
561
1015
  const update = this.state.connection
562
1016
  .prepare(`UPDATE runs
563
1017
  SET title = ?, status = ?, paused = ?, status_detail = ?,
564
- workflow_sources_hash = ?, human_decision_hash = ?, final_output_hash = ?,
565
- error_hash = ?, carried_step_count = ?, current_node = ?, current_attempt_id = ?,
566
- current_node_started_at = ?, waiting_on = ?, updated_at = ?, finished_at = ?
1018
+ final_output_hash = ?, error_hash = ?, updated_at = ?, finished_at = ?
567
1019
  WHERE run_id = ?`)
568
- .run(state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, workflowSourcesHash, humanDecisionHash, finalOutputHash, errorHash, state.carriedStepCount ?? 0, state.currentNode ?? null, state.currentAttemptId ?? null, state.currentNodeStartedAt === undefined ? null : Date.parse(state.currentNodeStartedAt), state.waitingOn ?? null, 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);
569
1021
  if (update.changes !== 1)
570
1022
  throw new Error(`Workflow run is missing: ${state.runId}`);
571
1023
  if (state.status !== "running") {
@@ -606,13 +1058,341 @@ export class WorkflowRunStore {
606
1058
  .run(effectId, effectResourceId, runResourceId, sourceRevision, effectType, idempotencyKey, payloadHash, now, now);
607
1059
  insertGenericEvent(this.state, effectResourceId, 1, "effect.created", "system", null, payloadHash, now);
608
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
+ }
609
1386
  syncNodeAttempts(state, snapshot, now) {
610
1387
  for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
611
- const metadata = stepMetadata(step);
612
- const stepMetadataHash = Object.keys(metadata).length === 0 ? null : this.state.putJson(metadata, 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);
613
1394
  const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
614
1395
  const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
615
- const promptHash = step.prompt === null ? null : this.state.putJson(step.prompt, now);
616
1396
  const existing = this.state.connection
617
1397
  .prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
618
1398
  .get(step.attemptId);
@@ -621,18 +1401,25 @@ export class WorkflowRunStore {
621
1401
  this.state.connection
622
1402
  .prepare(`INSERT INTO node_attempts(
623
1403
  attempt_id, run_id, node_id, attempt_number, node_type, status,
624
- prompt_hash, output_hash, step_metadata_hash, error_hash,
1404
+ prompt_hash, output_hash, receipt_hash, error_hash,
1405
+ settings_scope_id, settings_change_number, settings_hash,
625
1406
  started_at, finished_at, created_at, updated_at
626
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
627
- .run(step.attemptId, state.runId, step.nodeId, attemptNumber, step.nodeType, outcomeStatus(step.outcome), promptHash, outputHash, stepMetadataHash, 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);
628
1409
  }
629
1410
  else {
630
1411
  this.state.connection
631
1412
  .prepare(`UPDATE node_attempts
632
- SET status = ?, prompt_hash = ?, output_hash = ?, step_metadata_hash = ?,
633
- 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 = ?
634
1416
  WHERE attempt_id = ?`)
635
- .run(outcomeStatus(step.outcome), promptHash, outputHash, stepMetadataHash, 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);
636
1423
  }
637
1424
  }
638
1425
  if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
@@ -659,16 +1446,9 @@ export class WorkflowRunStore {
659
1446
  /* istanbul ignore if -- array index follows a checked bound */
660
1447
  if (step === undefined)
661
1448
  throw new Error("Workflow run step became unavailable");
662
- const attempt = this.state.connection
663
- .prepare("SELECT output_hash AS outputHash FROM node_attempts WHERE attempt_id = ?")
664
- .get(step.attemptId);
665
- if (!isAttemptOutputRow(attempt)) {
666
- throw new Error(`Workflow node attempt is missing: ${step.attemptId}`);
667
- }
1449
+ const attemptOutput = this.readAttemptOutput(step.attemptId);
668
1450
  const carried = stepIndex < (state.carriedStepCount ?? 0);
669
- const outputOverrideHash = carried &&
670
- (attempt.outputHash === null ||
671
- canonicalJson(this.state.readJson(attempt.outputHash)) !== canonicalJson(step.output))
1451
+ const outputOverrideHash = carried && canonicalJson(attemptOutput) !== canonicalJson(step.output)
672
1452
  ? this.state.putJson(step.output, now)
673
1453
  : null;
674
1454
  this.state.connection
@@ -677,6 +1457,78 @@ export class WorkflowRunStore {
677
1457
  .run(state.runId, stepIndex, step.attemptId, outputOverrideHash);
678
1458
  }
679
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);
1531
+ }
680
1532
  ensureAttempt(state, nodeId, attemptId, now, snapshot) {
681
1533
  const existing = this.state.connection
682
1534
  .prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
@@ -684,18 +1536,31 @@ export class WorkflowRunStore {
684
1536
  const status = state.status === "waiting" ? "waiting" : "running";
685
1537
  if (existing !== undefined) {
686
1538
  this.state.connection
687
- .prepare("UPDATE node_attempts SET status = ?, updated_at = ? WHERE attempt_id = ?")
688
- .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);
689
1546
  return;
690
1547
  }
691
1548
  const definition = snapshot ?? this.readDefinition(this.requireRunRow(state.runId).definitionHash);
692
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);
693
1555
  this.state.connection
694
1556
  .prepare(`INSERT INTO node_attempts(
695
1557
  attempt_id, run_id, node_id, attempt_number, node_type, status,
1558
+ settings_scope_id, settings_change_number, settings_hash,
696
1559
  started_at, created_at, updated_at
697
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
698
- .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);
699
1564
  }
700
1565
  nextAttemptNumber(runId, nodeId) {
701
1566
  const row = this.state.connection
@@ -717,19 +1582,45 @@ export class WorkflowRunStore {
717
1582
  throw new Error("Could not allocate workflow update sequence");
718
1583
  return row.updateSeq;
719
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
+ }
720
1616
  readRunRow(runId) {
721
1617
  const row = this.state.connection
722
1618
  .prepare(`SELECT r.run_id AS runId, r.resource_id AS resourceId,
723
1619
  d.definition_hash AS definitionHash, r.definition_digest AS definitionDigest,
724
- d.workflow_name AS workflowRef, r.source_ref AS sourceRef,
725
- r.workflow_source_hash AS workflowSourceHash,
726
- r.workflow_sources_hash AS workflowSourcesHash, r.parent_run_id AS parentRunId,
1620
+ d.workflow_name AS workflowRef, r.parent_run_id AS parentRunId,
727
1621
  r.title, r.status, r.paused, r.status_detail AS statusDetail,
728
- r.input_hash AS inputHash, r.human_decision_hash AS humanDecisionHash,
729
- r.final_output_hash AS finalOutputHash, r.error_hash AS errorHash,
730
- r.carried_step_count AS carriedStepCount, r.current_node AS currentNode,
731
- r.current_attempt_id AS currentAttemptId,
732
- r.current_node_started_at AS currentNodeStartedAt, r.waiting_on AS waitingOn,
1622
+ r.input_hash AS inputHash, r.final_output_hash AS finalOutputHash,
1623
+ r.error_hash AS errorHash,
733
1624
  r.created_at AS createdAt, r.updated_at AS updatedAt, r.finished_at AS finishedAt
734
1625
  FROM runs r
735
1626
  JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
@@ -745,6 +1636,10 @@ export class WorkflowRunStore {
745
1636
  }
746
1637
  readRunState(row, snapshot) {
747
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);
748
1643
  const outputs = {};
749
1644
  const results = {};
750
1645
  for (const step of steps) {
@@ -770,15 +1665,11 @@ export class WorkflowRunStore {
770
1665
  runId: row.runId,
771
1666
  workflowName: row.workflowRef,
772
1667
  ...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
773
- ...(row.carriedStepCount === 0 ? {} : { carriedStepCount: row.carriedStepCount }),
1668
+ ...(carriedStepCount === 0 ? {} : { carriedStepCount }),
774
1669
  ...(row.title === null ? {} : { runTitle: row.title }),
775
- ...(row.sourceRef.startsWith("inline:")
776
- ? {}
777
- : { workflowSource: this.readJsonAs(row.workflowSourceHash) }),
778
- ...(row.workflowSourcesHash === null
779
- ? {}
780
- : { workflowSources: this.readJsonAs(row.workflowSourcesHash) }),
781
- ...(row.workflowSourcesHash !== null || snapshot.composition?.mounts.length
1670
+ ...(sources.root === undefined ? {} : { workflowSource: sources.root }),
1671
+ ...(sources.mounted.length === 0 ? {} : { workflowSources: sources.mounted }),
1672
+ ...(sources.mounted.length !== 0 || snapshot.composition?.mounts.length
782
1673
  ? { definitionDigest: `sha256:${row.definitionDigest.toString("hex")}` }
783
1674
  : {}),
784
1675
  startedAt: new Date(row.createdAt).toISOString(),
@@ -790,17 +1681,18 @@ export class WorkflowRunStore {
790
1681
  results,
791
1682
  steps,
792
1683
  ...this.readUpdates(row.runId),
793
- ...(row.currentNode === null ? {} : { currentNode: row.currentNode }),
794
- ...(row.currentAttemptId === null ? {} : { currentAttemptId: row.currentAttemptId }),
795
- ...(row.currentNodeStartedAt === null
1684
+ ...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
1685
+ ...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
1686
+ ...(activeAttempt?.startedAt === null || activeAttempt === undefined
796
1687
  ? {}
797
- : { currentNodeStartedAt: new Date(row.currentNodeStartedAt).toISOString() }),
1688
+ : { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
1689
+ ...savedCurrentSettingsBinding(activeAttempt?.settingsScopeId ?? null, activeAttempt?.settingsChangeNumber ?? null, activeAttempt?.settingsHash ?? null),
798
1690
  ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
799
- ...(row.humanDecisionHash === null
800
- ? {}
801
- : { humanDecision: this.readJsonAs(row.humanDecisionHash) }),
1691
+ ...(humanDecision === undefined ? {} : { humanDecision }),
802
1692
  ...(row.paused === 0 ? {} : { paused: true }),
803
- ...(row.waitingOn === null ? {} : { waitingOn: row.waitingOn }),
1693
+ ...(row.status === "waiting" && steps.at(-1) !== undefined
1694
+ ? { waitingOn: steps.at(-1)?.nodeId }
1695
+ : {}),
804
1696
  ...(row.finalOutputHash === null
805
1697
  ? {}
806
1698
  : { finalOutput: this.state.readJson(row.finalOutputHash) }),
@@ -810,25 +1702,55 @@ export class WorkflowRunStore {
810
1702
  readSteps(runId) {
811
1703
  const rows = this.state.connection
812
1704
  .prepare(`SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
813
- a.node_type AS nodeType, a.status, a.prompt_hash AS promptHash,
814
- a.output_hash AS outputHash, s.output_override_hash AS outputOverrideHash,
815
- a.step_metadata_hash AS stepMetadataHash, a.error_hash AS errorHash,
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,
816
1715
  a.started_at AS startedAt, a.finished_at AS finishedAt
817
1716
  FROM run_steps s
818
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'
819
1732
  WHERE s.run_id = ? ORDER BY s.step_index`)
820
1733
  .all(runId)
821
1734
  .filter(isStepRow);
822
1735
  return rows.map((row, index) => {
823
1736
  if (row.stepIndex !== index)
824
1737
  throw new Error(`Workflow run step sequence has a gap: ${runId}`);
825
- const metadata = row.stepMetadataHash === null
826
- ? {}
827
- : this.readJsonAs(row.stepMetadataHash);
828
- const prompt = row.promptHash === null ? null : this.readJsonAs(row.promptHash);
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);
829
1744
  const outputHash = row.outputOverrideHash ?? row.outputHash;
830
- const output = outputHash === null ? null : this.state.readJson(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);
831
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 };
832
1754
  return {
833
1755
  attemptId: row.attemptId,
834
1756
  nodeId: row.nodeId,
@@ -839,10 +1761,94 @@ export class WorkflowRunStore {
839
1761
  prompt,
840
1762
  output,
841
1763
  ...(error === undefined ? {} : { error }),
842
- ...metadata,
1764
+ ...savedStepSettingsBinding(row.settingsScopeId, row.settingsChangeNumber, row.settingsHash),
1765
+ ...receipt,
1766
+ ...(conversation === undefined ? {} : { conversation }),
843
1767
  };
844
1768
  });
845
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
+ }
846
1852
  readUpdates(runId) {
847
1853
  const rows = this.state.connection
848
1854
  .prepare(`SELECT u.update_id AS updateId, u.run_revision AS runRevision,
@@ -1088,8 +2094,15 @@ function insertRunEvent(state, resourceId, revision, at, event) {
1088
2094
  insertGenericEvent(state, resourceId, revision, event.type, "system", null, payloadHash, Date.parse(at));
1089
2095
  }
1090
2096
  function compactTracePayload(eventType, payload) {
1091
- if ((eventType === "node_finished" || eventType === "include_exited") &&
1092
- Object.hasOwn(payload, "output")) {
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")) {
1093
2106
  const { output: _output, ...rest } = payload;
1094
2107
  return { ...rest, outputStored: true };
1095
2108
  }
@@ -1123,6 +2136,16 @@ function sourceForState(state, definitionDigest) {
1123
2136
  revision: definitionDigest.toString("hex"),
1124
2137
  };
1125
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
+ }
1126
2149
  function segmentIdFor(runId, attemptId) {
1127
2150
  return `segment-${createHash("sha256")
1128
2151
  .update(`${runId}\0${attemptId ?? ""}`)
@@ -1158,13 +2181,46 @@ function outcomeForStatus(status) {
1158
2181
  throw new Error(`Workflow step has nonterminal status: ${status}`);
1159
2182
  }
1160
2183
  }
1161
- function stepMetadata(step) {
2184
+ function attemptReceipt(step) {
1162
2185
  return {
1163
2186
  ...(step.action === undefined ? {} : { action: step.action }),
1164
2187
  ...(step.assistantMessage === undefined ? {} : { assistantMessage: step.assistantMessage }),
1165
- ...(step.conversation === undefined ? {} : { conversation: step.conversation }),
1166
2188
  };
1167
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
+ }
1168
2224
  function resultForStep(step) {
1169
2225
  return {
1170
2226
  attemptId: step.attemptId,
@@ -1229,6 +2285,143 @@ function validateSessionCapture(capture) {
1229
2285
  throw new Error("Only failed session capture may contain failure details");
1230
2286
  }
1231
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
+ }
1232
2425
  function assertValidRunId(runId) {
1233
2426
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
1234
2427
  throw new Error(`Invalid workflow run id: ${JSON.stringify(runId)}`);
@@ -1240,18 +2433,60 @@ function isRecord(value) {
1240
2433
  function isRunRow(value) {
1241
2434
  return isRecord(value);
1242
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
+ }
1243
2460
  function isRunIdRow(value) {
1244
2461
  return isRecord(value);
1245
2462
  }
1246
2463
  function isRunStepIdentityRow(value) {
1247
2464
  return isRecord(value);
1248
2465
  }
1249
- function isAttemptOutputRow(value) {
1250
- return isRecord(value) && (value.outputHash === null || Buffer.isBuffer(value.outputHash));
1251
- }
1252
2466
  function isStepRow(value) {
1253
2467
  return isRecord(value);
1254
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
+ }
1255
2490
  function isUpdateRow(value) {
1256
2491
  return isRecord(value);
1257
2492
  }
@@ -1280,7 +2515,15 @@ function isLeaseAuthorityRow(value) {
1280
2515
  return isRecord(value);
1281
2516
  }
1282
2517
  export function createDefinitionSnapshot(workflow) {
1283
- 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
+ }));
1284
2527
  return {
1285
2528
  schema: DEFINITION_SNAPSHOT_SCHEMA,
1286
2529
  name: workflow.name,
@@ -1292,6 +2535,8 @@ export function createDefinitionSnapshot(workflow) {
1292
2535
  ])),
1293
2536
  edges: structuredClone(workflow.edges),
1294
2537
  ...(composition !== undefined ? { composition: structuredClone(composition) } : {}),
2538
+ ...(workflow.settings !== undefined ? { settings: snapshotSettings(workflow.settings) } : {}),
2539
+ ...(settingsScopes.length > 0 ? { settingsScopes } : {}),
1295
2540
  };
1296
2541
  }
1297
2542
  function snapshotNode(workflow, nodeId, node) {
@@ -1326,6 +2571,9 @@ function snapshotNode(workflow, nodeId, node) {
1326
2571
  if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
1327
2572
  common.expectedOutput = node.expectedOutput;
1328
2573
  }
2574
+ if (node.nodeType === "compute" && node.settingsRoute === true) {
2575
+ common.settingsRoute = true;
2576
+ }
1329
2577
  if (node.nodeType === "notify")
1330
2578
  common.summary = node.kind ?? "progress";
1331
2579
  if (node.nodeType === "checkpoint" && node.summary !== undefined)
@@ -1346,4 +2594,16 @@ function snapshotNode(workflow, nodeId, node) {
1346
2594
  common.actionExecution = "exec" in node ? "shell" : "function";
1347
2595
  return common;
1348
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
+ }
1349
2609
  //# sourceMappingURL=store.js.map