@osolmaz/pi-workflows 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +56 -31
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +797 -142
  4. package/dist/builtins/autoimplement.workflow.js +17 -105
  5. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  6. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  7. package/dist/builtins/autoplan.workflow.js +200 -29
  8. package/dist/builtins/autoplan.workflow.js.map +1 -1
  9. package/dist/builtins/catalog.js +7 -5
  10. package/dist/builtins/catalog.js.map +1 -1
  11. package/dist/builtins/index.d.ts +4 -2
  12. package/dist/builtins/index.js +3 -1
  13. package/dist/builtins/index.js.map +1 -1
  14. package/dist/builtins/monitor.workflow.d.ts +56 -23
  15. package/dist/builtins/monitor.workflow.js +508 -329
  16. package/dist/builtins/monitor.workflow.js.map +1 -1
  17. package/dist/builtins/pi-agent-group.d.ts +72 -0
  18. package/dist/builtins/pi-agent-group.js +1087 -0
  19. package/dist/builtins/pi-agent-group.js.map +1 -0
  20. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  21. package/dist/builtins/plain-summary.workflow.js +134 -0
  22. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  23. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  24. package/dist/builtins/plan-approval.workflow.js +92 -14
  25. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  26. package/dist/builtins/plan-change.workflow.d.ts +449 -0
  27. package/dist/builtins/plan-change.workflow.js +256 -0
  28. package/dist/builtins/plan-change.workflow.js.map +1 -0
  29. package/dist/builtins/plan-presentation.js +2 -2
  30. package/dist/builtins/plan-presentation.js.map +1 -1
  31. package/dist/builtins/sanity-check.workflow.d.ts +50 -6
  32. package/dist/builtins/sanity-check.workflow.js +150 -28
  33. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  34. package/dist/controllers/effects.d.ts +3 -2
  35. package/dist/controllers/effects.js +8 -1
  36. package/dist/controllers/effects.js.map +1 -1
  37. package/dist/controllers/index.d.ts +1 -1
  38. package/dist/controllers/index.js +1 -1
  39. package/dist/controllers/index.js.map +1 -1
  40. package/dist/controllers/manager.d.ts +1 -0
  41. package/dist/controllers/manager.js +41 -17
  42. package/dist/controllers/manager.js.map +1 -1
  43. package/dist/controllers/sqlite.d.ts +107 -85
  44. package/dist/controllers/sqlite.js +1611 -1486
  45. package/dist/controllers/sqlite.js.map +1 -1
  46. package/dist/controllers/store.d.ts +9 -8
  47. package/dist/controllers/store.js +1 -31
  48. package/dist/controllers/store.js.map +1 -1
  49. package/dist/controllers/types.d.ts +3 -0
  50. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  51. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  52. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  53. package/dist/controllers/workflows.d.ts +2 -2
  54. package/dist/controllers/workflows.js +11 -10
  55. package/dist/controllers/workflows.js.map +1 -1
  56. package/dist/extension/controller-host.js +2 -2
  57. package/dist/extension/controller-host.js.map +1 -1
  58. package/dist/extension/decision-channels.d.ts +4 -2
  59. package/dist/extension/decision-channels.js +333 -146
  60. package/dist/extension/decision-channels.js.map +1 -1
  61. package/dist/extension/executor.d.ts +9 -2
  62. package/dist/extension/executor.js +89 -2
  63. package/dist/extension/executor.js.map +1 -1
  64. package/dist/extension/herdr-viewer.d.ts +0 -1
  65. package/dist/extension/herdr-viewer.js +0 -4
  66. package/dist/extension/herdr-viewer.js.map +1 -1
  67. package/dist/extension/index.js +209 -127
  68. package/dist/extension/index.js.map +1 -1
  69. package/dist/extension/recorder.d.ts +3 -10
  70. package/dist/extension/recorder.js +13 -22
  71. package/dist/extension/recorder.js.map +1 -1
  72. package/dist/extension/session-events.d.ts +2 -2
  73. package/dist/extension/step-message.d.ts +7 -2
  74. package/dist/extension/step-message.js +94 -2
  75. package/dist/extension/step-message.js.map +1 -1
  76. package/dist/extension/widget.js +29 -3
  77. package/dist/extension/widget.js.map +1 -1
  78. package/dist/host/rpc-executor.d.ts +3 -0
  79. package/dist/host/rpc-executor.js +2 -0
  80. package/dist/host/rpc-executor.js.map +1 -1
  81. package/dist/host/runner.d.ts +3 -6
  82. package/dist/host/runner.js +83 -32
  83. package/dist/host/runner.js.map +1 -1
  84. package/dist/render/graph-render.js +15 -10
  85. package/dist/render/graph-render.js.map +1 -1
  86. package/dist/state/database.d.ts +35 -0
  87. package/dist/state/database.js +287 -0
  88. package/dist/state/database.js.map +1 -0
  89. package/dist/state/index.d.ts +4 -0
  90. package/dist/state/index.js +5 -0
  91. package/dist/state/index.js.map +1 -0
  92. package/dist/state/json.d.ts +6 -0
  93. package/dist/state/json.js +38 -0
  94. package/dist/state/json.js.map +1 -0
  95. package/dist/state/mutation.d.ts +57 -0
  96. package/dist/state/mutation.js +255 -0
  97. package/dist/state/mutation.js.map +1 -0
  98. package/dist/state/schema.d.ts +6 -0
  99. package/dist/state/schema.js +473 -0
  100. package/dist/state/schema.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +2 -2
  102. package/dist/viewer/cli.js +142 -122
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/viewer/render.d.ts +4 -4
  105. package/dist/viewer/render.js +9 -23
  106. package/dist/viewer/render.js.map +1 -1
  107. package/dist/viewer/session-reducer.d.ts +1 -1
  108. package/dist/viewer/session-reducer.js +1 -1
  109. package/dist/viewer/tui.d.ts +3 -3
  110. package/dist/viewer/tui.js +15 -13
  111. package/dist/viewer/tui.js.map +1 -1
  112. package/dist/viewer/watch.d.ts +2 -5
  113. package/dist/viewer/watch.js +13 -13
  114. package/dist/viewer/watch.js.map +1 -1
  115. package/dist/workflows/catalog.js +7 -2
  116. package/dist/workflows/catalog.js.map +1 -1
  117. package/dist/workflows/composition.js +20 -2
  118. package/dist/workflows/composition.js.map +1 -1
  119. package/dist/workflows/decision-presentation.d.ts +1 -1
  120. package/dist/workflows/decision-presentation.js +51 -38
  121. package/dist/workflows/decision-presentation.js.map +1 -1
  122. package/dist/workflows/definition.d.ts +6 -2
  123. package/dist/workflows/definition.js +17 -0
  124. package/dist/workflows/definition.js.map +1 -1
  125. package/dist/workflows/engine.d.ts +7 -7
  126. package/dist/workflows/engine.js +196 -115
  127. package/dist/workflows/engine.js.map +1 -1
  128. package/dist/workflows/errors.d.ts +16 -3
  129. package/dist/workflows/errors.js +18 -3
  130. package/dist/workflows/errors.js.map +1 -1
  131. package/dist/workflows/human-decision.d.ts +45 -8
  132. package/dist/workflows/human-decision.js +641 -274
  133. package/dist/workflows/human-decision.js.map +1 -1
  134. package/dist/workflows/index.d.ts +5 -6
  135. package/dist/workflows/index.js +4 -5
  136. package/dist/workflows/index.js.map +1 -1
  137. package/dist/workflows/progress.d.ts +1 -0
  138. package/dist/workflows/progress.js +15 -3
  139. package/dist/workflows/progress.js.map +1 -1
  140. package/dist/workflows/schema.js +27 -1
  141. package/dist/workflows/schema.js.map +1 -1
  142. package/dist/workflows/store.d.ts +83 -103
  143. package/dist/workflows/store.js +907 -951
  144. package/dist/workflows/store.js.map +1 -1
  145. package/dist/workflows/types.d.ts +88 -126
  146. package/docs/CONTROLLERS.md +10 -8
  147. package/docs/DEFERRED_TURNS.md +2 -2
  148. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  149. package/docs/HUMAN_DECISIONS.md +36 -47
  150. package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
  151. package/docs/MONITOR.md +179 -237
  152. package/docs/SQLITE_STATE.md +219 -0
  153. package/docs/WORKFLOW_COMPOSITION.md +27 -26
  154. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  155. package/docs/WORKFLOW_UPDATES.md +10 -9
  156. package/docs/development.md +9 -9
  157. package/docs/live-replay-protocol.md +28 -37
  158. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  159. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  160. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  161. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  162. package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
  163. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  164. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  165. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  166. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  167. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  168. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  169. package/docs/session-event-journal.md +63 -434
  170. package/docs/tui-viewer.md +13 -15
  171. package/docs/workflows.md +127 -53
  172. package/examples/workflows/approved-plan.workflow.ts +19 -46
  173. package/examples/workflows/plain-summary.workflow.ts +1 -0
  174. package/herdr-plugin.toml +1 -1
  175. package/package.json +7 -7
  176. package/plugins/herdr/viewer.mjs +1 -11
  177. package/schemas/human-decision-accepted-v1.schema.json +15 -3
  178. package/schemas/human-decision-continuation-v1.schema.json +10 -1
  179. package/schemas/human-decision-delivery-v1.schema.json +8 -0
  180. package/schemas/human-decision-receipt-v1.schema.json +8 -0
  181. package/schemas/human-decision-request-v1.schema.json +24 -4
  182. package/skills/autoimplement/SKILL.md +27 -0
  183. package/skills/autoplan/SKILL.md +5 -4
  184. package/skills/monitor/SKILL.md +91 -135
  185. package/skills/pi-workflows/SKILL.md +9 -8
  186. package/skills/sanity-check/SKILL.md +44 -0
  187. package/src/builtins/autoimplement.workflow.ts +19 -118
  188. package/src/builtins/autoplan.workflow.ts +290 -40
  189. package/src/builtins/catalog.ts +7 -5
  190. package/src/builtins/index.ts +18 -1
  191. package/src/builtins/monitor.workflow.ts +673 -382
  192. package/src/builtins/pi-agent-group.ts +1407 -0
  193. package/src/builtins/plain-summary.workflow.ts +185 -0
  194. package/src/builtins/plan-approval.workflow.ts +157 -24
  195. package/src/builtins/plan-change.workflow.ts +321 -0
  196. package/src/builtins/plan-presentation.ts +2 -2
  197. package/src/builtins/sanity-check.workflow.ts +248 -48
  198. package/src/controllers/effects.ts +7 -0
  199. package/src/controllers/index.ts +0 -6
  200. package/src/controllers/manager.ts +72 -32
  201. package/src/controllers/sqlite.ts +2420 -1948
  202. package/src/controllers/store.ts +17 -38
  203. package/src/controllers/types.ts +3 -0
  204. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  205. package/src/controllers/workflows.ts +45 -21
  206. package/src/extension/controller-host.ts +1 -2
  207. package/src/extension/decision-channels.ts +456 -203
  208. package/src/extension/executor.ts +105 -2
  209. package/src/extension/herdr-viewer.ts +0 -5
  210. package/src/extension/index.ts +251 -151
  211. package/src/extension/recorder.ts +13 -22
  212. package/src/extension/session-events.ts +2 -2
  213. package/src/extension/step-message.ts +106 -4
  214. package/src/extension/widget.ts +32 -5
  215. package/src/host/rpc-executor.ts +4 -0
  216. package/src/host/runner.ts +93 -41
  217. package/src/render/graph-render.ts +12 -6
  218. package/src/state/database.ts +375 -0
  219. package/src/state/index.ts +29 -0
  220. package/src/state/json.ts +42 -0
  221. package/src/state/mutation.ts +435 -0
  222. package/src/state/schema.ts +475 -0
  223. package/src/viewer/cli.ts +151 -144
  224. package/src/viewer/render.ts +19 -30
  225. package/src/viewer/session-reducer.ts +1 -1
  226. package/src/viewer/tui.ts +18 -17
  227. package/src/viewer/watch.ts +13 -17
  228. package/src/workflows/catalog.ts +7 -2
  229. package/src/workflows/composition.ts +21 -3
  230. package/src/workflows/decision-presentation.ts +56 -43
  231. package/src/workflows/definition.ts +36 -3
  232. package/src/workflows/engine.ts +228 -130
  233. package/src/workflows/errors.ts +27 -3
  234. package/src/workflows/human-decision.ts +921 -349
  235. package/src/workflows/index.ts +20 -30
  236. package/src/workflows/progress.ts +18 -3
  237. package/src/workflows/schema.ts +36 -1
  238. package/src/workflows/store.ts +1300 -1163
  239. package/src/workflows/types.ts +100 -137
  240. package/dist/builtins/sanity-check-session.d.ts +0 -17
  241. package/dist/builtins/sanity-check-session.js +0 -168
  242. package/dist/builtins/sanity-check-session.js.map +0 -1
  243. package/dist/workflows/artifacts.d.ts +0 -40
  244. package/dist/workflows/artifacts.js +0 -155
  245. package/dist/workflows/artifacts.js.map +0 -1
  246. package/dist/workflows/migrate-sources.d.ts +0 -42
  247. package/dist/workflows/migrate-sources.js +0 -133
  248. package/dist/workflows/migrate-sources.js.map +0 -1
  249. package/docs/run-bundles.md +0 -481
  250. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  251. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  252. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  253. package/schemas/human-decision-request-v2.schema.json +0 -69
  254. package/schemas/human-decision-resolution-v2.schema.json +0 -27
  255. package/src/builtins/sanity-check-session.ts +0 -205
  256. package/src/workflows/artifacts.ts +0 -188
  257. package/src/workflows/migrate-sources.ts +0 -178
