@osolmaz/pi-workflows 0.14.0 → 0.15.1

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 (235) hide show
  1. package/README.md +78 -110
  2. package/dist/builtins/autoimplement.workflow.js +3 -1
  3. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  4. package/dist/builtins/change-verification.workflow.js +7 -2
  5. package/dist/builtins/change-verification.workflow.js.map +1 -1
  6. package/dist/builtins/metadata.d.ts +22 -0
  7. package/dist/builtins/metadata.js +10 -0
  8. package/dist/builtins/metadata.js.map +1 -0
  9. package/dist/builtins/monitor.workflow.js +4 -1
  10. package/dist/builtins/monitor.workflow.js.map +1 -1
  11. package/dist/builtins/sanity-check.workflow.js +4 -1
  12. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  13. package/dist/builtins/workspace-preparation.workflow.js +3 -1
  14. package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
  15. package/dist/controllers/index.d.ts +0 -1
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +51 -0
  19. package/dist/controllers/sqlite.js +433 -57
  20. package/dist/controllers/sqlite.js.map +1 -1
  21. package/dist/controllers/store.d.ts +1 -0
  22. package/dist/extension/controller-command.d.ts +22 -0
  23. package/dist/extension/controller-command.js +32 -0
  24. package/dist/extension/controller-command.js.map +1 -0
  25. package/dist/extension/index.d.ts +2 -9
  26. package/dist/extension/index.js +876 -3447
  27. package/dist/extension/index.js.map +1 -1
  28. package/dist/extension/message-card.d.ts +13 -0
  29. package/dist/extension/message-card.js +46 -0
  30. package/dist/extension/message-card.js.map +1 -0
  31. package/dist/extension/step-message.d.ts +5 -6
  32. package/dist/extension/step-message.js +41 -52
  33. package/dist/extension/step-message.js.map +1 -1
  34. package/dist/host/child-worker-supervisor.d.ts +50 -0
  35. package/dist/host/child-worker-supervisor.js +159 -0
  36. package/dist/host/child-worker-supervisor.js.map +1 -0
  37. package/dist/host/client.d.ts +48 -0
  38. package/dist/host/client.js +216 -0
  39. package/dist/host/client.js.map +1 -0
  40. package/dist/host/controller-worker-entry.d.ts +2 -0
  41. package/dist/host/controller-worker-entry.js +244 -0
  42. package/dist/host/controller-worker-entry.js.map +1 -0
  43. package/dist/host/controller-worker-protocol.d.ts +36 -0
  44. package/dist/host/controller-worker-protocol.js +49 -0
  45. package/dist/host/controller-worker-protocol.js.map +1 -0
  46. package/dist/host/controller-worker-supervisor.d.ts +21 -0
  47. package/dist/host/controller-worker-supervisor.js +54 -0
  48. package/dist/host/controller-worker-supervisor.js.map +1 -0
  49. package/dist/host/host-entry.d.ts +2 -0
  50. package/dist/host/host-entry.js +23 -0
  51. package/dist/host/host-entry.js.map +1 -0
  52. package/dist/host/processes.d.ts +17 -12
  53. package/dist/host/processes.js +154 -50
  54. package/dist/host/processes.js.map +1 -1
  55. package/dist/host/protocol.d.ts +38 -0
  56. package/dist/host/protocol.js +156 -0
  57. package/dist/host/protocol.js.map +1 -0
  58. package/dist/host/resolver-entry.d.ts +49 -0
  59. package/dist/host/resolver-entry.js +149 -0
  60. package/dist/host/resolver-entry.js.map +1 -0
  61. package/dist/host/rpc-executor.d.ts +14 -3
  62. package/dist/host/rpc-executor.js +63 -30
  63. package/dist/host/rpc-executor.js.map +1 -1
  64. package/dist/host/runner.d.ts +95 -29
  65. package/dist/host/runner.js +2311 -348
  66. package/dist/host/runner.js.map +1 -1
  67. package/dist/host/state.d.ts +174 -0
  68. package/dist/host/state.js +689 -0
  69. package/dist/host/state.js.map +1 -0
  70. package/dist/host/worker-entry.d.ts +10 -0
  71. package/dist/host/worker-entry.js +446 -0
  72. package/dist/host/worker-entry.js.map +1 -0
  73. package/dist/host/worker-protocol.d.ts +31 -0
  74. package/dist/host/worker-protocol.js +122 -0
  75. package/dist/host/worker-protocol.js.map +1 -0
  76. package/dist/host/worker-store.d.ts +80 -0
  77. package/dist/host/worker-store.js +91 -0
  78. package/dist/host/worker-store.js.map +1 -0
  79. package/dist/host/worker-supervisor.d.ts +22 -0
  80. package/dist/host/worker-supervisor.js +54 -0
  81. package/dist/host/worker-supervisor.js.map +1 -0
  82. package/dist/render/canvas.d.ts +2 -1
  83. package/dist/render/canvas.js +40 -15
  84. package/dist/render/canvas.js.map +1 -1
  85. package/dist/render/graph-render.d.ts +7 -3
  86. package/dist/render/graph-render.js +125 -74
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.js +3 -1
  89. package/dist/state/database.js.map +1 -1
  90. package/dist/state/index.d.ts +1 -0
  91. package/dist/state/index.js +1 -0
  92. package/dist/state/index.js.map +1 -1
  93. package/dist/state/schema.d.ts +1 -1
  94. package/dist/state/schema.js +171 -3
  95. package/dist/state/schema.js.map +1 -1
  96. package/dist/state/viewer.d.ts +46 -0
  97. package/dist/state/viewer.js +249 -0
  98. package/dist/state/viewer.js.map +1 -0
  99. package/dist/viewer/cli.d.ts +1 -1
  100. package/dist/viewer/cli.js +47 -18
  101. package/dist/viewer/cli.js.map +1 -1
  102. package/dist/workflows/composition.js +25 -2
  103. package/dist/workflows/composition.js.map +1 -1
  104. package/dist/workflows/definition.d.ts +3 -1
  105. package/dist/workflows/definition.js +25 -0
  106. package/dist/workflows/definition.js.map +1 -1
  107. package/dist/workflows/engine.d.ts +1 -0
  108. package/dist/workflows/engine.js +157 -42
  109. package/dist/workflows/engine.js.map +1 -1
  110. package/dist/workflows/errors.d.ts +3 -1
  111. package/dist/workflows/errors.js +4 -7
  112. package/dist/workflows/errors.js.map +1 -1
  113. package/dist/workflows/human-decision.d.ts +3 -0
  114. package/dist/workflows/human-decision.js +31 -0
  115. package/dist/workflows/human-decision.js.map +1 -1
  116. package/dist/workflows/index.d.ts +1 -1
  117. package/dist/workflows/index.js +1 -1
  118. package/dist/workflows/index.js.map +1 -1
  119. package/dist/workflows/schema.js +17 -6
  120. package/dist/workflows/schema.js.map +1 -1
  121. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  122. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  123. package/dist/workflows/session-reducer.js.map +1 -0
  124. package/dist/workflows/store.d.ts +87 -1
  125. package/dist/workflows/store.js +892 -93
  126. package/dist/workflows/store.js.map +1 -1
  127. package/dist/workflows/tool-input.d.ts +0 -26
  128. package/dist/workflows/tool-input.js +2 -50
  129. package/dist/workflows/tool-input.js.map +1 -1
  130. package/dist/workflows/types.d.ts +36 -4
  131. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  132. package/docs/CONTROLLERS.md +11 -11
  133. package/docs/DEFERRED_TURNS.md +23 -2
  134. package/docs/SQLITE_STATE.md +41 -31
  135. package/docs/WORKFLOW_HOST.md +452 -0
  136. package/docs/development.md +46 -30
  137. package/docs/live-replay-protocol.md +129 -100
  138. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +42 -0
  139. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  140. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  141. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  142. package/docs/tui-viewer.md +18 -3
  143. package/docs/workflows.md +162 -210
  144. package/examples/workflows/command-batch.workflow.ts +2 -0
  145. package/examples/workflows/shell.workflow.ts +2 -1
  146. package/herdr-plugin.toml +1 -1
  147. package/package.json +1 -1
  148. package/src/builtins/autoimplement.workflow.ts +3 -0
  149. package/src/builtins/change-verification.workflow.ts +7 -2
  150. package/src/builtins/metadata.ts +9 -0
  151. package/src/builtins/monitor.workflow.ts +4 -0
  152. package/src/builtins/sanity-check.workflow.ts +4 -0
  153. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  154. package/src/controllers/index.ts +0 -5
  155. package/src/controllers/sqlite.ts +709 -76
  156. package/src/controllers/store.ts +1 -0
  157. package/src/extension/controller-command.ts +45 -0
  158. package/src/extension/index.ts +944 -4163
  159. package/src/extension/message-card.ts +61 -0
  160. package/src/extension/step-message.ts +58 -63
  161. package/src/host/child-worker-supervisor.ts +183 -0
  162. package/src/host/client.ts +293 -0
  163. package/src/host/controller-worker-entry.ts +311 -0
  164. package/src/host/controller-worker-protocol.ts +104 -0
  165. package/src/host/controller-worker-supervisor.ts +79 -0
  166. package/src/host/host-entry.ts +23 -0
  167. package/src/host/processes.ts +171 -54
  168. package/src/host/protocol.ts +196 -0
  169. package/src/host/resolver-entry.ts +241 -0
  170. package/src/host/rpc-executor.ts +76 -34
  171. package/src/host/runner.ts +2813 -422
  172. package/src/host/state.ts +1160 -0
  173. package/src/host/worker-entry.ts +533 -0
  174. package/src/host/worker-protocol.ts +165 -0
  175. package/src/host/worker-store.ts +229 -0
  176. package/src/host/worker-supervisor.ts +74 -0
  177. package/src/render/canvas.ts +44 -10
  178. package/src/render/graph-render.ts +145 -90
  179. package/src/state/database.ts +2 -1
  180. package/src/state/index.ts +14 -0
  181. package/src/state/schema.ts +171 -3
  182. package/src/state/viewer.ts +356 -0
  183. package/src/viewer/cli.ts +49 -17
  184. package/src/workflows/composition.ts +36 -2
  185. package/src/workflows/definition.ts +32 -0
  186. package/src/workflows/engine.ts +157 -54
  187. package/src/workflows/errors.ts +11 -2
  188. package/src/workflows/human-decision.ts +49 -0
  189. package/src/workflows/index.ts +2 -0
  190. package/src/workflows/schema.ts +19 -6
  191. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  192. package/src/workflows/store.ts +1316 -108
  193. package/src/workflows/tool-input.ts +3 -69
  194. package/src/workflows/types.ts +32 -4
  195. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  196. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  197. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  198. package/dist/extension/controller-host.d.ts +0 -48
  199. package/dist/extension/controller-host.js +0 -110
  200. package/dist/extension/controller-host.js.map +0 -1
  201. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  202. package/dist/extension/deferred-turn-coordinator.js +0 -143
  203. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  204. package/dist/extension/deferred-turn.d.ts +0 -44
  205. package/dist/extension/deferred-turn.js +0 -110
  206. package/dist/extension/deferred-turn.js.map +0 -1
  207. package/dist/extension/executor.d.ts +0 -86
  208. package/dist/extension/executor.js +0 -311
  209. package/dist/extension/executor.js.map +0 -1
  210. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  211. package/dist/extension/follow-up-coordinator.js +0 -131
  212. package/dist/extension/follow-up-coordinator.js.map +0 -1
  213. package/dist/extension/recorder.d.ts +0 -84
  214. package/dist/extension/recorder.js +0 -528
  215. package/dist/extension/recorder.js.map +0 -1
  216. package/dist/extension/restart-policy.d.ts +0 -38
  217. package/dist/extension/restart-policy.js +0 -116
  218. package/dist/extension/restart-policy.js.map +0 -1
  219. package/dist/extension/session-events.d.ts +0 -133
  220. package/dist/extension/session-events.js +0 -61
  221. package/dist/extension/session-events.js.map +0 -1
  222. package/dist/extension/terminal-decision.d.ts +0 -51
  223. package/dist/extension/terminal-decision.js +0 -110
  224. package/dist/extension/terminal-decision.js.map +0 -1
  225. package/dist/viewer/session-reducer.js.map +0 -1
  226. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  227. package/src/extension/controller-host.ts +0 -167
  228. package/src/extension/deferred-turn-coordinator.ts +0 -171
  229. package/src/extension/deferred-turn.ts +0 -166
  230. package/src/extension/executor.ts +0 -411
  231. package/src/extension/follow-up-coordinator.ts +0 -151
  232. package/src/extension/recorder.ts +0 -655
  233. package/src/extension/restart-policy.ts +0 -163
  234. package/src/extension/session-events.ts +0 -121
  235. package/src/extension/terminal-decision.ts +0 -172
