@osolmaz/pi-workflows 0.12.1 → 0.13.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 (233) hide show
  1. package/README.md +59 -33
  2. package/dist/builtins/autodoc.workflow.d.ts +194 -7
  3. package/dist/builtins/autodoc.workflow.js +156 -30
  4. package/dist/builtins/autodoc.workflow.js.map +1 -1
  5. package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
  6. package/dist/builtins/autoimplement-command-batches.js +29 -31
  7. package/dist/builtins/autoimplement-command-batches.js.map +1 -1
  8. package/dist/builtins/autoimplement.workflow.d.ts +1619 -97
  9. package/dist/builtins/autoimplement.workflow.js +416 -153
  10. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  11. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  12. package/dist/builtins/autoplan.workflow.js +200 -29
  13. package/dist/builtins/autoplan.workflow.js.map +1 -1
  14. package/dist/builtins/catalog.js +7 -5
  15. package/dist/builtins/catalog.js.map +1 -1
  16. package/dist/builtins/change-verification.workflow.d.ts +110 -0
  17. package/dist/builtins/change-verification.workflow.js +860 -0
  18. package/dist/builtins/change-verification.workflow.js.map +1 -0
  19. package/dist/builtins/index.d.ts +2 -1
  20. package/dist/builtins/index.js +1 -0
  21. package/dist/builtins/index.js.map +1 -1
  22. package/dist/builtins/monitor.workflow.d.ts +56 -21
  23. package/dist/builtins/monitor.workflow.js +499 -217
  24. package/dist/builtins/monitor.workflow.js.map +1 -1
  25. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  26. package/dist/builtins/plain-summary.workflow.js +134 -0
  27. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  28. package/dist/builtins/plan-change.workflow.d.ts +527 -26
  29. package/dist/builtins/plan-change.workflow.js +26 -0
  30. package/dist/builtins/plan-change.workflow.js.map +1 -1
  31. package/dist/builtins/sanity-check.workflow.d.ts +45 -3
  32. package/dist/builtins/sanity-check.workflow.js +45 -7
  33. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  34. package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
  35. package/dist/builtins/workspace-preparation.workflow.js +498 -0
  36. package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
  37. package/dist/controllers/effects.d.ts +3 -2
  38. package/dist/controllers/effects.js +8 -1
  39. package/dist/controllers/effects.js.map +1 -1
  40. package/dist/controllers/index.d.ts +1 -1
  41. package/dist/controllers/index.js +1 -1
  42. package/dist/controllers/index.js.map +1 -1
  43. package/dist/controllers/manager.d.ts +1 -0
  44. package/dist/controllers/manager.js +41 -17
  45. package/dist/controllers/manager.js.map +1 -1
  46. package/dist/controllers/sqlite.d.ts +107 -85
  47. package/dist/controllers/sqlite.js +1611 -1486
  48. package/dist/controllers/sqlite.js.map +1 -1
  49. package/dist/controllers/store.d.ts +9 -8
  50. package/dist/controllers/store.js +1 -31
  51. package/dist/controllers/store.js.map +1 -1
  52. package/dist/controllers/types.d.ts +3 -0
  53. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  54. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  55. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  56. package/dist/controllers/workflows.d.ts +2 -2
  57. package/dist/controllers/workflows.js +11 -10
  58. package/dist/controllers/workflows.js.map +1 -1
  59. package/dist/extension/controller-host.js +2 -2
  60. package/dist/extension/controller-host.js.map +1 -1
  61. package/dist/extension/decision-channels.d.ts +2 -0
  62. package/dist/extension/decision-channels.js +311 -118
  63. package/dist/extension/decision-channels.js.map +1 -1
  64. package/dist/extension/executor.d.ts +9 -2
  65. package/dist/extension/executor.js +90 -6
  66. package/dist/extension/executor.js.map +1 -1
  67. package/dist/extension/herdr-viewer.d.ts +0 -1
  68. package/dist/extension/herdr-viewer.js +0 -4
  69. package/dist/extension/herdr-viewer.js.map +1 -1
  70. package/dist/extension/index.js +156 -117
  71. package/dist/extension/index.js.map +1 -1
  72. package/dist/extension/recorder.d.ts +3 -10
  73. package/dist/extension/recorder.js +13 -22
  74. package/dist/extension/recorder.js.map +1 -1
  75. package/dist/extension/step-message.d.ts +7 -2
  76. package/dist/extension/step-message.js +94 -2
  77. package/dist/extension/step-message.js.map +1 -1
  78. package/dist/extension/widget.js +6 -0
  79. package/dist/extension/widget.js.map +1 -1
  80. package/dist/host/rpc-executor.d.ts +3 -0
  81. package/dist/host/rpc-executor.js +2 -0
  82. package/dist/host/rpc-executor.js.map +1 -1
  83. package/dist/host/runner.d.ts +3 -6
  84. package/dist/host/runner.js +83 -32
  85. package/dist/host/runner.js.map +1 -1
  86. package/dist/render/graph-render.js +14 -8
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.d.ts +35 -0
  89. package/dist/state/database.js +287 -0
  90. package/dist/state/database.js.map +1 -0
  91. package/dist/state/index.d.ts +4 -0
  92. package/dist/state/index.js +5 -0
  93. package/dist/state/index.js.map +1 -0
  94. package/dist/state/json.d.ts +6 -0
  95. package/dist/state/json.js +38 -0
  96. package/dist/state/json.js.map +1 -0
  97. package/dist/state/mutation.d.ts +57 -0
  98. package/dist/state/mutation.js +255 -0
  99. package/dist/state/mutation.js.map +1 -0
  100. package/dist/state/schema.d.ts +6 -0
  101. package/dist/state/schema.js +473 -0
  102. package/dist/state/schema.js.map +1 -0
  103. package/dist/viewer/cli.d.ts +2 -2
  104. package/dist/viewer/cli.js +142 -122
  105. package/dist/viewer/cli.js.map +1 -1
  106. package/dist/viewer/render.d.ts +4 -4
  107. package/dist/viewer/render.js +2 -17
  108. package/dist/viewer/render.js.map +1 -1
  109. package/dist/viewer/session-reducer.d.ts +1 -1
  110. package/dist/viewer/session-reducer.js +1 -1
  111. package/dist/viewer/tui.d.ts +3 -3
  112. package/dist/viewer/tui.js +15 -13
  113. package/dist/viewer/tui.js.map +1 -1
  114. package/dist/viewer/watch.d.ts +2 -5
  115. package/dist/viewer/watch.js +13 -13
  116. package/dist/viewer/watch.js.map +1 -1
  117. package/dist/workflows/composition.js +12 -2
  118. package/dist/workflows/composition.js.map +1 -1
  119. package/dist/workflows/definition.d.ts +6 -2
  120. package/dist/workflows/definition.js +17 -0
  121. package/dist/workflows/definition.js.map +1 -1
  122. package/dist/workflows/engine.d.ts +6 -6
  123. package/dist/workflows/engine.js +183 -103
  124. package/dist/workflows/engine.js.map +1 -1
  125. package/dist/workflows/errors.d.ts +3 -3
  126. package/dist/workflows/errors.js +3 -3
  127. package/dist/workflows/human-decision.d.ts +29 -4
  128. package/dist/workflows/human-decision.js +547 -283
  129. package/dist/workflows/human-decision.js.map +1 -1
  130. package/dist/workflows/index.d.ts +4 -5
  131. package/dist/workflows/index.js +3 -4
  132. package/dist/workflows/index.js.map +1 -1
  133. package/dist/workflows/schema.js +17 -1
  134. package/dist/workflows/schema.js.map +1 -1
  135. package/dist/workflows/store.d.ts +83 -103
  136. package/dist/workflows/store.js +902 -951
  137. package/dist/workflows/store.js.map +1 -1
  138. package/dist/workflows/types.d.ts +55 -81
  139. package/docs/CONTROLLERS.md +10 -8
  140. package/docs/DEFERRED_TURNS.md +2 -2
  141. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  142. package/docs/HUMAN_DECISIONS.md +13 -14
  143. package/docs/MONITOR.md +179 -231
  144. package/docs/SQLITE_STATE.md +219 -0
  145. package/docs/WORKFLOW_COMPOSITION.md +27 -19
  146. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  147. package/docs/WORKFLOW_UPDATES.md +10 -9
  148. package/docs/development.md +9 -9
  149. package/docs/live-replay-protocol.md +28 -37
  150. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  151. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  152. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  153. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
  154. package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
  155. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  156. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  157. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  158. package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
  159. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  160. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  161. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  162. package/docs/session-event-journal.md +63 -434
  163. package/docs/tui-viewer.md +13 -15
  164. package/docs/workflows.md +111 -62
  165. package/examples/workflows/plain-summary.workflow.ts +1 -0
  166. package/herdr-plugin.toml +1 -1
  167. package/package.json +1 -1
  168. package/plugins/herdr/viewer.mjs +1 -11
  169. package/skills/autodoc/SKILL.md +7 -0
  170. package/skills/autoimplement/SKILL.md +4 -0
  171. package/skills/autoplan/SKILL.md +5 -4
  172. package/skills/monitor/SKILL.md +91 -163
  173. package/skills/pi-workflows/SKILL.md +7 -7
  174. package/skills/sanity-check/SKILL.md +1 -1
  175. package/src/builtins/autodoc.workflow.ts +184 -33
  176. package/src/builtins/autoimplement-command-batches.ts +39 -33
  177. package/src/builtins/autoimplement.workflow.ts +483 -175
  178. package/src/builtins/autoplan.workflow.ts +290 -40
  179. package/src/builtins/catalog.ts +7 -5
  180. package/src/builtins/change-verification.workflow.ts +1143 -0
  181. package/src/builtins/index.ts +7 -1
  182. package/src/builtins/monitor.workflow.ts +667 -250
  183. package/src/builtins/plain-summary.workflow.ts +185 -0
  184. package/src/builtins/plan-change.workflow.ts +35 -0
  185. package/src/builtins/sanity-check.workflow.ts +62 -7
  186. package/src/builtins/workspace-preparation.workflow.ts +668 -0
  187. package/src/controllers/effects.ts +7 -0
  188. package/src/controllers/index.ts +0 -6
  189. package/src/controllers/manager.ts +72 -32
  190. package/src/controllers/sqlite.ts +2420 -1948
  191. package/src/controllers/store.ts +17 -38
  192. package/src/controllers/types.ts +3 -0
  193. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  194. package/src/controllers/workflows.ts +45 -21
  195. package/src/extension/controller-host.ts +1 -2
  196. package/src/extension/decision-channels.ts +427 -144
  197. package/src/extension/executor.ts +106 -6
  198. package/src/extension/herdr-viewer.ts +0 -5
  199. package/src/extension/index.ts +180 -132
  200. package/src/extension/recorder.ts +13 -22
  201. package/src/extension/step-message.ts +106 -4
  202. package/src/extension/widget.ts +8 -0
  203. package/src/host/rpc-executor.ts +4 -0
  204. package/src/host/runner.ts +93 -41
  205. package/src/render/graph-render.ts +11 -4
  206. package/src/state/database.ts +375 -0
  207. package/src/state/index.ts +29 -0
  208. package/src/state/json.ts +42 -0
  209. package/src/state/mutation.ts +435 -0
  210. package/src/state/schema.ts +475 -0
  211. package/src/viewer/cli.ts +151 -144
  212. package/src/viewer/render.ts +12 -24
  213. package/src/viewer/session-reducer.ts +1 -1
  214. package/src/viewer/tui.ts +18 -17
  215. package/src/viewer/watch.ts +13 -17
  216. package/src/workflows/composition.ts +12 -3
  217. package/src/workflows/definition.ts +36 -3
  218. package/src/workflows/engine.ts +212 -116
  219. package/src/workflows/errors.ts +3 -3
  220. package/src/workflows/human-decision.ts +795 -340
  221. package/src/workflows/index.ts +15 -19
  222. package/src/workflows/schema.ts +19 -1
  223. package/src/workflows/store.ts +1295 -1163
  224. package/src/workflows/types.ts +61 -81
  225. package/dist/workflows/artifacts.d.ts +0 -40
  226. package/dist/workflows/artifacts.js +0 -155
  227. package/dist/workflows/artifacts.js.map +0 -1
  228. package/dist/workflows/migrate-sources.d.ts +0 -42
  229. package/dist/workflows/migrate-sources.js +0 -133
  230. package/dist/workflows/migrate-sources.js.map +0 -1
  231. package/docs/run-bundles.md +0 -481
  232. package/src/workflows/artifacts.ts +0 -188
  233. 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
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";
@@ -101,7 +99,7 @@ function safeLaunchError(error) {
101
99
  .replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
102
100
  .replaceAll("\n", " ")
103
101
  .trim();
104
- 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)
105
103
  ? "workflow_not_found"