@@ -1,17 +1,15 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import path from "node:path";
3
2
  import { isDeepStrictEqual } from "node:util";
4
3
  import { builtinWorkflowCatalog } from "../builtins/catalog.js";
5
- import { projectControllerStorePath, SqliteControllerStore, } from "../controllers/index.js";
4
+ import { SqliteControllerStore } from "../controllers/index.js";
6
5
  import { compositionMetadata } from "../workflows/composition.js";
7
6
  import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
8
7
  import { WorkflowEngine } from "../workflows/engine.js";
9
- import { ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
8
+ import { BuiltinWorkflowRevisionChangedError, ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
10
9
  import { HumanDecisionStore, createHumanDecisionAttemptId, validateHumanDecisionResponse, } from "../workflows/human-decision.js";
11
10
  import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
12
- import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
13
11
  import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/progress.js";
14
- import { createRunId, listRunBundles, readLastTraceEvent, readRunBundle, WorkflowRunStore, createDefinitionSnapshot, } from "../workflows/store.js";
12
+ import { createRunId, listWorkflowRuns, readWorkflowRun, WorkflowRunStore, createDefinitionSnapshot, } from "../workflows/store.js";
15
13
  import { PiControllerHost, parseControllerArgs, } from "./controller-host.js";
16
14
  import { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTelegramChannels, loadDecisionChannelConfig, verifyTelegramTokenFile, writeDecisionChannelProfile, } from "./decision-channels.js";
17
15
  import { DeferredTurnCoordinator, } from "./deferred-turn-coordinator.js";
@@ -19,7 +17,7 @@ import { buildDeferredTurnContent, createDeferredTurnDescriptor, DEFERRED_TURN_M
19
17
  import { ConversationStepExecutor } from "./executor.js";
20
18
  import { HerdrWorkflowViewer, parseViewerPlacement, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
21
19
  import { SessionRecorder } from "./recorder.js";
22
- import { registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
20
+ import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
23
21
  import { buildWidgetView } from "./widget.js";
24
22
  import { parseWorkflowToolInput, WorkflowToolParameters } from "./workflow-tool.js";
25
23
  export { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTelegramChannels, decisionConfigDir, loadDecisionChannelConfig, verifyTelegramTokenFile, writeDecisionChannelProfile, } from "./decision-channels.js";
@@ -55,10 +53,8 @@ function humanDecisionRequest(value) {
55
53
  const schema = value !== null && typeof value === "object"
56
54
  ? value.schema
57
55
  : undefined;
58
- if (schema !== "pi-workflows.human-decision-request.v1" &&
59
- schema !== "pi-workflows.human-decision-request.v2") {
56
+ if (schema !== "pi-workflows.human-decision-request.v1")
60
57
  return null;
61
- }
62
58
  return value;
63
59
  }
64
60
  function definitionDigest(snapshot) {
@@ -70,6 +66,20 @@ function launchSourceIdentity(workflow, root) {
70
66
  mounted: compositionMetadata(workflow)?.sources ?? [],
71
67
  };
72
68
  }
69
+ function continuationSourceChangedError(runId, previous, current) {
70
+ if (previous.kind === "builtin" &&
71
+ current.kind === "builtin" &&
72
+ previous.id === current.id &&
73
+ previous.revision !== current.revision) {
74
+ return new BuiltinWorkflowRevisionChangedError({
75
+ runId,
76
+ workflowId: current.id,
77
+ previousRevision: previous.revision,
78
+ currentRevision: current.revision,
79
+ });
80
+ }
81
+ return new Error(`Workflow source changed since run ${runId} started; revert the edit to answer its checkpoint`);
82
+ }
73
83
  function preparedLaunchOptions(options) {
74
84
  return {
75
85
  ...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
@@ -89,7 +99,7 @@ function safeLaunchError(error) {
89
99
  .replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
90
100
  .replaceAll("\n", " ")
91
101
  .trim();
92
- const code = /not found|cannot find|unknown workflow/iu.test(raw)
102
+ const code = /not found|cannot find|unknown workflow|no such file or directory/iu.test(raw)
93
103
  ? "workflow_not_found"
94
104
  : /source changed|source mismatch/iu.test(raw)
95
105
  ? "source_changed"
@@ -212,11 +222,21 @@ export default function piWorkflows(pi) {
212
222
  ...[...telegramDecisionChannels.values()].map(async (channel) => channel.settle(decision)),
213
223
  ]);
214
224
  };
215
- const migrationBlockedRuns = new Set();
216
225
  const ensureRunQueueStore = (cwd) => {
217
- runQueueStore ??= new SqliteControllerStore(projectControllerStorePath(cwd));
226
+ runQueueStore ??= new SqliteControllerStore(undefined, { projectPath: cwd });
218
227
  return runQueueStore;
219
228
  };
229
+ const workflowStore = (cwd, options = {}) => {
230
+ const queue = ensureRunQueueStore(cwd);
231
+ return new WorkflowRunStore(queue.filePath, { ...options, state: queue.state });
232
+ };
233
+ const humanDecisionStore = (cwd, authorityProvider) => {
234
+ const queue = ensureRunQueueStore(cwd);
235
+ return new HumanDecisionStore(queue.filePath, {
236
+ state: queue.state,
237
+ ...(authorityProvider === undefined ? {} : { authorityProvider }),
238
+ });
239
+ };
220
240
  // Session-addressed delivery: each session polls only its durable outbox.
221
241
  // Run events remain an audit feed and never enter a conversation.
222
242
  let syncArmed = false;
@@ -557,7 +577,6 @@ export default function piWorkflows(pi) {
557
577
  workflowViewTarget = {
558
578
  runId: state.runId,
559
579
  workflowName: state.workflowName,
560
- runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
561
580
  };
562
581
  renderWidget(ctx);
563
582
  };
@@ -734,14 +753,16 @@ export default function piWorkflows(pi) {
734
753
  }
735
754
  try {
736
755
  if (outcome === "park") {
737
- runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
756
+ if (runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
757
+ runQueueStore.settleRunEffect(run.runId, "run.park_queue");
758
+ }
738
759
  }
739
- else {
740
- runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
760
+ else if (runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
761
+ runQueueStore.settleRunEffect(run.runId, "run.settle_queue");
741
762
  }
742
763
  }
743
764
  catch {
744
- // Queue bookkeeping is best-effort next to the durable bundle.
765
+ // Queue bookkeeping is best-effort next to the durable run fact.
745
766
  }
746
767
  finally {
747
768
  run.claimToken = undefined;
@@ -752,7 +773,7 @@ export default function piWorkflows(pi) {
752
773
  activeRun = null;
753
774
  }
754
775
  if (result.state.status === "running") {
755
- // The run was parked for another runner; its bundle stays resumable
776
+ // The run was parked for another runner; its SQLite state stays resumable
756
777
  // and its queue row becomes claimable. The recorder drains first: its
757
778
  // finalization writes share the fenced store, so the claim must stay
758
779
  // valid until capture is complete. Nothing else to present.
@@ -776,7 +797,7 @@ export default function piWorkflows(pi) {
776
797
  notify(ctx, `Workflow ${run.workflowName} continues under another runner (run ${run.runId}).`);
777
798
  return;
778
799
  }
779
- releaseClaim(run, "done");
800
+ releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
780
801
  recordRunEvent({
781
802
  runId: run.runId,
782
803
  workflowRef: run.workflowName,
@@ -882,13 +903,13 @@ export default function piWorkflows(pi) {
882
903
  // refused continuation (edited source, missing parent) must not consume
883
904
  // the parent's one-continuation slot.
884
905
  if (options.parentRunId !== undefined) {
885
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
906
+ const parent = readWorkflowRun(options.parentRunId);
886
907
  if (parent === null || parent.state.status !== "waiting") {
887
908
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
888
909
  }
889
910
  if (parent.state.workflowSource !== undefined &&
890
911
  !isDeepStrictEqual(parent.state.workflowSource, workflowSource)) {
891
- throw new Error(`Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`);
912
+ throw continuationSourceChangedError(options.parentRunId, parent.state.workflowSource, workflowSource);
892
913
  }
893
914
  }
894
915
  // Interactive runs are queued and claimed atomically, so this session
@@ -909,6 +930,7 @@ export default function piWorkflows(pi) {
909
930
  : workflowSource.path,
910
931
  workflowSource: launchSourceIdentity(workflow, workflowSource),
911
932
  definitionDigest: definitionDigest(snapshot),
933
+ definitionSnapshot: snapshot,
912
934
  input,
913
935
  launchOptions: preparedLaunchOptions(options),
914
936
  runnerId,
@@ -982,14 +1004,17 @@ export default function piWorkflows(pi) {
982
1004
  },
983
1005
  conversation: {
984
1006
  beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
1007
+ recoverAssistant: (contract) => recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
985
1008
  mark: () => run.recorder?.mark() ?? 0,
986
1009
  rangeSince: (mark) => run.recorder?.rangeSince(mark),
987
1010
  },
988
1011
  });
989
- // The store is shared between the engine and the session recorder so the
990
- // trace sequence stays single-writer (see docs/run-bundles.md). Queued
991
- // runs are fenced: every write proves the claim is still ours first.
992
- const store = new WorkflowRunStore(undefined, fence === undefined ? {} : { fenceProvider: () => fence });
1012
+ // The engine and session recorder share one transactional SQLite store.
1013
+ const store = workflowStore(ctx.cwd, queueStore === null || claimToken === undefined
1014
+ ? {}
1015
+ : {
1016
+ authorityProvider: () => queueStore.workflowRunAuthority(runId, claimToken),
1017
+ });
993
1018
  const engine = new WorkflowEngine({
994
1019
  executor,
995
1020
  store,
@@ -1014,8 +1039,8 @@ export default function piWorkflows(pi) {
1014
1039
  },
1015
1040
  // Awaited by the engine after run_started is persisted, so the session
1016
1041
  // binding and its trace event always precede node and terminal events.
1017
- onRunStarted: async (runDir, state) => {
1018
- const recorder = new SessionRecorder(store, runDir, state.runId);
1042
+ onRunStarted: async (runId) => {
1043
+ const recorder = new SessionRecorder(store, runId);
1019
1044
  try {
1020
1045
  await recorder.bind(ctx);
1021
1046
  run.recorder = recorder;
@@ -1107,31 +1132,30 @@ export default function piWorkflows(pi) {
1107
1132
  // once the cause is fixed, and say so in the feed.
1108
1133
  let resumeFailed = run.resume === true && !isClaimLostError(error);
1109
1134
  let terminalStatus;
1110
- // Re-parking only makes sense while the bundle is still resumable.
1111
- // A terminal or waiting bundle closes the queue row instead, or
1112
- // every session start would retry it forever — and the feed reports
1113
- // the bundle's real state, not a bogus failure.
1135
+ // Re-parking only makes sense while the run is still resumable.
1136
+ // A terminal or waiting run closes the queue row instead, and the feed
1137
+ // reports the authoritative run state.
1114
1138
  if (resumeFailed) {
1115
1139
  try {
1116
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1140
+ const bundle = readWorkflowRun(runId);
1117
1141
  if (bundle === null || bundle.state.status !== "running") {
1118
1142
  resumeFailed = false;
1119
1143
  terminalStatus = bundle?.state.status;
1120
1144
  }
1121
1145
  }
1122
1146
  catch {
1123
- // Unreadable bundles close the row too.
1147
+ // Unreadable run state closes the row too.
1124
1148
  resumeFailed = false;
1125
1149
  }
1126
1150
  }
1127
- // A continuation that failed before its bundle exists frees the
1151
+ // A continuation that failed before its run row exists frees the
1128
1152
  // parent's continuation slot instead of consuming it forever.
1129
1153
  let continuationSlotFreed = false;
1130
1154
  if (run.parentRunId !== undefined &&
1131
1155
  run.claimToken !== undefined &&
1132
1156
  !isClaimLostError(error)) {
1133
1157
  try {
1134
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1158
+ const bundle = readWorkflowRun(runId);
1135
1159
  if (bundle === null) {
1136
1160
  continuationSlotFreed =
1137
1161
  runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
@@ -1201,7 +1225,7 @@ export default function piWorkflows(pi) {
1201
1225
  runnerId,
1202
1226
  claimToken,
1203
1227
  leaseMs: RUN_CLAIM_LEASE_MS,
1204
- excludeRunIds: [...migrationBlockedRuns],
1228
+ excludeRunIds: [],
1205
1229
  sessionId: ctx.sessionManager.getSessionId(),
1206
1230
  });
1207
1231
  if (claimed === undefined) {
@@ -1209,7 +1233,11 @@ export default function piWorkflows(pi) {
1209
1233
  }
1210
1234
  let started;
1211
1235
  try {
1212
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(claimed.runId));
1236
+ const bundle = readWorkflowRun(claimed.runId);
1237
+ if (bundle?.state.status === "waiting") {
1238
+ queueStore.parkWorkflowRun({ runId: claimed.runId, claimToken });
1239
+ return;
1240
+ }
1213
1241
  const sourceRef = bundle?.state.workflowSource === undefined
1214
1242
  ? claimed.workflowSourceRef
1215
1243
  : bundle.state.workflowSource.kind === "builtin"
@@ -1218,7 +1246,7 @@ export default function piWorkflows(pi) {
1218
1246
  const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
1219
1247
  started = await startRun(ctx, sourceRef, claimed.input, {
1220
1248
  ...launchOptions,
1221
- resume: bundle !== null,
1249
+ resume: claimed.initialized,
1222
1250
  runId: claimed.runId,
1223
1251
  claimToken,
1224
1252
  });
@@ -1247,12 +1275,12 @@ export default function piWorkflows(pi) {
1247
1275
  ? { state: "running", runId: activeRun.runId }
1248
1276
  : workflowSchedulerResult(activeRun.lastState);
1249
1277
  }
1250
- const store = new WorkflowRunStore();
1251
- const bundle = await readRunBundle(store.runDirFor(request.runId));
1278
+ const store = workflowStore(controllerContext?.cwd ?? process.cwd());
1279
+ const bundle = store.readRun(request.runId);
1252
1280
  if (bundle !== null) {
1253
1281
  const recovered = bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
1254
1282
  if (recovered !== null) {
1255
- const lastTraceEvent = await readLastTraceEvent(recovered.runDir, recovered.manifest.paths.trace);
1283
+ const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
1256
1284
  return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
1257
1285
  }
1258
1286
  return { state: "pending" };
@@ -1260,7 +1288,6 @@ export default function piWorkflows(pi) {
1260
1288
  if (controllerContext === null) {
1261
1289
  return { state: "pending" };
1262
1290
  }
1263
- await store.quarantineIncompleteRun(request.runId);
1264
1291
  const runId = await startRun(controllerContext, request.workflow, request.input, {
1265
1292
  runId: request.runId,
1266
1293
  childKey,
@@ -1344,12 +1371,14 @@ export default function piWorkflows(pi) {
1344
1371
  const result = await listWorkflowControl(ctx);
1345
1372
  notify(ctx, result.message, result.level);
1346
1373
  };
1347
- const cancelHumanDecision = async (request) => {
1348
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1374
+ const cancelHumanDecision = async (ctx, request) => {
1375
+ const store = humanDecisionStore(ctx.cwd);
1349
1376
  await store.cancel(request, "cancelled");
1350
1377
  const cancellation = await store.readCancellation(request.decisionId);
1351
- if (cancellation !== null)
1378
+ if (cancellation !== null) {
1352
1379
  await settleHumanDecisionChannels(cancellation);
1380
+ store.markEffectApplied(request.decisionId, "decision.settle_presentations");
1381
+ }
1353
1382
  lastWaitingRunId = null;
1354
1383
  };
1355
1384
  const findOwnedWaitingHumanDecision = async (ctx) => {
@@ -1359,7 +1388,7 @@ export default function piWorkflows(pi) {
1359
1388
  .filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
1360
1389
  .map((row) => row.runId));
1361
1390
  const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
1362
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1391
+ const bundles = listWorkflowRuns();
1363
1392
  for (const bundle of bundles) {
1364
1393
  if (bundle.state.status !== "waiting" ||
1365
1394
  !owned.has(bundle.state.runId) ||
@@ -1414,7 +1443,7 @@ export default function piWorkflows(pi) {
1414
1443
  const { state } = widgetSource;
1415
1444
  const request = humanDecisionRequest(state.finalOutput);
1416
1445
  if (state.status === "waiting" && request !== null) {
1417
- await cancelHumanDecision(request);
1446
+ await cancelHumanDecision(ctx, request);
1418
1447
  clearWidgetTimer();
1419
1448
  clearWidget(ctx);
1420
1449
  return {
@@ -1434,7 +1463,7 @@ export default function piWorkflows(pi) {
1434
1463
  }
1435
1464
  const recovered = await findOwnedWaitingHumanDecision(ctx);
1436
1465
  if (recovered !== null) {
1437
- await cancelHumanDecision(recovered.request);
1466
+ await cancelHumanDecision(ctx, recovered.request);
1438
1467
  return {
1439
1468
  message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
1440
1469
  details: {
@@ -1532,9 +1561,13 @@ export default function piWorkflows(pi) {
1532
1561
  });
1533
1562
  const statusWorkflowControl = async (ctx, runId) => {
1534
1563
  if (runId !== undefined) {
1535
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1564
+ const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1565
+ if (launch !== undefined &&
1566
+ ["queued", "starting", "failed", "cancelled"].includes(launch.status)) {
1567
+ return workflowLaunchStatus(launch);
1568
+ }
1569
+ const bundle = readWorkflowRun(runId);
1536
1570
  if (bundle === null) {
1537
- const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1538
1571
  if (launch === undefined)
1539
1572
  throw new Error(`Workflow run not found: ${runId}`);
1540
1573
  return workflowLaunchStatus(launch);
@@ -1572,7 +1605,7 @@ export default function piWorkflows(pi) {
1572
1605
  .filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
1573
1606
  .map((row) => row.runId));
1574
1607
  const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
1575
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1608
+ const bundles = listWorkflowRuns();
1576
1609
  parentRunId =
1577
1610
  bundles.find((bundle) => bundle.state.status === "waiting" &&
1578
1611
  known.has(bundle.state.runId) &&
@@ -1588,7 +1621,7 @@ export default function piWorkflows(pi) {
1588
1621
  queueRecord.originSessionId !== ctx.sessionManager.getSessionId()) {
1589
1622
  throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
1590
1623
  }
1591
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(parentRunId));
1624
+ const parent = readWorkflowRun(parentRunId);
1592
1625
  if (parent === null ||
1593
1626
  parent.state.status !== "waiting" ||
1594
1627
  parent.state.workflowSource === undefined) {
@@ -1606,7 +1639,7 @@ export default function piWorkflows(pi) {
1606
1639
  };
1607
1640
  const answerWorkflowControl = async (ctx, input, requestedRunId, verified) => {
1608
1641
  const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
1609
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
1642
+ const parent = readWorkflowRun(waiting.parentRunId);
1610
1643
  if (parent === null)
1611
1644
  throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
1612
1645
  const request = humanDecisionRequest(parent.state.finalOutput);
@@ -1631,9 +1664,9 @@ export default function piWorkflows(pi) {
1631
1664
  },
1632
1665
  idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
1633
1666
  };
1634
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1667
+ const store = humanDecisionStore(ctx.cwd);
1635
1668
  const acceptance = await store.accept(request, submission);
1636
- if (acceptance.status === "conflict") {
1669
+ if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
1637
1670
  throw new Error("That human decision was already answered differently.");
1638
1671
  }
1639
1672
  accepted = acceptance.decision;
@@ -1657,19 +1690,9 @@ export default function piWorkflows(pi) {
1657
1690
  };
1658
1691
  }
1659
1692
  }
1660
- const decisionStore = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1661
- if (request !== null && continuationRunId !== undefined && accepted !== undefined) {
1662
- await decisionStore.recordContinuation(request.decisionId, {
1663
- schema: "pi-workflows.human-decision-continuation.v1",
1664
- decisionId: request.decisionId,
1665
- requestDigest: request.requestDigest,
1666
- parentRunId: waiting.parentRunId,
1667
- runId: continuationRunId,
1668
- createdAt: accepted.acceptedAt,
1669
- });
1670
- }
1693
+ const decisionStore = humanDecisionStore(ctx.cwd);
1671
1694
  if (continuationRunId !== undefined) {
1672
- const existingContinuation = await readRunBundle(new WorkflowRunStore().runDirFor(continuationRunId));
1695
+ const existingContinuation = readWorkflowRun(continuationRunId);
1673
1696
  if (existingContinuation !== null) {
1674
1697
  if (accepted !== undefined)
1675
1698
  await settleHumanDecisionChannels(accepted);
@@ -1694,7 +1717,18 @@ export default function piWorkflows(pi) {
1694
1717
  throw new Error("Could not start the checkpoint continuation.");
1695
1718
  }
1696
1719
  if (request !== null && accepted !== undefined) {
1720
+ await decisionStore.recordContinuation(request.decisionId, {
1721
+ schema: "pi-workflows.human-decision-continuation.v1",
1722
+ decisionId: request.decisionId,
1723
+ requestDigest: request.requestDigest,
1724
+ provenance: accepted.provenance,
1725
+ parentRunId: waiting.parentRunId,
1726
+ runId: continued,
1727
+ createdAt: accepted.acceptedAt,
1728
+ });
1729
+ decisionStore.markEffectApplied(request.decisionId, "decision.continue");
1697
1730
  await settleHumanDecisionChannels(accepted);
1731
+ decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1698
1732
  }
1699
1733
  lastWaitingRunId = null;
1700
1734
  return {
@@ -1712,7 +1746,7 @@ export default function piWorkflows(pi) {
1712
1746
  const channel = new PiDecisionChannel({
1713
1747
  actorId: ctx.sessionManager.getSessionId(),
1714
1748
  ui: ctx.ui,
1715
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
1749
+ store: humanDecisionStore(ctx.cwd),
1716
1750
  onAnswer: async (answer) => {
1717
1751
  const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
1718
1752
  notify(ctx, result.message, result.level);
@@ -1750,7 +1784,7 @@ export default function piWorkflows(pi) {
1750
1784
  config: loaded.channels,
1751
1785
  credentials: loaded.credentials,
1752
1786
  configDir: loaded.configDir,
1753
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
1787
+ store: humanDecisionStore(ctx.cwd),
1754
1788
  onAnswer: async (answer) => {
1755
1789
  await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
1756
1790
  },
@@ -1758,11 +1792,11 @@ export default function piWorkflows(pi) {
1758
1792
  await Promise.all([...telegramDecisionChannels.values()].map(async (channel) => channel.start()));
1759
1793
  };
1760
1794
  const recoverHumanDecisions = async (ctx, deliverPending = true) => {
1761
- const runStore = new WorkflowRunStore();
1762
- const store = new HumanDecisionStore(runStore.outputRoot);
1795
+ const runStore = workflowStore(ctx.cwd);
1796
+ const store = humanDecisionStore(ctx.cwd);
1763
1797
  const requests = (await store.listRequests()).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
1764
1798
  for (const request of requests) {
1765
- const parent = await readRunBundle(runStore.runDirFor(request.runId));
1799
+ const parent = runStore.readRun(request.runId);
1766
1800
  if (parent === null || parent.state.status !== "waiting")
1767
1801
  continue;
1768
1802
  const currentRequest = humanDecisionRequest(parent.state.finalOutput);
@@ -1775,60 +1809,110 @@ export default function piWorkflows(pi) {
1775
1809
  const ownedBySession = queueRecord !== undefined &&
1776
1810
  (queueRecord.originSessionId === null ||
1777
1811
  queueRecord.originSessionId === ctx.sessionManager.getSessionId());
1778
- const accepted = await store.readAccepted(request.decisionId);
1779
- if (accepted === null) {
1812
+ if (!ownedBySession)
1813
+ continue;
1814
+ const recoveryToken = randomUUID();
1815
+ const recoveryQueue = ensureRunQueueStore(ctx.cwd);
1816
+ const claimed = recoveryQueue.claimWorkflowRun({
1817
+ runId: request.runId,
1818
+ runnerId: ctx.sessionManager.getSessionId(),
1819
+ claimToken: recoveryToken,
1820
+ leaseMs: RUN_CLAIM_LEASE_MS,
1821
+ });
1822
+ if (claimed === undefined)
1823
+ continue;
1824
+ const ownerStore = humanDecisionStore(ctx.cwd, () => recoveryQueue.workflowRunAuthority(request.runId, recoveryToken));
1825
+ let resolved = await store.readResolved(request.decisionId);
1826
+ if (resolved === null) {
1780
1827
  let cancellation = await store.readCancellation(request.decisionId);
1781
- if (cancellation === null &&
1782
- request.expiresAt !== undefined &&
1783
- Date.parse(request.expiresAt) <= Date.now()) {
1784
- await store.cancel(request, "expired");
1785
- cancellation = await store.readCancellation(request.decisionId);
1828
+ const expired = request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
1829
+ if (cancellation === null && expired) {
1830
+ if (request.defaultResponse === undefined) {
1831
+ await ownerStore.cancel(request, "expired");
1832
+ cancellation = await ownerStore.readCancellation(request.decisionId);
1833
+ }
1834
+ else {
1835
+ try {
1836
+ resolved = (await ownerStore.resolveTimeout(request)).decision;
1837
+ }
1838
+ catch {
1839
+ cancellation = await store.readCancellation(request.decisionId);
1840
+ resolved = await store.readResolved(request.decisionId);
1841
+ }
1842
+ }
1786
1843
  }
1787
1844
  if (cancellation !== null) {
1845
+ recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1846
+ ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
1788
1847
  await settleHumanDecisionChannels(cancellation);
1848
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1849
+ ownerStore.close();
1789
1850
  continue;
1790
1851
  }
1791
- if (!deliverPending || !ownedBySession)
1852
+ if (resolved === null) {
1853
+ if (!deliverPending) {
1854
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1855
+ ownerStore.close();
1856
+ continue;
1857
+ }
1858
+ const channels = audienceChannels(decisionChannelConfig, request.audience);
1859
+ for (const channelId of channels) {
1860
+ const channel = telegramDecisionChannels.get(channelId);
1861
+ if (channel !== undefined)
1862
+ await channel.deliver(humanDecisionChannelRequest(request));
1863
+ }
1864
+ if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
1865
+ lastWaitingRunId = request.runId;
1866
+ queueMicrotask(() => void promptHumanDecision(ctx, request));
1867
+ }
1868
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1869
+ ownerStore.close();
1792
1870
  continue;
1793
- const channels = audienceChannels(decisionChannelConfig, request.audience);
1794
- for (const channelId of channels) {
1795
- const channel = telegramDecisionChannels.get(channelId);
1796
- if (channel !== undefined)
1797
- await channel.deliver(humanDecisionChannelRequest(request));
1798
1871
  }
1799
- if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
1800
- lastWaitingRunId = request.runId;
1801
- queueMicrotask(() => void promptHumanDecision(ctx, request));
1802
- }
1803
- continue;
1804
1872
  }
1805
- if (!ownedBySession)
1873
+ const currentParent = runStore.readRun(request.runId);
1874
+ if (currentParent === null || currentParent.state.status !== "waiting") {
1875
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1876
+ ownerStore.close();
1806
1877
  continue;
1878
+ }
1807
1879
  const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
1808
1880
  const continuation = (await store.readContinuation(request.decisionId)) ?? {
1809
1881
  schema: "pi-workflows.human-decision-continuation.v1",
1810
1882
  decisionId: request.decisionId,
1811
1883
  requestDigest: request.requestDigest,
1884
+ provenance: resolved.provenance,
1812
1885
  parentRunId: request.runId,
1813
1886
  runId,
1814
- createdAt: accepted.acceptedAt,
1887
+ createdAt: resolved.acceptedAt,
1815
1888
  };
1816
- await store.recordContinuation(request.decisionId, continuation);
1817
- const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
1889
+ recoveryQueue.parkWorkflowRun({
1890
+ runId: request.runId,
1891
+ claimToken: recoveryToken,
1892
+ });
1893
+ const existing = runStore.readRun(continuation.runId);
1818
1894
  if (existing === null && activeRun === null) {
1819
- if (parent.state.workflowSource === undefined)
1895
+ if (currentParent.state.workflowSource === undefined) {
1896
+ ownerStore.close();
1820
1897
  continue;
1821
- const workflowRef = parent.state.workflowSource.kind === "builtin"
1822
- ? `builtin:${parent.state.workflowSource.id}`
1823
- : parent.state.workflowSource.path;
1824
- await startRun(ctx, workflowRef, parent.state.input, {
1898
+ }
1899
+ const workflowRef = currentParent.state.workflowSource.kind === "builtin"
1900
+ ? `builtin:${currentParent.state.workflowSource.id}`
1901
+ : currentParent.state.workflowSource.path;
1902
+ await startRun(ctx, workflowRef, currentParent.state.input, {
1825
1903
  parentRunId: request.runId,
1826
- humanDecision: accepted,
1904
+ humanDecision: resolved,
1827
1905
  runId: continuation.runId,
1828
1906
  quiet: true,
1829
1907
  });
1830
1908
  }
1831
- await settleHumanDecisionChannels(accepted);
1909
+ if (runStore.readRun(continuation.runId) !== null) {
1910
+ await ownerStore.recordContinuation(request.decisionId, continuation);
1911
+ ownerStore.markEffectApplied(request.decisionId, "decision.continue");
1912
+ }
1913
+ await settleHumanDecisionChannels(resolved);
1914
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1915
+ ownerStore.close();
1832
1916
  if (activeRun !== null)
1833
1917
  break;
1834
1918
  }
@@ -1874,7 +1958,7 @@ export default function piWorkflows(pi) {
1874
1958
  }
1875
1959
  const queue = ensureRunQueueStore(ctx.cwd);
1876
1960
  const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
1877
- if (existing !== undefined) {
1961
+ if (existing !== undefined && existing.runId !== options.parentRunId) {
1878
1962
  throw new Error(`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`);
1879
1963
  }
1880
1964
  if (presentationPending !== null) {
@@ -1884,13 +1968,13 @@ export default function piWorkflows(pi) {
1884
1968
  const workflow = resolved.definition;
1885
1969
  const workflowSource = resolved.source;
1886
1970
  if (options.parentRunId !== undefined) {
1887
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
1971
+ const parent = readWorkflowRun(options.parentRunId);
1888
1972
  if (parent === null || parent.state.status !== "waiting") {
1889
1973
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
1890
1974
  }
1891
1975
  if (parent.state.workflowSource !== undefined &&
1892
1976
  !isDeepStrictEqual(parent.state.workflowSource, workflowSource)) {
1893
- throw new Error(`Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`);
1977
+ throw continuationSourceChangedError(options.parentRunId, parent.state.workflowSource, workflowSource);
1894
1978
  }
1895
1979
  }
1896
1980
  const snapshot = createDefinitionSnapshot(workflow);
@@ -1902,6 +1986,7 @@ export default function piWorkflows(pi) {
1902
1986
  workflowSourceRef: workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
1903
1987
  workflowSource: launchSourceIdentity(workflow, workflowSource),
1904
1988
  definitionDigest: definitionDigest(snapshot),
1989
+ definitionSnapshot: snapshot,
1905
1990
  input,
1906
1991
  launchOptions: preparedLaunchOptions(options),
1907
1992
  runnerId,
@@ -2090,7 +2175,7 @@ export default function piWorkflows(pi) {
2090
2175
  }
2091
2176
  catch (error) {
2092
2177
  const message = errorMessage(error);
2093
- notify(ctx, /workflow_run_queue_parent/.test(message)
2178
+ notify(ctx, /continuations\.parent_run_id/.test(message)
2094
2179
  ? "That checkpoint was already answered; see its continuation run."
2095
2180
  : `Could not continue workflow: ${message}`, "error");
2096
2181
  }
@@ -2237,9 +2322,10 @@ export default function piWorkflows(pi) {
2237
2322
  label: "Workflow",
2238
2323
  description: [
2239
2324
  "List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
2240
- "When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, everyMinutes, stopWhen, and optional maxChecks.",
2325
+ "When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, stopWhen, everyMinutes, and optional maxChecks.",
2326
+ "Put the exact goal, authority, limits, and recovery rules in task; Monitor observes first, performs only safe authorized actions, verifies them immediately, and waits only while target work is moving or an external event is pending.",
2241
2327
  "Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
2242
- "Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
2328
+ "Do not start repeated work without the user's request.",
2243
2329
  ].join(" "),
2244
2330
  parameters: WorkflowToolParameters,
2245
2331
  async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
@@ -2266,7 +2352,7 @@ export default function piWorkflows(pi) {
2266
2352
  break;
2267
2353
  case "answer": {
2268
2354
  const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2269
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2355
+ const parent = readWorkflowRun(waiting.parentRunId);
2270
2356
  if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2271
2357
  throw new Error("This checkpoint requires a verified human answer from Pi UI or a configured decision channel.");
2272
2358
  }
@@ -2294,7 +2380,7 @@ export default function piWorkflows(pi) {
2294
2380
  }
2295
2381
  throw new Error("No workflow step is waiting for output.");
2296
2382
  }
2297
- // Flush the conversation into the bundle before accepting, so the
2383
+ // Flush the conversation into SQLite before accepting, so the
2298
2384
  // attempt range includes the assistant message carrying this call.
2299
2385
  await activeRun.recorder?.record(ctx).catch(() => undefined);
2300
2386
  await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
@@ -2334,22 +2420,7 @@ export default function piWorkflows(pi) {
2334
2420
  controllerContext = ctx;
2335
2421
  if (herdrEnabled)
2336
2422
  void refreshHerdrCapability(ctx);
2337
- try {
2338
- const queue = ensureRunQueueStore(ctx.cwd);
2339
- const migration = await migrateLegacyWorkflowSources({
2340
- catalog: builtinWorkflowCatalog,
2341
- queue,
2342
- });
2343
- migrationBlockedRuns.clear();
2344
- for (const blocked of migration.blocked)
2345
- migrationBlockedRuns.add(blocked.runId);
2346
- if (migration.blocked.length > 0) {
2347
- notify(ctx, `Could not migrate ${migration.blocked.length} legacy workflow source(s).`, "warning");
2348
- }
2349
- }
2350
- catch (error) {
2351
- notify(ctx, `Could not migrate legacy workflow sources: ${errorMessage(error)}`, "warning");
2352
- }
2423
+ ensureRunQueueStore(ctx.cwd);
2353
2424
  try {
2354
2425
  await reloadDecisionChannels(ctx);
2355
2426
  }
@@ -2431,6 +2502,9 @@ export default function piWorkflows(pi) {
2431
2502
  });
2432
2503
  pi.on("turn_end", async (event, ctx) => {
2433
2504
  await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
2505
+ // turn_end observes the message after message_end replacement handlers,
2506
+ // so this is the exact assistant message that remains visible.
2507
+ activeRun?.executor.handleMessageEnd(event.message);
2434
2508
  });
2435
2509
  pi.on("message_start", async (event, ctx) => {
2436
2510
  await activeRun?.recorder?.handleMessageStart(event, ctx).catch(() => undefined);
@@ -2473,9 +2547,9 @@ export default function piWorkflows(pi) {
2473
2547
  return;
2474
2548
  }
2475
2549
  }
2476
- const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2477
2550
  const run = activeRun;
2478
2551
  if (!run) {
2552
+ turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2479
2553
  presentationPending = null;
2480
2554
  runSyncPass(ctx);
2481
2555
  return;
@@ -2483,8 +2557,16 @@ export default function piWorkflows(pi) {
2483
2557
  await run.recorder?.synchronize(ctx).catch(() => undefined);
2484
2558
  run.recorder?.settleAttempt();
2485
2559
  run.executor.setStreaming(false);
2486
- if (flushedNatural === 0)
2560
+ if (run.executor.pendingCompletion === "assistant") {
2561
+ // The visible response is the step output. Resolve it before any
2562
+ // unrelated deferred turn can start and replace the captured message.
2487
2563
  run.executor.handleAgentSettled();
2564
+ }
2565
+ else {
2566
+ const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2567
+ if (flushedNatural === 0)
2568
+ run.executor.handleAgentSettled();
2569
+ }
2488
2570
  runSyncPass(ctx);
2489
2571
  });
2490
2572
  pi.on("session_shutdown", async () => {