@@ -5,7 +5,7 @@ import { StaleResourceError } from "../state/mutation.js";
5
5
  import { compileWorkflowDefinition, compositionMetadata, isCompiledWorkflow, } from "./composition.js";
6
6
  import { CancelledError, errorMessage, isAbortLikeError, isClaimLostError, isRunParkedError, RunParkedError, TimeoutError, WorkflowSourceChangedError, } from "./errors.js";
7
7
  import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
8
- import { HumanDecisionStore, createHumanDecisionRequest, validateHumanDecisionResponse, } from "./human-decision.js";
8
+ import { createHumanDecisionRequest, validateHumanDecisionResponse } from "./human-decision.js";
9
9
  import { extractJsonValue } from "./json.js";
10
10
  import { parseWorkflowSettingsValue, resolveInitialWorkflowSettings, } from "./settings.js";
11
11
  import { runShellAction, shellResultFromError } from "./shell.js";
@@ -173,6 +173,8 @@ export class WorkflowEngine {
173
173
  await this.executeGraph(workflow, state, runId);
174
174
  }
175
175
  catch (error) {
176
+ if (isClaimLostError(error))
177
+ throw error;
176
178
  if (isRunParkedError(error) || this.parked) {
177
179
  return { runId, state };
178
180
  }
@@ -208,24 +210,29 @@ export class WorkflowEngine {
208
210
  state.currentAttemptId !== undefined &&
209
211
  state.currentNodeStartedAt !== undefined &&
210
212
  interruptedNode?.nodeType === "agent" &&
211
- assistantMessageConfig(interruptedNode) !== undefined) {
212
- const resumedSettings = this.persistedSettingsBinding(state);
213
+ (assistantMessageConfig(interruptedNode) !== undefined ||
214
+ options.resumeInteractionAttemptId === state.currentAttemptId)) {
215
+ const resumedSettings = await this.persistedSettingsBinding(state);
213
216
  resumedAttempt = {
214
217
  nodeId: state.currentNode,
215
218
  attemptId: state.currentAttemptId,
216
219
  startedAt: state.currentNodeStartedAt,
220
+ ...(state.currentNodeDeadlineAt === undefined
221
+ ? {}
222
+ : { deadlineAt: state.currentNodeDeadlineAt }),
217
223
  ...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
218
224
  };
219
225
  }
220
- // A resumed run starts unpaused. Submitted and non-agent nodes discard
221
- // stale in-flight markers and start a new attempt. Assistant-message
222
- // nodes keep their attempt id so the origin session can adopt an already
223
- // visible response without showing it twice.
226
+ // A resumed run starts unpaused. Submitted and non-agent nodes normally
227
+ // discard stale in-flight markers and start a new attempt. A submitted
228
+ // interaction and an assistant-message node keep their exact attempt id
229
+ // so durable origin-session work is never detached from its contract.
224
230
  delete state.paused;
225
231
  if (resumedAttempt === undefined) {
226
232
  delete state.currentNode;
227
233
  delete state.currentAttemptId;
228
234
  delete state.currentNodeStartedAt;
235
+ delete state.currentNodeDeadlineAt;
229
236
  delete state.currentSettingsScopeId;
230
237
  delete state.currentSettingsChangeNumber;
231
238
  delete state.currentSettingsHash;
@@ -267,6 +274,8 @@ export class WorkflowEngine {
267
274
  await this.executeGraph(workflow, state, runId, point.nodeId, countExecutableSteps(workflow, state.steps), point.lastOutput, resumedAttempt);
268
275
  }
269
276
  catch (error) {
277
+ if (isClaimLostError(error))
278
+ throw error;
270
279
  if (isRunParkedError(error) || this.parked) {
271
280
  return { runId, state };
272
281
  }
@@ -287,7 +296,7 @@ export class WorkflowEngine {
287
296
  this.cancelled = false;
288
297
  this.paused = false;
289
298
  this.parked = false;
290
- const parent = this.store.readRun(parentRunId);
299
+ const parent = await this.store.readRun(parentRunId);
291
300
  if (parent === null) {
292
301
  throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
293
302
  }
@@ -314,9 +323,7 @@ export class WorkflowEngine {
314
323
  request.requestDigest !== options.humanDecision.requestDigest) {
315
324
  throw new Error("Accepted human decision does not match the waiting request");
316
325
  }
317
- const durableDecision = await new HumanDecisionStore(this.store.databasePath, {
318
- state: this.store.state,
319
- }).readResolved(request.decisionId);
326
+ const durableDecision = await this.store.readResolvedHumanDecision(request.decisionId);
320
327
  if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
321
328
  throw new Error("Accepted human decision does not match the durable decision record");
322
329
  }
@@ -393,6 +400,8 @@ export class WorkflowEngine {
393
400
  await this.executeGraph(workflow, state, runId, point.nodeId, countExecutableSteps(workflow, state.steps), point.lastOutput);
394
401
  }
395
402
  catch (error) {
403
+ if (isClaimLostError(error))
404
+ throw error;
396
405
  if (isRunParkedError(error) || this.parked) {
397
406
  return { runId, state };
398
407
  }
@@ -741,6 +750,7 @@ export class WorkflowEngine {
741
750
  delete state.currentNode;
742
751
  delete state.currentAttemptId;
743
752
  delete state.currentNodeStartedAt;
753
+ delete state.currentNodeDeadlineAt;
744
754
  delete state.currentSettingsScopeId;
745
755
  delete state.currentSettingsChangeNumber;
746
756
  delete state.currentSettingsHash;
@@ -786,7 +796,7 @@ export class WorkflowEngine {
786
796
  }
787
797
  const meta = { promptText: null };
788
798
  try {
789
- const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings);
799
+ const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, resumedAttempt?.deadlineAt);
790
800
  return {
791
801
  result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
792
802
  execution,
@@ -794,6 +804,8 @@ export class WorkflowEngine {
794
804
  };
795
805
  }
796
806
  catch (error) {
807
+ if (isRunParkedError(error) || isClaimLostError(error))
808
+ throw error;
797
809
  const outcome = this.outcomeForError(error);
798
810
  return {
799
811
  result: {
@@ -834,7 +846,7 @@ export class WorkflowEngine {
834
846
  ...(output !== undefined ? { output } : {}),
835
847
  };
836
848
  }
837
- async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings) {
849
+ async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, persistedDeadlineAt) {
838
850
  const abort = new AbortController();
839
851
  const context = this.createNodeContext(state, abort.signal, settings);
840
852
  let timer;
@@ -849,13 +861,54 @@ export class WorkflowEngine {
849
861
  if (this.cancelled) {
850
862
  throw new CancelledError();
851
863
  }
852
- const timeoutMs = await this.resolveNodeTimeout(node, context, abort);
864
+ let timeoutMs;
865
+ let timeoutErrorMs;
866
+ if (persistedDeadlineAt === undefined) {
867
+ timeoutMs = await this.resolveNodeTimeout(node, context, abort);
868
+ timeoutErrorMs = timeoutMs;
869
+ if (node.nodeType === "agent" && this.executor.preservesDeadlineWhileParked === true) {
870
+ state.currentNodeDeadlineAt =
871
+ timeoutMs === null ? null : new Date(Date.now() + timeoutMs).toISOString();
872
+ await this.persist(runId, state, {
873
+ scope: "node",
874
+ type: "node_deadline_set",
875
+ nodeId,
876
+ attemptId,
877
+ payload: { deadlineAt: state.currentNodeDeadlineAt },
878
+ });
879
+ if (state.currentNodeDeadlineAt !== null && timeoutErrorMs !== null) {
880
+ timeoutMs = Date.parse(state.currentNodeDeadlineAt) - Date.now();
881
+ if (timeoutMs <= 0)
882
+ throw new TimeoutError(timeoutErrorMs);
883
+ }
884
+ }
885
+ else {
886
+ delete state.currentNodeDeadlineAt;
887
+ }
888
+ }
889
+ else if (persistedDeadlineAt === null) {
890
+ state.currentNodeDeadlineAt = null;
891
+ timeoutMs = null;
892
+ timeoutErrorMs = null;
893
+ }
894
+ else {
895
+ const deadline = Date.parse(persistedDeadlineAt);
896
+ const started = Date.parse(state.currentNodeStartedAt ?? persistedDeadlineAt);
897
+ if (!Number.isFinite(deadline) || !Number.isFinite(started)) {
898
+ throw new Error("Persisted node deadline is invalid");
899
+ }
900
+ state.currentNodeDeadlineAt = persistedDeadlineAt;
901
+ timeoutMs = deadline - Date.now();
902
+ timeoutErrorMs = Math.max(1, deadline - started);
903
+ if (timeoutMs <= 0)
904
+ throw new TimeoutError(timeoutErrorMs);
905
+ }
853
906
  if (abort.signal.aborted) {
854
907
  throw abortError(abort.signal);
855
908
  }
856
- if (timeoutMs !== null) {
909
+ if (timeoutMs !== null && timeoutErrorMs !== null) {
857
910
  timer = setTimeout(() => {
858
- abort.abort(new TimeoutError(timeoutMs));
911
+ abort.abort(new TimeoutError(timeoutErrorMs));
859
912
  }, timeoutMs);
860
913
  }
861
914
  this.activeAttempt = {
@@ -988,7 +1041,7 @@ export class WorkflowEngine {
988
1041
  if (definition === undefined)
989
1042
  return undefined;
990
1043
  const invocation = mountPath === "" ? 1 : currentMountInvocation(state, mountPath);
991
- const existing = this.store.findSettingsScope(state.runId, mountPath, invocation);
1044
+ const existing = await this.store.findSettingsScope(state.runId, mountPath, invocation);
992
1045
  if (existing !== undefined)
993
1046
  return existing;
994
1047
  const input = mountPath === "" ? state.input : currentMountInput(state, mountPath);
@@ -996,21 +1049,21 @@ export class WorkflowEngine {
996
1049
  const initial = mappedSettings.present
997
1050
  ? await parseWorkflowSettingsValue(definition, mappedSettings.value)
998
1051
  : await resolveInitialWorkflowSettings(definition, input);
999
- return this.store.ensureSettingsScope({
1052
+ return await this.store.ensureSettingsScope({
1000
1053
  runId: state.runId,
1001
1054
  mountPath,
1002
1055
  invocation,
1003
1056
  settings: initial.json,
1004
1057
  });
1005
1058
  }
1006
- persistedSettingsBinding(state) {
1059
+ async persistedSettingsBinding(state) {
1007
1060
  if (state.currentSettingsScopeId === undefined)
1008
1061
  return undefined;
1009
1062
  if (state.currentSettingsChangeNumber === undefined ||
1010
1063
  state.currentSettingsHash === undefined) {
1011
1064
  throw new Error("Saved workflow settings binding is incomplete");
1012
1065
  }
1013
- const scope = this.store.getSettingsScopeAtChange(state.currentSettingsScopeId, state.currentSettingsChangeNumber);
1066
+ const scope = await this.store.getSettingsScopeAtChange(state.currentSettingsScopeId, state.currentSettingsChangeNumber);
1014
1067
  if (scope === undefined) {
1015
1068
  throw new Error(`Saved workflow settings scope is missing: ${state.currentSettingsScopeId}`);
1016
1069
  }
@@ -1125,16 +1178,79 @@ export class WorkflowEngine {
1125
1178
  }
1126
1179
  }
1127
1180
  async runActionNode(node, context, nodeId, attemptId, signal, meta) {
1181
+ /* istanbul ignore if -- graph validation rejects action nodes without managed effects */
1182
+ if (node.effect === undefined) {
1183
+ throw new Error(`Action node ${nodeId} must declare a managed effect with an explicit recovery policy`);
1184
+ }
1185
+ const effectType = node.effect.type.trim();
1186
+ const idempotencyKey = typeof node.effect.idempotencyKey === "function"
1187
+ ? await node.effect.idempotencyKey(context)
1188
+ : node.effect.idempotencyKey;
1189
+ const request = typeof node.effect.request === "function"
1190
+ ? await node.effect.request(context)
1191
+ : node.effect.request;
1192
+ /* istanbul ignore if -- definition validation and effect helpers reject empty fields */
1193
+ if (effectType.length === 0 || idempotencyKey.trim().length === 0) {
1194
+ throw new Error("Managed effect type and idempotency key must be nonempty text");
1195
+ }
1196
+ canonicalJson(request);
1197
+ const reservation = await this.store.reserveEffect({
1198
+ runId: context.state.runId,
1199
+ attemptId,
1200
+ effectType,
1201
+ idempotencyKey,
1202
+ request: request,
1203
+ recovery: node.effect.recovery,
1204
+ });
1205
+ if (reservation.disposition === "ambiguous")
1206
+ throw new RunParkedError();
1207
+ if (reservation.disposition === "adopted") {
1208
+ const adopted = reservation.result;
1209
+ if (adopted === null || typeof adopted !== "object" || Array.isArray(adopted)) {
1210
+ throw new Error("Managed effect receipt is invalid");
1211
+ }
1212
+ const execution = adopted;
1213
+ if (execution.action !== undefined)
1214
+ meta.action = execution.action;
1215
+ return execution;
1216
+ }
1128
1217
  const actionContext = {
1129
1218
  ...context,
1219
+ effect: { type: effectType, idempotencyKey, recovery: node.effect.recovery },
1130
1220
  publishUpdate: async (update) => await this.publishUpdate(nodeId, attemptId, update),
1131
1221
  };
1132
- if ("exec" in node) {
1133
- return await runShellActionNode(node, context, actionContext, signal, meta);
1222
+ try {
1223
+ let execution;
1224
+ if ("exec" in node) {
1225
+ execution = await runShellActionNode(node, actionContext, signal, meta);
1226
+ }
1227
+ else {
1228
+ meta.action = { actionType: "function" };
1229
+ const output = await node.run(actionContext);
1230
+ execution = { output, promptText: null, action: { actionType: "function" } };
1231
+ }
1232
+ canonicalJson(execution);
1233
+ await this.store.settleEffect({
1234
+ runId: context.state.runId,
1235
+ effectId: reservation.effectId,
1236
+ attemptNumber: reservation.attemptNumber,
1237
+ outcome: "applied",
1238
+ result: execution,
1239
+ });
1240
+ return execution;
1241
+ }
1242
+ catch (error) {
1243
+ if (isRunParkedError(error) || isClaimLostError(error))
1244
+ throw error;
1245
+ await this.store.settleEffect({
1246
+ runId: context.state.runId,
1247
+ effectId: reservation.effectId,
1248
+ attemptNumber: reservation.attemptNumber,
1249
+ outcome: "rejected",
1250
+ error: errorMessage(error),
1251
+ });
1252
+ throw error;
1134
1253
  }
1135
- meta.action = { actionType: "function" };
1136
- const output = await node.run(actionContext);
1137
- return { output, promptText: null, action: { actionType: "function" } };
1138
1254
  }
1139
1255
  async persist(runId, state, event) {
1140
1256
  const traceEvent = await this.store.writeSnapshot(runId, state, event);
@@ -1145,19 +1261,12 @@ export class WorkflowEngine {
1145
1261
  // Observers (UI updates, loggers) must never determine workflow
1146
1262
  // correctness; a throwing observer would otherwise fail the run.
1147
1263
  }
1264
+ await new Promise((resolve) => setImmediate(resolve));
1148
1265
  }
1149
1266
  async finishRun(runId, state, status, fields) {
1150
1267
  if (status === "failed" && state.status === "timed_out") {
1151
1268
  status = "timed_out";
1152
1269
  }
1153
- // Let observers (e.g. the session recorder) stop and drain before the
1154
- // terminal event exists, so the terminal fact is immutable from that point on.
1155
- try {
1156
- await this.onRunFinishing?.(runId, state);
1157
- }
1158
- catch {
1159
- // Finishing the run wins over observer failures.
1160
- }
1161
1270
  state.status = status;
1162
1271
  state.finishedAt = new Date().toISOString();
1163
1272
  if (fields.error !== undefined) {
@@ -1172,9 +1281,18 @@ export class WorkflowEngine {
1172
1281
  delete state.currentNode;
1173
1282
  delete state.currentAttemptId;
1174
1283
  delete state.currentNodeStartedAt;
1284
+ delete state.currentNodeDeadlineAt;
1175
1285
  delete state.currentSettingsScopeId;
1176
1286
  delete state.currentSettingsChangeNumber;
1177
1287
  delete state.currentSettingsHash;
1288
+ // Let observers stop, drain, and prepare terminal delivery from the final
1289
+ // in-memory state before the immutable terminal event is committed.
1290
+ try {
1291
+ await this.onRunFinishing?.(runId, state);
1292
+ }
1293
+ catch {
1294
+ // Finishing the run wins over observer failures.
1295
+ }
1178
1296
  await this.persist(runId, state, {
1179
1297
  scope: "run",
1180
1298
  type: `run_${status}`,
@@ -1206,9 +1324,7 @@ async function runCheckpointNode(node, context, execution) {
1206
1324
  prompt,
1207
1325
  ...(timeout !== undefined ? { timeout } : {}),
1208
1326
  });
1209
- await new HumanDecisionStore(execution.store.databasePath, {
1210
- state: execution.store.state,
1211
- }).createRequest(request);
1327
+ await execution.store.createHumanDecisionRequest(request);
1212
1328
  return { output: request, promptText: null };
1213
1329
  }
1214
1330
  const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
@@ -1225,8 +1341,8 @@ function shellReceipt(result) {
1225
1341
  durationMs: result.durationMs,
1226
1342
  };
1227
1343
  }
1228
- async function runShellActionNode(node, context, actionContext, signal, meta) {
1229
- const spec = await node.exec(context);
1344
+ async function runShellActionNode(node, actionContext, signal, meta) {
1345
+ const spec = await node.exec(actionContext);
1230
1346
  const streams = new Set(node.updates?.streams ?? ["stdout"]);
1231
1347
  let result;
1232
1348
  try {
@@ -1251,7 +1367,7 @@ async function runShellActionNode(node, context, actionContext, signal, meta) {
1251
1367
  throw error;
1252
1368
  }
1253
1369
  meta.action = shellReceipt(result);
1254
- const output = node.parse ? await node.parse(result, context) : result;
1370
+ const output = node.parse ? await node.parse(result, actionContext) : result;
1255
1371
  return { output, promptText: null, action: shellReceipt(result) };
1256
1372
  }
1257
1373
  function assertValidTimeout(value) {
@@ -1427,7 +1543,7 @@ function assistantMessageConfig(node) {
1427
1543
  return assistantMessageOutput(node.expectedOutput);
1428
1544
  }
1429
1545
  function appendLiveControlInstructions(prompt, workflow, nodeId, context) {
1430
- const lines = [prompt.trimEnd(), "", "Workflow changes during this run:"];
1546
+ const lines = [prompt.trimEnd(), "", "Workflow context for this run:"];
1431
1547
  if (context.settingsScopeId !== undefined &&
1432
1548
  context.settingsChangeNumber !== undefined &&
1433
1549
  context.settings !== undefined) {
@@ -1441,12 +1557,11 @@ function appendLiveControlInstructions(prompt, workflow, nodeId, context) {
1441
1557
  .join(", ");
1442
1558
  lines.push(`- Active settings scope: ${context.settingsScopeId}`, `- Current settings change number: ${context.settingsChangeNumber}`, ...(definition?.description !== undefined
1443
1559
  ? [`- Settings purpose: ${definition.description}`]
1444
- : []), `- Current settings: ${summary}`, `- Changes allowed from this model step: ${allowed || "none"}`, "- Treat the current settings as authoritative for future behavior in this workflow scope.", "- To change future workflow settings, use the workflow tool with action change-settings, this scopeId, an RFC 6902 patch, and optionally expectedChangeNumber.");
1560
+ : []), `- Current settings: ${summary}`, `- Settings permissions: ${allowed || "none"}`, "- Treat the current settings as authoritative for this workflow scope.");
1445
1561
  }
1446
1562
  else {
1447
1563
  lines.push("- This workflow scope has no editable settings.");
1448
1564
  }
1449
- lines.push("- To save normal user work for after successful completion, use workflow action queue-follow-up once for each prompt.", "- To remove an unsent prompt that this model queued, use workflow action remove-follow-up with its followUpId.", "- Setting changes affect only future node attempts and future settings routes. They do not change this running attempt or completed work.");
1450
1565
  return lines.join("\n");
1451
1566
  }
1452
1567
  function settingsDefinitionForNode(workflow, nodeId) {