106
104
  : /source changed|source mismatch/iu.test(raw)
107
105
  ? "source_changed"
@@ -224,11 +222,21 @@ export default function piWorkflows(pi) {
224
222
  ...[...telegramDecisionChannels.values()].map(async (channel) => channel.settle(decision)),
225
223
  ]);
226
224
  };
227
- const migrationBlockedRuns = new Set();
228
225
  const ensureRunQueueStore = (cwd) => {
229
- runQueueStore ??= new SqliteControllerStore(projectControllerStorePath(cwd));
226
+ runQueueStore ??= new SqliteControllerStore(undefined, { projectPath: cwd });
230
227
  return runQueueStore;
231
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
+ };
232
240
  // Session-addressed delivery: each session polls only its durable outbox.
233
241
  // Run events remain an audit feed and never enter a conversation.
234
242
  let syncArmed = false;
@@ -321,7 +329,6 @@ export default function piWorkflows(pi) {
321
329
  };
322
330
  let activeRun = null;
323
331
  let systemTurnAbort = null;
324
- let suppressWorkflowAssistantTail = false;
325
332
  let lastExpiredAttempt = null;
326
333
  // The interactive run currently parked at a checkpoint, if any.
327
334
  let lastWaitingRunId = null;
@@ -569,7 +576,6 @@ export default function piWorkflows(pi) {
569
576
  workflowViewTarget = {
570
577
  runId: state.runId,
571
578
  workflowName: state.workflowName,
572
- runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
573
579
  };
574
580
  renderWidget(ctx);
575
581
  };
@@ -693,7 +699,6 @@ export default function piWorkflows(pi) {
693
699
  resolution: "presentation",
694
700
  send: (turnIntentId) => {
695
701
  presentationPending = run.generation;
696
- suppressWorkflowAssistantTail = false;
697
702
  pi.sendMessage({
698
703
  customType: PRESENTATION_MESSAGE_TYPE,
699
704
  content: buildPresentationMessage(instructions, state),
@@ -746,14 +751,16 @@ export default function piWorkflows(pi) {
746
751
  }
747
752
  try {
748
753
  if (outcome === "park") {
749
- runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
754
+ if (runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
755
+ runQueueStore.settleRunEffect(run.runId, "run.park_queue");
756
+ }
750
757
  }
751
- else {
752
- runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
758
+ else if (runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
759
+ runQueueStore.settleRunEffect(run.runId, "run.settle_queue");
753
760
  }
754
761
  }
755
762
  catch {
756
- // Queue bookkeeping is best-effort next to the durable bundle.
763
+ // Queue bookkeeping is best-effort next to the durable run fact.
757
764
  }
758
765
  finally {
759
766
  run.claimToken = undefined;
@@ -764,7 +771,7 @@ export default function piWorkflows(pi) {
764
771
  activeRun = null;
765
772
  }
766
773
  if (result.state.status === "running") {
767
- // The run was parked for another runner; its bundle stays resumable
774
+ // The run was parked for another runner; its SQLite state stays resumable
768
775
  // and its queue row becomes claimable. The recorder drains first: its
769
776
  // finalization writes share the fenced store, so the claim must stay
770
777
  // valid until capture is complete. Nothing else to present.
@@ -788,7 +795,7 @@ export default function piWorkflows(pi) {
788
795
  notify(ctx, `Workflow ${run.workflowName} continues under another runner (run ${run.runId}).`);
789
796
  return;
790
797
  }
791
- releaseClaim(run, "done");
798
+ releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
792
799
  recordRunEvent({
793
800
  runId: run.runId,
794
801
  workflowRef: run.workflowName,
@@ -894,7 +901,7 @@ export default function piWorkflows(pi) {
894
901
  // refused continuation (edited source, missing parent) must not consume
895
902
  // the parent's one-continuation slot.
896
903
  if (options.parentRunId !== undefined) {
897
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
904
+ const parent = readWorkflowRun(options.parentRunId);
898
905
  if (parent === null || parent.state.status !== "waiting") {
899
906
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
900
907
  }
@@ -921,6 +928,7 @@ export default function piWorkflows(pi) {
921
928
  : workflowSource.path,
922
929
  workflowSource: launchSourceIdentity(workflow, workflowSource),
923
930
  definitionDigest: definitionDigest(snapshot),
931
+ definitionSnapshot: snapshot,
924
932
  input,
925
933
  launchOptions: preparedLaunchOptions(options),
926
934
  runnerId,
@@ -994,14 +1002,17 @@ export default function piWorkflows(pi) {
994
1002
  },
995
1003
  conversation: {
996
1004
  beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
1005
+ recoverAssistant: (contract) => recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
997
1006
  mark: () => run.recorder?.mark() ?? 0,
998
1007
  rangeSince: (mark) => run.recorder?.rangeSince(mark),
999
1008
  },
1000
1009
  });
1001
- // The store is shared between the engine and the session recorder so the
1002
- // trace sequence stays single-writer (see docs/run-bundles.md). Queued
1003
- // runs are fenced: every write proves the claim is still ours first.
1004
- const store = new WorkflowRunStore(undefined, fence === undefined ? {} : { fenceProvider: () => fence });
1010
+ // The engine and session recorder share one transactional SQLite store.
1011
+ const store = workflowStore(ctx.cwd, queueStore === null || claimToken === undefined
1012
+ ? {}
1013
+ : {
1014
+ authorityProvider: () => queueStore.workflowRunAuthority(runId, claimToken),
1015
+ });
1005
1016
  const engine = new WorkflowEngine({
1006
1017
  executor,
1007
1018
  store,
@@ -1026,8 +1037,8 @@ export default function piWorkflows(pi) {
1026
1037
  },
1027
1038
  // Awaited by the engine after run_started is persisted, so the session
1028
1039
  // binding and its trace event always precede node and terminal events.
1029
- onRunStarted: async (runDir, state) => {
1030
- const recorder = new SessionRecorder(store, runDir, state.runId);
1040
+ onRunStarted: async (runId) => {
1041
+ const recorder = new SessionRecorder(store, runId);
1031
1042
  try {
1032
1043
  await recorder.bind(ctx);
1033
1044
  run.recorder = recorder;
@@ -1119,31 +1130,30 @@ export default function piWorkflows(pi) {
1119
1130
  // once the cause is fixed, and say so in the feed.
1120
1131
  let resumeFailed = run.resume === true && !isClaimLostError(error);
1121
1132
  let terminalStatus;
1122
- // Re-parking only makes sense while the bundle is still resumable.
1123
- // A terminal or waiting bundle closes the queue row instead, or
1124
- // every session start would retry it forever — and the feed reports
1125
- // the bundle's real state, not a bogus failure.
1133
+ // Re-parking only makes sense while the run is still resumable.
1134
+ // A terminal or waiting run closes the queue row instead, and the feed
1135
+ // reports the authoritative run state.
1126
1136
  if (resumeFailed) {
1127
1137
  try {
1128
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1138
+ const bundle = readWorkflowRun(runId);
1129
1139
  if (bundle === null || bundle.state.status !== "running") {
1130
1140
  resumeFailed = false;
1131
1141
  terminalStatus = bundle?.state.status;
1132
1142
  }
1133
1143
  }
1134
1144
  catch {
1135
- // Unreadable bundles close the row too.
1145
+ // Unreadable run state closes the row too.
1136
1146
  resumeFailed = false;
1137
1147
  }
1138
1148
  }
1139
- // A continuation that failed before its bundle exists frees the
1149
+ // A continuation that failed before its run row exists frees the
1140
1150
  // parent's continuation slot instead of consuming it forever.
1141
1151
  let continuationSlotFreed = false;
1142
1152
  if (run.parentRunId !== undefined &&
1143
1153
  run.claimToken !== undefined &&
1144
1154
  !isClaimLostError(error)) {
1145
1155
  try {
1146
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1156
+ const bundle = readWorkflowRun(runId);
1147
1157
  if (bundle === null) {
1148
1158
  continuationSlotFreed =
1149
1159
  runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
@@ -1213,7 +1223,7 @@ export default function piWorkflows(pi) {
1213
1223
  runnerId,
1214
1224
  claimToken,
1215
1225
  leaseMs: RUN_CLAIM_LEASE_MS,
1216
- excludeRunIds: [...migrationBlockedRuns],
1226
+ excludeRunIds: [],
1217
1227
  sessionId: ctx.sessionManager.getSessionId(),
1218
1228
  });
1219
1229
  if (claimed === undefined) {
@@ -1221,7 +1231,11 @@ export default function piWorkflows(pi) {
1221
1231
  }
1222
1232
  let started;
1223
1233
  try {
1224
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(claimed.runId));
1234
+ const bundle = readWorkflowRun(claimed.runId);
1235
+ if (bundle?.state.status === "waiting") {
1236
+ queueStore.parkWorkflowRun({ runId: claimed.runId, claimToken });
1237
+ return;
1238
+ }
1225
1239
  const sourceRef = bundle?.state.workflowSource === undefined
1226
1240
  ? claimed.workflowSourceRef
1227
1241
  : bundle.state.workflowSource.kind === "builtin"
@@ -1230,7 +1244,7 @@ export default function piWorkflows(pi) {
1230
1244
  const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
1231
1245
  started = await startRun(ctx, sourceRef, claimed.input, {
1232
1246
  ...launchOptions,
1233
- resume: bundle !== null,
1247
+ resume: claimed.initialized,
1234
1248
  runId: claimed.runId,
1235
1249
  claimToken,
1236
1250
  });
@@ -1259,12 +1273,12 @@ export default function piWorkflows(pi) {
1259
1273
  ? { state: "running", runId: activeRun.runId }
1260
1274
  : workflowSchedulerResult(activeRun.lastState);
1261
1275
  }
1262
- const store = new WorkflowRunStore();
1263
- const bundle = await readRunBundle(store.runDirFor(request.runId));
1276
+ const store = workflowStore(controllerContext?.cwd ?? process.cwd());
1277
+ const bundle = store.readRun(request.runId);
1264
1278
  if (bundle !== null) {
1265
1279
  const recovered = bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
1266
1280
  if (recovered !== null) {
1267
- const lastTraceEvent = await readLastTraceEvent(recovered.runDir, recovered.manifest.paths.trace);
1281
+ const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
1268
1282
  return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
1269
1283
  }
1270
1284
  return { state: "pending" };
@@ -1272,7 +1286,6 @@ export default function piWorkflows(pi) {
1272
1286
  if (controllerContext === null) {
1273
1287
  return { state: "pending" };
1274
1288
  }
1275
- await store.quarantineIncompleteRun(request.runId);
1276
1289
  const runId = await startRun(controllerContext, request.workflow, request.input, {
1277
1290
  runId: request.runId,
1278
1291
  childKey,
@@ -1356,12 +1369,14 @@ export default function piWorkflows(pi) {
1356
1369
  const result = await listWorkflowControl(ctx);
1357
1370
  notify(ctx, result.message, result.level);
1358
1371
  };
1359
- const cancelHumanDecision = async (request) => {
1360
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1372
+ const cancelHumanDecision = async (ctx, request) => {
1373
+ const store = humanDecisionStore(ctx.cwd);
1361
1374
  await store.cancel(request, "cancelled");
1362
1375
  const cancellation = await store.readCancellation(request.decisionId);
1363
- if (cancellation !== null)
1376
+ if (cancellation !== null) {
1364
1377
  await settleHumanDecisionChannels(cancellation);
1378
+ store.markEffectApplied(request.decisionId, "decision.settle_presentations");
1379
+ }
1365
1380
  lastWaitingRunId = null;
1366
1381
  };
1367
1382
  const findOwnedWaitingHumanDecision = async (ctx) => {
@@ -1371,7 +1386,7 @@ export default function piWorkflows(pi) {
1371
1386
  .filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
1372
1387
  .map((row) => row.runId));
1373
1388
  const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
1374
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1389
+ const bundles = listWorkflowRuns();
1375
1390
  for (const bundle of bundles) {
1376
1391
  if (bundle.state.status !== "waiting" ||
1377
1392
  !owned.has(bundle.state.runId) ||
@@ -1426,7 +1441,7 @@ export default function piWorkflows(pi) {
1426
1441
  const { state } = widgetSource;
1427
1442
  const request = humanDecisionRequest(state.finalOutput);
1428
1443
  if (state.status === "waiting" && request !== null) {
1429
- await cancelHumanDecision(request);
1444
+ await cancelHumanDecision(ctx, request);
1430
1445
  clearWidgetTimer();
1431
1446
  clearWidget(ctx);
1432
1447
  return {
@@ -1446,7 +1461,7 @@ export default function piWorkflows(pi) {
1446
1461
  }
1447
1462
  const recovered = await findOwnedWaitingHumanDecision(ctx);
1448
1463
  if (recovered !== null) {
1449
- await cancelHumanDecision(recovered.request);
1464
+ await cancelHumanDecision(ctx, recovered.request);
1450
1465
  return {
1451
1466
  message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
1452
1467
  details: {
@@ -1544,9 +1559,13 @@ export default function piWorkflows(pi) {
1544
1559
  });
1545
1560
  const statusWorkflowControl = async (ctx, runId) => {
1546
1561
  if (runId !== undefined) {
1547
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1562
+ const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1563
+ if (launch !== undefined &&
1564
+ ["queued", "starting", "failed", "cancelled"].includes(launch.status)) {
1565
+ return workflowLaunchStatus(launch);
1566
+ }
1567
+ const bundle = readWorkflowRun(runId);
1548
1568
  if (bundle === null) {
1549
- const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1550
1569
  if (launch === undefined)
1551
1570
  throw new Error(`Workflow run not found: ${runId}`);
1552
1571
  return workflowLaunchStatus(launch);
@@ -1584,7 +1603,7 @@ export default function piWorkflows(pi) {
1584
1603
  .filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
1585
1604
  .map((row) => row.runId));
1586
1605
  const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
1587
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1606
+ const bundles = listWorkflowRuns();
1588
1607
  parentRunId =
1589
1608
  bundles.find((bundle) => bundle.state.status === "waiting" &&
1590
1609
  known.has(bundle.state.runId) &&
@@ -1600,7 +1619,7 @@ export default function piWorkflows(pi) {
1600
1619
  queueRecord.originSessionId !== ctx.sessionManager.getSessionId()) {
1601
1620
  throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
1602
1621
  }
1603
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(parentRunId));
1622
+ const parent = readWorkflowRun(parentRunId);
1604
1623
  if (parent === null ||
1605
1624
  parent.state.status !== "waiting" ||
1606
1625
  parent.state.workflowSource === undefined) {
@@ -1618,7 +1637,7 @@ export default function piWorkflows(pi) {
1618
1637
  };
1619
1638
  const answerWorkflowControl = async (ctx, input, requestedRunId, verified) => {
1620
1639
  const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
1621
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
1640
+ const parent = readWorkflowRun(waiting.parentRunId);
1622
1641
  if (parent === null)
1623
1642
  throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
1624
1643
  const request = humanDecisionRequest(parent.state.finalOutput);
@@ -1643,7 +1662,7 @@ export default function piWorkflows(pi) {
1643
1662
  },
1644
1663
  idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
1645
1664
  };
1646
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1665
+ const store = humanDecisionStore(ctx.cwd);
1647
1666
  const acceptance = await store.accept(request, submission);
1648
1667
  if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
1649
1668
  throw new Error("That human decision was already answered differently.");
@@ -1669,20 +1688,9 @@ export default function piWorkflows(pi) {
1669
1688
  };
1670
1689
  }
1671
1690
  }
1672
- const decisionStore = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1673
- if (request !== null && continuationRunId !== undefined && accepted !== undefined) {
1674
- await decisionStore.recordContinuation(request.decisionId, {
1675
- schema: "pi-workflows.human-decision-continuation.v1",
1676
- decisionId: request.decisionId,
1677
- requestDigest: request.requestDigest,
1678
- provenance: accepted.provenance,
1679
- parentRunId: waiting.parentRunId,
1680
- runId: continuationRunId,
1681
- createdAt: accepted.acceptedAt,
1682
- });
1683
- }
1691
+ const decisionStore = humanDecisionStore(ctx.cwd);
1684
1692
  if (continuationRunId !== undefined) {
1685
- const existingContinuation = await readRunBundle(new WorkflowRunStore().runDirFor(continuationRunId));
1693
+ const existingContinuation = readWorkflowRun(continuationRunId);
1686
1694
  if (existingContinuation !== null) {
1687
1695
  if (accepted !== undefined)
1688
1696
  await settleHumanDecisionChannels(accepted);
@@ -1707,7 +1715,18 @@ export default function piWorkflows(pi) {
1707
1715
  throw new Error("Could not start the checkpoint continuation.");
1708
1716
  }
1709
1717
  if (request !== null && accepted !== undefined) {
1718
+ await decisionStore.recordContinuation(request.decisionId, {
1719
+ schema: "pi-workflows.human-decision-continuation.v1",
1720
+ decisionId: request.decisionId,
1721
+ requestDigest: request.requestDigest,
1722
+ provenance: accepted.provenance,
1723
+ parentRunId: waiting.parentRunId,
1724
+ runId: continued,
1725
+ createdAt: accepted.acceptedAt,
1726
+ });
1727
+ decisionStore.markEffectApplied(request.decisionId, "decision.continue");
1710
1728
  await settleHumanDecisionChannels(accepted);
1729
+ decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1711
1730
  }
1712
1731
  lastWaitingRunId = null;
1713
1732
  return {
@@ -1725,7 +1744,7 @@ export default function piWorkflows(pi) {
1725
1744
  const channel = new PiDecisionChannel({
1726
1745
  actorId: ctx.sessionManager.getSessionId(),
1727
1746
  ui: ctx.ui,
1728
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
1747
+ store: humanDecisionStore(ctx.cwd),
1729
1748
  onAnswer: async (answer) => {
1730
1749
  const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
1731
1750
  notify(ctx, result.message, result.level);
@@ -1763,7 +1782,7 @@ export default function piWorkflows(pi) {
1763
1782
  config: loaded.channels,
1764
1783
  credentials: loaded.credentials,
1765
1784
  configDir: loaded.configDir,
1766
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
1785
+ store: humanDecisionStore(ctx.cwd),
1767
1786
  onAnswer: async (answer) => {
1768
1787
  await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
1769
1788
  },
@@ -1771,11 +1790,11 @@ export default function piWorkflows(pi) {
1771
1790
  await Promise.all([...telegramDecisionChannels.values()].map(async (channel) => channel.start()));
1772
1791
  };
1773
1792
  const recoverHumanDecisions = async (ctx, deliverPending = true) => {
1774
- const runStore = new WorkflowRunStore();
1775
- const store = new HumanDecisionStore(runStore.outputRoot);
1793
+ const runStore = workflowStore(ctx.cwd);
1794
+ const store = humanDecisionStore(ctx.cwd);
1776
1795
  const requests = (await store.listRequests()).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
1777
1796
  for (const request of requests) {
1778
- const parent = await readRunBundle(runStore.runDirFor(request.runId));
1797
+ const parent = runStore.readRun(request.runId);
1779
1798
  if (parent === null || parent.state.status !== "waiting")
1780
1799
  continue;
1781
1800
  const currentRequest = humanDecisionRequest(parent.state.finalOutput);
@@ -1788,18 +1807,31 @@ export default function piWorkflows(pi) {
1788
1807
  const ownedBySession = queueRecord !== undefined &&
1789
1808
  (queueRecord.originSessionId === null ||
1790
1809
  queueRecord.originSessionId === ctx.sessionManager.getSessionId());
1810
+ if (!ownedBySession)
1811
+ continue;
1812
+ const recoveryToken = randomUUID();
1813
+ const recoveryQueue = ensureRunQueueStore(ctx.cwd);
1814
+ const claimed = recoveryQueue.claimWorkflowRun({
1815
+ runId: request.runId,
1816
+ runnerId: ctx.sessionManager.getSessionId(),
1817
+ claimToken: recoveryToken,
1818
+ leaseMs: RUN_CLAIM_LEASE_MS,
1819
+ });
1820
+ if (claimed === undefined)
1821
+ continue;
1822
+ const ownerStore = humanDecisionStore(ctx.cwd, () => recoveryQueue.workflowRunAuthority(request.runId, recoveryToken));
1791
1823
  let resolved = await store.readResolved(request.decisionId);
1792
1824
  if (resolved === null) {
1793
1825
  let cancellation = await store.readCancellation(request.decisionId);
1794
1826
  const expired = request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
1795
1827
  if (cancellation === null && expired) {
1796
1828
  if (request.defaultResponse === undefined) {
1797
- await store.cancel(request, "expired");
1798
- cancellation = await store.readCancellation(request.decisionId);
1829
+ await ownerStore.cancel(request, "expired");
1830
+ cancellation = await ownerStore.readCancellation(request.decisionId);
1799
1831
  }
1800
1832
  else {
1801
1833
  try {
1802
- resolved = (await store.resolveTimeout(request)).decision;
1834
+ resolved = (await ownerStore.resolveTimeout(request)).decision;
1803
1835
  }
1804
1836
  catch {
1805
1837
  cancellation = await store.readCancellation(request.decisionId);
@@ -1808,12 +1840,19 @@ export default function piWorkflows(pi) {
1808
1840
  }
1809
1841
  }
1810
1842
  if (cancellation !== null) {
1843
+ recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1844
+ ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
1811
1845
  await settleHumanDecisionChannels(cancellation);
1846
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1847
+ ownerStore.close();
1812
1848
  continue;
1813
1849
  }
1814
1850
  if (resolved === null) {
1815
- if (!deliverPending || !ownedBySession)
1851
+ if (!deliverPending) {
1852
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1853
+ ownerStore.close();
1816
1854
  continue;
1855
+ }
1817
1856
  const channels = audienceChannels(decisionChannelConfig, request.audience);
1818
1857
  for (const channelId of channels) {
1819
1858
  const channel = telegramDecisionChannels.get(channelId);
@@ -1824,14 +1863,17 @@ export default function piWorkflows(pi) {
1824
1863
  lastWaitingRunId = request.runId;
1825
1864
  queueMicrotask(() => void promptHumanDecision(ctx, request));
1826
1865
  }
1866
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1867
+ ownerStore.close();
1827
1868
  continue;
1828
1869
  }
1829
1870
  }
1830
- if (!ownedBySession)
1831
- continue;
1832
- const currentParent = await readRunBundle(runStore.runDirFor(request.runId));
1833
- if (currentParent === null || currentParent.state.status !== "waiting")
1871
+ const currentParent = runStore.readRun(request.runId);
1872
+ if (currentParent === null || currentParent.state.status !== "waiting") {
1873
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1874
+ ownerStore.close();
1834
1875
  continue;
1876
+ }
1835
1877
  const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
1836
1878
  const continuation = (await store.readContinuation(request.decisionId)) ?? {
1837
1879
  schema: "pi-workflows.human-decision-continuation.v1",
@@ -1842,11 +1884,16 @@ export default function piWorkflows(pi) {
1842
1884
  runId,
1843
1885
  createdAt: resolved.acceptedAt,
1844
1886
  };
1845
- await store.recordContinuation(request.decisionId, continuation);
1846
- const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
1887
+ recoveryQueue.parkWorkflowRun({
1888
+ runId: request.runId,
1889
+ claimToken: recoveryToken,
1890
+ });
1891
+ const existing = runStore.readRun(continuation.runId);
1847
1892
  if (existing === null && activeRun === null) {
1848
- if (currentParent.state.workflowSource === undefined)
1893
+ if (currentParent.state.workflowSource === undefined) {
1894
+ ownerStore.close();
1849
1895
  continue;
1896
+ }
1850
1897
  const workflowRef = currentParent.state.workflowSource.kind === "builtin"
1851
1898
  ? `builtin:${currentParent.state.workflowSource.id}`
1852
1899
  : currentParent.state.workflowSource.path;
@@ -1857,7 +1904,13 @@ export default function piWorkflows(pi) {
1857
1904
  quiet: true,
1858
1905
  });
1859
1906
  }
1907
+ if (runStore.readRun(continuation.runId) !== null) {
1908
+ await ownerStore.recordContinuation(request.decisionId, continuation);
1909
+ ownerStore.markEffectApplied(request.decisionId, "decision.continue");
1910
+ }
1860
1911
  await settleHumanDecisionChannels(resolved);
1912
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1913
+ ownerStore.close();
1861
1914
  if (activeRun !== null)
1862
1915
  break;
1863
1916
  }
@@ -1903,7 +1956,7 @@ export default function piWorkflows(pi) {
1903
1956
  }
1904
1957
  const queue = ensureRunQueueStore(ctx.cwd);
1905
1958
  const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
1906
- if (existing !== undefined) {
1959
+ if (existing !== undefined && existing.runId !== options.parentRunId) {
1907
1960
  throw new Error(`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`);
1908
1961
  }
1909
1962
  if (presentationPending !== null) {
@@ -1913,7 +1966,7 @@ export default function piWorkflows(pi) {
1913
1966
  const workflow = resolved.definition;
1914
1967
  const workflowSource = resolved.source;
1915
1968
  if (options.parentRunId !== undefined) {
1916
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
1969
+ const parent = readWorkflowRun(options.parentRunId);
1917
1970
  if (parent === null || parent.state.status !== "waiting") {
1918
1971
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
1919
1972
  }
@@ -1931,6 +1984,7 @@ export default function piWorkflows(pi) {
1931
1984
  workflowSourceRef: workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
1932
1985
  workflowSource: launchSourceIdentity(workflow, workflowSource),
1933
1986
  definitionDigest: definitionDigest(snapshot),
1987
+ definitionSnapshot: snapshot,
1934
1988
  input,
1935
1989
  launchOptions: preparedLaunchOptions(options),
1936
1990
  runnerId,
@@ -2119,7 +2173,7 @@ export default function piWorkflows(pi) {
2119
2173
  }
2120
2174
  catch (error) {
2121
2175
  const message = errorMessage(error);
2122
- notify(ctx, /workflow_run_queue_parent/.test(message)
2176
+ notify(ctx, /continuations\.parent_run_id/.test(message)
2123
2177
  ? "That checkpoint was already answered; see its continuation run."
2124
2178
  : `Could not continue workflow: ${message}`, "error");
2125
2179
  }
@@ -2266,9 +2320,10 @@ export default function piWorkflows(pi) {
2266
2320
  label: "Workflow",
2267
2321
  description: [
2268
2322
  "List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
2269
- "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.",
2323
+ "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.",
2324
+ "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.",
2270
2325
  "Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
2271
- "Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
2326
+ "Do not start repeated work without the user's request.",
2272
2327
  ].join(" "),
2273
2328
  parameters: WorkflowToolParameters,
2274
2329
  async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
@@ -2295,7 +2350,7 @@ export default function piWorkflows(pi) {
2295
2350
  break;
2296
2351
  case "answer": {
2297
2352
  const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2298
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2353
+ const parent = readWorkflowRun(waiting.parentRunId);
2299
2354
  if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2300
2355
  throw new Error("This checkpoint requires a verified human answer from Pi UI or a configured decision channel.");
2301
2356
  }
@@ -2323,7 +2378,7 @@ export default function piWorkflows(pi) {
2323
2378
  }
2324
2379
  throw new Error("No workflow step is waiting for output.");
2325
2380
  }
2326
- // Flush the conversation into the bundle before accepting, so the
2381
+ // Flush the conversation into SQLite before accepting, so the
2327
2382
  // attempt range includes the assistant message carrying this call.
2328
2383
  await activeRun.recorder?.record(ctx).catch(() => undefined);
2329
2384
  await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
@@ -2331,7 +2386,6 @@ export default function piWorkflows(pi) {
2331
2386
  if (!result.accepted) {
2332
2387
  throw new Error(result.message);
2333
2388
  }
2334
- suppressWorkflowAssistantTail = true;
2335
2389
  return {
2336
2390
  content: [{ type: "text", text: result.message }],
2337
2391
  details: { action: "submit", step: params.step, accepted: true },
@@ -2363,22 +2417,7 @@ export default function piWorkflows(pi) {
2363
2417
  controllerContext = ctx;
2364
2418
  if (herdrEnabled)
2365
2419
  void refreshHerdrCapability(ctx);
2366
- try {
2367
- const queue = ensureRunQueueStore(ctx.cwd);
2368
- const migration = await migrateLegacyWorkflowSources({
2369
- catalog: builtinWorkflowCatalog,
2370
- queue,
2371
- });
2372
- migrationBlockedRuns.clear();
2373
- for (const blocked of migration.blocked)
2374
- migrationBlockedRuns.add(blocked.runId);
2375
- if (migration.blocked.length > 0) {
2376
- notify(ctx, `Could not migrate ${migration.blocked.length} legacy workflow source(s).`, "warning");
2377
- }
2378
- }
2379
- catch (error) {
2380
- notify(ctx, `Could not migrate legacy workflow sources: ${errorMessage(error)}`, "warning");
2381
- }
2420
+ ensureRunQueueStore(ctx.cwd);
2382
2421
  try {
2383
2422
  await reloadDecisionChannels(ctx);
2384
2423
  }
@@ -2420,7 +2459,6 @@ export default function piWorkflows(pi) {
2420
2459
  }
2421
2460
  });
2422
2461
  pi.on("agent_start", () => {
2423
- suppressWorkflowAssistantTail = false;
2424
2462
  if (!activeRun && presentationPending === null && presentationAbort) {
2425
2463
  // A normal user turn started while an async presentation prompt was
2426
2464
  // still resolving. The user's new request supersedes that old result.
@@ -2430,7 +2468,6 @@ export default function piWorkflows(pi) {
2430
2468
  activeRun?.executor.setStreaming(true);
2431
2469
  });
2432
2470
  pi.on("agent_end", (event, ctx) => {
2433
- suppressWorkflowAssistantTail = false;
2434
2471
  const aborted = event.messages.some((message) => typeof message === "object" &&
2435
2472
  message !== null &&
2436
2473
  "stopReason" in message &&
@@ -2460,6 +2497,9 @@ export default function piWorkflows(pi) {
2460
2497
  });
2461
2498
  pi.on("turn_end", async (event, ctx) => {
2462
2499
  await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
2500
+ // turn_end observes the message after message_end replacement handlers,
2501
+ // so this is the exact assistant message that remains visible.
2502
+ activeRun?.executor.handleMessageEnd(event.message);
2463
2503
  });
2464
2504
  pi.on("message_start", async (event, ctx) => {
2465
2505
  await activeRun?.recorder?.handleMessageStart(event, ctx).catch(() => undefined);
@@ -2468,14 +2508,6 @@ export default function piWorkflows(pi) {
2468
2508
  activeRun?.recorder?.handleMessageUpdate(event);
2469
2509
  });
2470
2510
  pi.on("message_end", (event) => {
2471
- if (suppressWorkflowAssistantTail && event.message.role === "assistant") {
2472
- const message = {
2473
- ...event.message,
2474
- content: event.message.content.filter((part) => part.type !== "text"),
2475
- };
2476
- activeRun?.recorder?.handleMessageEnd({ ...event, message });
2477
- return { message };
2478
- }
2479
2511
  activeRun?.recorder?.handleMessageEnd(event);
2480
2512
  });
2481
2513
  pi.on("tool_execution_start", (event) => {
@@ -2502,9 +2534,9 @@ export default function piWorkflows(pi) {
2502
2534
  return;
2503
2535
  }
2504
2536
  }
2505
- const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2506
2537
  const run = activeRun;
2507
2538
  if (!run) {
2539
+ turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2508
2540
  presentationPending = null;
2509
2541
  runSyncPass(ctx);
2510
2542
  return;
@@ -2512,15 +2544,22 @@ export default function piWorkflows(pi) {
2512
2544
  await run.recorder?.synchronize(ctx).catch(() => undefined);
2513
2545
  run.recorder?.settleAttempt();
2514
2546
  run.executor.setStreaming(false);
2515
- if (flushedNatural === 0)
2547
+ if (run.executor.pendingCompletion === "assistant") {
2548
+ // The visible response is the step output. Resolve it before any
2549
+ // unrelated deferred turn can start and replace the captured message.
2516
2550
  run.executor.handleAgentSettled();
2551
+ }
2552
+ else {
2553
+ const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2554
+ if (flushedNatural === 0)
2555
+ run.executor.handleAgentSettled();
2556
+ }
2517
2557
  runSyncPass(ctx);
2518
2558
  });
2519
2559
  pi.on("session_shutdown", async () => {
2520
2560
  sessionClosed = true;
2521
2561
  herdrProbeGeneration += 1;
2522
2562
  systemTurnAbort = null;
2523
- suppressWorkflowAssistantTail = false;
2524
2563
  turnCoordinator.clearDeferred();
2525
2564
  supersedePresentation();
2526
2565
  const run = activeRun;