@osolmaz/pi-workflows 0.12.1 → 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 (209) hide show
  1. package/README.md +52 -28
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +369 -73
  4. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  5. package/dist/builtins/autoplan.workflow.js +200 -29
  6. package/dist/builtins/autoplan.workflow.js.map +1 -1
  7. package/dist/builtins/catalog.js +6 -4
  8. package/dist/builtins/catalog.js.map +1 -1
  9. package/dist/builtins/index.d.ts +2 -1
  10. package/dist/builtins/index.js +1 -0
  11. package/dist/builtins/index.js.map +1 -1
  12. package/dist/builtins/monitor.workflow.d.ts +56 -21
  13. package/dist/builtins/monitor.workflow.js +497 -216
  14. package/dist/builtins/monitor.workflow.js.map +1 -1
  15. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  16. package/dist/builtins/plain-summary.workflow.js +134 -0
  17. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  18. package/dist/builtins/plan-change.workflow.d.ts +170 -22
  19. package/dist/builtins/sanity-check.workflow.d.ts +45 -3
  20. package/dist/builtins/sanity-check.workflow.js +45 -7
  21. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  22. package/dist/controllers/effects.d.ts +3 -2
  23. package/dist/controllers/effects.js +8 -1
  24. package/dist/controllers/effects.js.map +1 -1
  25. package/dist/controllers/index.d.ts +1 -1
  26. package/dist/controllers/index.js +1 -1
  27. package/dist/controllers/index.js.map +1 -1
  28. package/dist/controllers/manager.d.ts +1 -0
  29. package/dist/controllers/manager.js +41 -17
  30. package/dist/controllers/manager.js.map +1 -1
  31. package/dist/controllers/sqlite.d.ts +107 -85
  32. package/dist/controllers/sqlite.js +1611 -1486
  33. package/dist/controllers/sqlite.js.map +1 -1
  34. package/dist/controllers/store.d.ts +9 -8
  35. package/dist/controllers/store.js +1 -31
  36. package/dist/controllers/store.js.map +1 -1
  37. package/dist/controllers/types.d.ts +3 -0
  38. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  39. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  40. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  41. package/dist/controllers/workflows.d.ts +2 -2
  42. package/dist/controllers/workflows.js +11 -10
  43. package/dist/controllers/workflows.js.map +1 -1
  44. package/dist/extension/controller-host.js +2 -2
  45. package/dist/extension/controller-host.js.map +1 -1
  46. package/dist/extension/decision-channels.d.ts +2 -0
  47. package/dist/extension/decision-channels.js +311 -118
  48. package/dist/extension/decision-channels.js.map +1 -1
  49. package/dist/extension/executor.d.ts +9 -2
  50. package/dist/extension/executor.js +89 -2
  51. package/dist/extension/executor.js.map +1 -1
  52. package/dist/extension/herdr-viewer.d.ts +0 -1
  53. package/dist/extension/herdr-viewer.js +0 -4
  54. package/dist/extension/herdr-viewer.js.map +1 -1
  55. package/dist/extension/index.js +156 -103
  56. package/dist/extension/index.js.map +1 -1
  57. package/dist/extension/recorder.d.ts +3 -10
  58. package/dist/extension/recorder.js +13 -22
  59. package/dist/extension/recorder.js.map +1 -1
  60. package/dist/extension/step-message.d.ts +7 -2
  61. package/dist/extension/step-message.js +94 -2
  62. package/dist/extension/step-message.js.map +1 -1
  63. package/dist/extension/widget.js +6 -0
  64. package/dist/extension/widget.js.map +1 -1
  65. package/dist/host/rpc-executor.d.ts +3 -0
  66. package/dist/host/rpc-executor.js +2 -0
  67. package/dist/host/rpc-executor.js.map +1 -1
  68. package/dist/host/runner.d.ts +3 -6
  69. package/dist/host/runner.js +83 -32
  70. package/dist/host/runner.js.map +1 -1
  71. package/dist/render/graph-render.js +14 -8
  72. package/dist/render/graph-render.js.map +1 -1
  73. package/dist/state/database.d.ts +35 -0
  74. package/dist/state/database.js +287 -0
  75. package/dist/state/database.js.map +1 -0
  76. package/dist/state/index.d.ts +4 -0
  77. package/dist/state/index.js +5 -0
  78. package/dist/state/index.js.map +1 -0
  79. package/dist/state/json.d.ts +6 -0
  80. package/dist/state/json.js +38 -0
  81. package/dist/state/json.js.map +1 -0
  82. package/dist/state/mutation.d.ts +57 -0
  83. package/dist/state/mutation.js +255 -0
  84. package/dist/state/mutation.js.map +1 -0
  85. package/dist/state/schema.d.ts +6 -0
  86. package/dist/state/schema.js +473 -0
  87. package/dist/state/schema.js.map +1 -0
  88. package/dist/viewer/cli.d.ts +2 -2
  89. package/dist/viewer/cli.js +142 -122
  90. package/dist/viewer/cli.js.map +1 -1
  91. package/dist/viewer/render.d.ts +4 -4
  92. package/dist/viewer/render.js +2 -17
  93. package/dist/viewer/render.js.map +1 -1
  94. package/dist/viewer/session-reducer.d.ts +1 -1
  95. package/dist/viewer/session-reducer.js +1 -1
  96. package/dist/viewer/tui.d.ts +3 -3
  97. package/dist/viewer/tui.js +15 -13
  98. package/dist/viewer/tui.js.map +1 -1
  99. package/dist/viewer/watch.d.ts +2 -5
  100. package/dist/viewer/watch.js +13 -13
  101. package/dist/viewer/watch.js.map +1 -1
  102. package/dist/workflows/composition.js +12 -2
  103. package/dist/workflows/composition.js.map +1 -1
  104. package/dist/workflows/definition.d.ts +6 -2
  105. package/dist/workflows/definition.js +17 -0
  106. package/dist/workflows/definition.js.map +1 -1
  107. package/dist/workflows/engine.d.ts +6 -6
  108. package/dist/workflows/engine.js +183 -103
  109. package/dist/workflows/engine.js.map +1 -1
  110. package/dist/workflows/errors.d.ts +3 -3
  111. package/dist/workflows/errors.js +3 -3
  112. package/dist/workflows/human-decision.d.ts +29 -4
  113. package/dist/workflows/human-decision.js +547 -283
  114. package/dist/workflows/human-decision.js.map +1 -1
  115. package/dist/workflows/index.d.ts +4 -5
  116. package/dist/workflows/index.js +3 -4
  117. package/dist/workflows/index.js.map +1 -1
  118. package/dist/workflows/schema.js +17 -1
  119. package/dist/workflows/schema.js.map +1 -1
  120. package/dist/workflows/store.d.ts +83 -103
  121. package/dist/workflows/store.js +902 -951
  122. package/dist/workflows/store.js.map +1 -1
  123. package/dist/workflows/types.d.ts +55 -81
  124. package/docs/CONTROLLERS.md +10 -8
  125. package/docs/DEFERRED_TURNS.md +2 -2
  126. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  127. package/docs/HUMAN_DECISIONS.md +13 -14
  128. package/docs/MONITOR.md +179 -231
  129. package/docs/SQLITE_STATE.md +219 -0
  130. package/docs/WORKFLOW_COMPOSITION.md +19 -19
  131. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  132. package/docs/WORKFLOW_UPDATES.md +10 -9
  133. package/docs/development.md +9 -9
  134. package/docs/live-replay-protocol.md +28 -37
  135. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  136. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  137. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  138. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
  139. package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
  140. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  141. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  142. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  143. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  144. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  145. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  146. package/docs/session-event-journal.md +63 -434
  147. package/docs/tui-viewer.md +13 -15
  148. package/docs/workflows.md +104 -50
  149. package/examples/workflows/plain-summary.workflow.ts +1 -0
  150. package/herdr-plugin.toml +1 -1
  151. package/package.json +1 -1
  152. package/plugins/herdr/viewer.mjs +1 -11
  153. package/skills/autoplan/SKILL.md +5 -4
  154. package/skills/monitor/SKILL.md +91 -163
  155. package/skills/pi-workflows/SKILL.md +7 -7
  156. package/skills/sanity-check/SKILL.md +1 -1
  157. package/src/builtins/autoplan.workflow.ts +290 -40
  158. package/src/builtins/catalog.ts +6 -4
  159. package/src/builtins/index.ts +7 -1
  160. package/src/builtins/monitor.workflow.ts +663 -249
  161. package/src/builtins/plain-summary.workflow.ts +185 -0
  162. package/src/builtins/sanity-check.workflow.ts +62 -7
  163. package/src/controllers/effects.ts +7 -0
  164. package/src/controllers/index.ts +0 -6
  165. package/src/controllers/manager.ts +72 -32
  166. package/src/controllers/sqlite.ts +2420 -1948
  167. package/src/controllers/store.ts +17 -38
  168. package/src/controllers/types.ts +3 -0
  169. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  170. package/src/controllers/workflows.ts +45 -21
  171. package/src/extension/controller-host.ts +1 -2
  172. package/src/extension/decision-channels.ts +427 -144
  173. package/src/extension/executor.ts +105 -2
  174. package/src/extension/herdr-viewer.ts +0 -5
  175. package/src/extension/index.ts +180 -118
  176. package/src/extension/recorder.ts +13 -22
  177. package/src/extension/step-message.ts +106 -4
  178. package/src/extension/widget.ts +8 -0
  179. package/src/host/rpc-executor.ts +4 -0
  180. package/src/host/runner.ts +93 -41
  181. package/src/render/graph-render.ts +11 -4
  182. package/src/state/database.ts +375 -0
  183. package/src/state/index.ts +29 -0
  184. package/src/state/json.ts +42 -0
  185. package/src/state/mutation.ts +435 -0
  186. package/src/state/schema.ts +475 -0
  187. package/src/viewer/cli.ts +151 -144
  188. package/src/viewer/render.ts +12 -24
  189. package/src/viewer/session-reducer.ts +1 -1
  190. package/src/viewer/tui.ts +18 -17
  191. package/src/viewer/watch.ts +13 -17
  192. package/src/workflows/composition.ts +12 -3
  193. package/src/workflows/definition.ts +36 -3
  194. package/src/workflows/engine.ts +212 -116
  195. package/src/workflows/errors.ts +3 -3
  196. package/src/workflows/human-decision.ts +795 -340
  197. package/src/workflows/index.ts +15 -19
  198. package/src/workflows/schema.ts +19 -1
  199. package/src/workflows/store.ts +1295 -1163
  200. package/src/workflows/types.ts +61 -81
  201. package/dist/workflows/artifacts.d.ts +0 -40
  202. package/dist/workflows/artifacts.js +0 -155
  203. package/dist/workflows/artifacts.js.map +0 -1
  204. package/dist/workflows/migrate-sources.d.ts +0 -42
  205. package/dist/workflows/migrate-sources.js +0 -133
  206. package/dist/workflows/migrate-sources.js.map +0 -1
  207. package/docs/run-bundles.md +0 -481
  208. package/src/workflows/artifacts.ts +0 -188
  209. 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;
@@ -569,7 +577,6 @@ export default function piWorkflows(pi) {
569
577
  workflowViewTarget = {
570
578
  runId: state.runId,
571
579
  workflowName: state.workflowName,
572
- runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
573
580
  };
574
581
  renderWidget(ctx);
575
582
  };
@@ -746,14 +753,16 @@ export default function piWorkflows(pi) {
746
753
  }
747
754
  try {
748
755
  if (outcome === "park") {
749
- 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
+ }
750
759
  }
751
- else {
752
- 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");
753
762
  }
754
763
  }
755
764
  catch {
756
- // Queue bookkeeping is best-effort next to the durable bundle.
765
+ // Queue bookkeeping is best-effort next to the durable run fact.
757
766
  }
758
767
  finally {
759
768
  run.claimToken = undefined;
@@ -764,7 +773,7 @@ export default function piWorkflows(pi) {
764
773
  activeRun = null;
765
774
  }
766
775
  if (result.state.status === "running") {
767
- // The run was parked for another runner; its bundle stays resumable
776
+ // The run was parked for another runner; its SQLite state stays resumable
768
777
  // and its queue row becomes claimable. The recorder drains first: its
769
778
  // finalization writes share the fenced store, so the claim must stay
770
779
  // valid until capture is complete. Nothing else to present.
@@ -788,7 +797,7 @@ export default function piWorkflows(pi) {
788
797
  notify(ctx, `Workflow ${run.workflowName} continues under another runner (run ${run.runId}).`);
789
798
  return;
790
799
  }
791
- releaseClaim(run, "done");
800
+ releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
792
801
  recordRunEvent({
793
802
  runId: run.runId,
794
803
  workflowRef: run.workflowName,
@@ -894,7 +903,7 @@ export default function piWorkflows(pi) {
894
903
  // refused continuation (edited source, missing parent) must not consume
895
904
  // the parent's one-continuation slot.
896
905
  if (options.parentRunId !== undefined) {
897
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
906
+ const parent = readWorkflowRun(options.parentRunId);
898
907
  if (parent === null || parent.state.status !== "waiting") {
899
908
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
900
909
  }
@@ -921,6 +930,7 @@ export default function piWorkflows(pi) {
921
930
  : workflowSource.path,
922
931
  workflowSource: launchSourceIdentity(workflow, workflowSource),
923
932
  definitionDigest: definitionDigest(snapshot),
933
+ definitionSnapshot: snapshot,
924
934
  input,
925
935
  launchOptions: preparedLaunchOptions(options),
926
936
  runnerId,
@@ -994,14 +1004,17 @@ export default function piWorkflows(pi) {
994
1004
  },
995
1005
  conversation: {
996
1006
  beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
1007
+ recoverAssistant: (contract) => recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
997
1008
  mark: () => run.recorder?.mark() ?? 0,
998
1009
  rangeSince: (mark) => run.recorder?.rangeSince(mark),
999
1010
  },
1000
1011
  });
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 });
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
+ });
1005
1018
  const engine = new WorkflowEngine({
1006
1019
  executor,
1007
1020
  store,
@@ -1026,8 +1039,8 @@ export default function piWorkflows(pi) {
1026
1039
  },
1027
1040
  // Awaited by the engine after run_started is persisted, so the session
1028
1041
  // 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);
1042
+ onRunStarted: async (runId) => {
1043
+ const recorder = new SessionRecorder(store, runId);
1031
1044
  try {
1032
1045
  await recorder.bind(ctx);
1033
1046
  run.recorder = recorder;
@@ -1119,31 +1132,30 @@ export default function piWorkflows(pi) {
1119
1132
  // once the cause is fixed, and say so in the feed.
1120
1133
  let resumeFailed = run.resume === true && !isClaimLostError(error);
1121
1134
  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.
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.
1126
1138
  if (resumeFailed) {
1127
1139
  try {
1128
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1140
+ const bundle = readWorkflowRun(runId);
1129
1141
  if (bundle === null || bundle.state.status !== "running") {
1130
1142
  resumeFailed = false;
1131
1143
  terminalStatus = bundle?.state.status;
1132
1144
  }
1133
1145
  }
1134
1146
  catch {
1135
- // Unreadable bundles close the row too.
1147
+ // Unreadable run state closes the row too.
1136
1148
  resumeFailed = false;
1137
1149
  }
1138
1150
  }
1139
- // A continuation that failed before its bundle exists frees the
1151
+ // A continuation that failed before its run row exists frees the
1140
1152
  // parent's continuation slot instead of consuming it forever.
1141
1153
  let continuationSlotFreed = false;
1142
1154
  if (run.parentRunId !== undefined &&
1143
1155
  run.claimToken !== undefined &&
1144
1156
  !isClaimLostError(error)) {
1145
1157
  try {
1146
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1158
+ const bundle = readWorkflowRun(runId);
1147
1159
  if (bundle === null) {
1148
1160
  continuationSlotFreed =
1149
1161
  runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
@@ -1213,7 +1225,7 @@ export default function piWorkflows(pi) {
1213
1225
  runnerId,
1214
1226
  claimToken,
1215
1227
  leaseMs: RUN_CLAIM_LEASE_MS,
1216
- excludeRunIds: [...migrationBlockedRuns],
1228
+ excludeRunIds: [],
1217
1229
  sessionId: ctx.sessionManager.getSessionId(),
1218
1230
  });
1219
1231
  if (claimed === undefined) {
@@ -1221,7 +1233,11 @@ export default function piWorkflows(pi) {
1221
1233
  }
1222
1234
  let started;
1223
1235
  try {
1224
- 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
+ }
1225
1241
  const sourceRef = bundle?.state.workflowSource === undefined
1226
1242
  ? claimed.workflowSourceRef
1227
1243
  : bundle.state.workflowSource.kind === "builtin"
@@ -1230,7 +1246,7 @@ export default function piWorkflows(pi) {
1230
1246
  const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
1231
1247
  started = await startRun(ctx, sourceRef, claimed.input, {
1232
1248
  ...launchOptions,
1233
- resume: bundle !== null,
1249
+ resume: claimed.initialized,
1234
1250
  runId: claimed.runId,
1235
1251
  claimToken,
1236
1252
  });
@@ -1259,12 +1275,12 @@ export default function piWorkflows(pi) {
1259
1275
  ? { state: "running", runId: activeRun.runId }
1260
1276
  : workflowSchedulerResult(activeRun.lastState);
1261
1277
  }
1262
- const store = new WorkflowRunStore();
1263
- const bundle = await readRunBundle(store.runDirFor(request.runId));
1278
+ const store = workflowStore(controllerContext?.cwd ?? process.cwd());
1279
+ const bundle = store.readRun(request.runId);
1264
1280
  if (bundle !== null) {
1265
1281
  const recovered = bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
1266
1282
  if (recovered !== null) {
1267
- const lastTraceEvent = await readLastTraceEvent(recovered.runDir, recovered.manifest.paths.trace);
1283
+ const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
1268
1284
  return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
1269
1285
  }
1270
1286
  return { state: "pending" };
@@ -1272,7 +1288,6 @@ export default function piWorkflows(pi) {
1272
1288
  if (controllerContext === null) {
1273
1289
  return { state: "pending" };
1274
1290
  }
1275
- await store.quarantineIncompleteRun(request.runId);
1276
1291
  const runId = await startRun(controllerContext, request.workflow, request.input, {
1277
1292
  runId: request.runId,
1278
1293
  childKey,
@@ -1356,12 +1371,14 @@ export default function piWorkflows(pi) {
1356
1371
  const result = await listWorkflowControl(ctx);
1357
1372
  notify(ctx, result.message, result.level);
1358
1373
  };
1359
- const cancelHumanDecision = async (request) => {
1360
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1374
+ const cancelHumanDecision = async (ctx, request) => {
1375
+ const store = humanDecisionStore(ctx.cwd);
1361
1376
  await store.cancel(request, "cancelled");
1362
1377
  const cancellation = await store.readCancellation(request.decisionId);
1363
- if (cancellation !== null)
1378
+ if (cancellation !== null) {
1364
1379
  await settleHumanDecisionChannels(cancellation);
1380
+ store.markEffectApplied(request.decisionId, "decision.settle_presentations");
1381
+ }
1365
1382
  lastWaitingRunId = null;
1366
1383
  };
1367
1384
  const findOwnedWaitingHumanDecision = async (ctx) => {
@@ -1371,7 +1388,7 @@ export default function piWorkflows(pi) {
1371
1388
  .filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
1372
1389
  .map((row) => row.runId));
1373
1390
  const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
1374
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1391
+ const bundles = listWorkflowRuns();
1375
1392
  for (const bundle of bundles) {
1376
1393
  if (bundle.state.status !== "waiting" ||
1377
1394
  !owned.has(bundle.state.runId) ||
@@ -1426,7 +1443,7 @@ export default function piWorkflows(pi) {
1426
1443
  const { state } = widgetSource;
1427
1444
  const request = humanDecisionRequest(state.finalOutput);
1428
1445
  if (state.status === "waiting" && request !== null) {
1429
- await cancelHumanDecision(request);
1446
+ await cancelHumanDecision(ctx, request);
1430
1447
  clearWidgetTimer();
1431
1448
  clearWidget(ctx);
1432
1449
  return {
@@ -1446,7 +1463,7 @@ export default function piWorkflows(pi) {
1446
1463
  }
1447
1464
  const recovered = await findOwnedWaitingHumanDecision(ctx);
1448
1465
  if (recovered !== null) {
1449
- await cancelHumanDecision(recovered.request);
1466
+ await cancelHumanDecision(ctx, recovered.request);
1450
1467
  return {
1451
1468
  message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
1452
1469
  details: {
@@ -1544,9 +1561,13 @@ export default function piWorkflows(pi) {
1544
1561
  });
1545
1562
  const statusWorkflowControl = async (ctx, runId) => {
1546
1563
  if (runId !== undefined) {
1547
- 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);
1548
1570
  if (bundle === null) {
1549
- const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1550
1571
  if (launch === undefined)
1551
1572
  throw new Error(`Workflow run not found: ${runId}`);
1552
1573
  return workflowLaunchStatus(launch);
@@ -1584,7 +1605,7 @@ export default function piWorkflows(pi) {
1584
1605
  .filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
1585
1606
  .map((row) => row.runId));
1586
1607
  const continued = new Set(rows.map((row) => row.parentRunId).filter((parent) => parent !== null));
1587
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1608
+ const bundles = listWorkflowRuns();
1588
1609
  parentRunId =
1589
1610
  bundles.find((bundle) => bundle.state.status === "waiting" &&
1590
1611
  known.has(bundle.state.runId) &&
@@ -1600,7 +1621,7 @@ export default function piWorkflows(pi) {
1600
1621
  queueRecord.originSessionId !== ctx.sessionManager.getSessionId()) {
1601
1622
  throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
1602
1623
  }
1603
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(parentRunId));
1624
+ const parent = readWorkflowRun(parentRunId);
1604
1625
  if (parent === null ||
1605
1626
  parent.state.status !== "waiting" ||
1606
1627
  parent.state.workflowSource === undefined) {
@@ -1618,7 +1639,7 @@ export default function piWorkflows(pi) {
1618
1639
  };
1619
1640
  const answerWorkflowControl = async (ctx, input, requestedRunId, verified) => {
1620
1641
  const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
1621
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
1642
+ const parent = readWorkflowRun(waiting.parentRunId);
1622
1643
  if (parent === null)
1623
1644
  throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
1624
1645
  const request = humanDecisionRequest(parent.state.finalOutput);
@@ -1643,7 +1664,7 @@ export default function piWorkflows(pi) {
1643
1664
  },
1644
1665
  idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
1645
1666
  };
1646
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1667
+ const store = humanDecisionStore(ctx.cwd);
1647
1668
  const acceptance = await store.accept(request, submission);
1648
1669
  if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
1649
1670
  throw new Error("That human decision was already answered differently.");
@@ -1669,20 +1690,9 @@ export default function piWorkflows(pi) {
1669
1690
  };
1670
1691
  }
1671
1692
  }
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
- }
1693
+ const decisionStore = humanDecisionStore(ctx.cwd);
1684
1694
  if (continuationRunId !== undefined) {
1685
- const existingContinuation = await readRunBundle(new WorkflowRunStore().runDirFor(continuationRunId));
1695
+ const existingContinuation = readWorkflowRun(continuationRunId);
1686
1696
  if (existingContinuation !== null) {
1687
1697
  if (accepted !== undefined)
1688
1698
  await settleHumanDecisionChannels(accepted);
@@ -1707,7 +1717,18 @@ export default function piWorkflows(pi) {
1707
1717
  throw new Error("Could not start the checkpoint continuation.");
1708
1718
  }
1709
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");
1710
1730
  await settleHumanDecisionChannels(accepted);
1731
+ decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1711
1732
  }
1712
1733
  lastWaitingRunId = null;
1713
1734
  return {
@@ -1725,7 +1746,7 @@ export default function piWorkflows(pi) {
1725
1746
  const channel = new PiDecisionChannel({
1726
1747
  actorId: ctx.sessionManager.getSessionId(),
1727
1748
  ui: ctx.ui,
1728
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
1749
+ store: humanDecisionStore(ctx.cwd),
1729
1750
  onAnswer: async (answer) => {
1730
1751
  const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
1731
1752
  notify(ctx, result.message, result.level);
@@ -1763,7 +1784,7 @@ export default function piWorkflows(pi) {
1763
1784
  config: loaded.channels,
1764
1785
  credentials: loaded.credentials,
1765
1786
  configDir: loaded.configDir,
1766
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
1787
+ store: humanDecisionStore(ctx.cwd),
1767
1788
  onAnswer: async (answer) => {
1768
1789
  await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
1769
1790
  },
@@ -1771,11 +1792,11 @@ export default function piWorkflows(pi) {
1771
1792
  await Promise.all([...telegramDecisionChannels.values()].map(async (channel) => channel.start()));
1772
1793
  };
1773
1794
  const recoverHumanDecisions = async (ctx, deliverPending = true) => {
1774
- const runStore = new WorkflowRunStore();
1775
- const store = new HumanDecisionStore(runStore.outputRoot);
1795
+ const runStore = workflowStore(ctx.cwd);
1796
+ const store = humanDecisionStore(ctx.cwd);
1776
1797
  const requests = (await store.listRequests()).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
1777
1798
  for (const request of requests) {
1778
- const parent = await readRunBundle(runStore.runDirFor(request.runId));
1799
+ const parent = runStore.readRun(request.runId);
1779
1800
  if (parent === null || parent.state.status !== "waiting")
1780
1801
  continue;
1781
1802
  const currentRequest = humanDecisionRequest(parent.state.finalOutput);
@@ -1788,18 +1809,31 @@ export default function piWorkflows(pi) {
1788
1809
  const ownedBySession = queueRecord !== undefined &&
1789
1810
  (queueRecord.originSessionId === null ||
1790
1811
  queueRecord.originSessionId === ctx.sessionManager.getSessionId());
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));
1791
1825
  let resolved = await store.readResolved(request.decisionId);
1792
1826
  if (resolved === null) {
1793
1827
  let cancellation = await store.readCancellation(request.decisionId);
1794
1828
  const expired = request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
1795
1829
  if (cancellation === null && expired) {
1796
1830
  if (request.defaultResponse === undefined) {
1797
- await store.cancel(request, "expired");
1798
- cancellation = await store.readCancellation(request.decisionId);
1831
+ await ownerStore.cancel(request, "expired");
1832
+ cancellation = await ownerStore.readCancellation(request.decisionId);
1799
1833
  }
1800
1834
  else {
1801
1835
  try {
1802
- resolved = (await store.resolveTimeout(request)).decision;
1836
+ resolved = (await ownerStore.resolveTimeout(request)).decision;
1803
1837
  }
1804
1838
  catch {
1805
1839
  cancellation = await store.readCancellation(request.decisionId);
@@ -1808,12 +1842,19 @@ export default function piWorkflows(pi) {
1808
1842
  }
1809
1843
  }
1810
1844
  if (cancellation !== null) {
1845
+ recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1846
+ ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
1811
1847
  await settleHumanDecisionChannels(cancellation);
1848
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1849
+ ownerStore.close();
1812
1850
  continue;
1813
1851
  }
1814
1852
  if (resolved === null) {
1815
- if (!deliverPending || !ownedBySession)
1853
+ if (!deliverPending) {
1854
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1855
+ ownerStore.close();
1816
1856
  continue;
1857
+ }
1817
1858
  const channels = audienceChannels(decisionChannelConfig, request.audience);
1818
1859
  for (const channelId of channels) {
1819
1860
  const channel = telegramDecisionChannels.get(channelId);
@@ -1824,14 +1865,17 @@ export default function piWorkflows(pi) {
1824
1865
  lastWaitingRunId = request.runId;
1825
1866
  queueMicrotask(() => void promptHumanDecision(ctx, request));
1826
1867
  }
1868
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
1869
+ ownerStore.close();
1827
1870
  continue;
1828
1871
  }
1829
1872
  }
1830
- if (!ownedBySession)
1831
- continue;
1832
- const currentParent = await readRunBundle(runStore.runDirFor(request.runId));
1833
- if (currentParent === null || currentParent.state.status !== "waiting")
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();
1834
1877
  continue;
1878
+ }
1835
1879
  const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
1836
1880
  const continuation = (await store.readContinuation(request.decisionId)) ?? {
1837
1881
  schema: "pi-workflows.human-decision-continuation.v1",
@@ -1842,11 +1886,16 @@ export default function piWorkflows(pi) {
1842
1886
  runId,
1843
1887
  createdAt: resolved.acceptedAt,
1844
1888
  };
1845
- await store.recordContinuation(request.decisionId, continuation);
1846
- 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);
1847
1894
  if (existing === null && activeRun === null) {
1848
- if (currentParent.state.workflowSource === undefined)
1895
+ if (currentParent.state.workflowSource === undefined) {
1896
+ ownerStore.close();
1849
1897
  continue;
1898
+ }
1850
1899
  const workflowRef = currentParent.state.workflowSource.kind === "builtin"
1851
1900
  ? `builtin:${currentParent.state.workflowSource.id}`
1852
1901
  : currentParent.state.workflowSource.path;
@@ -1857,7 +1906,13 @@ export default function piWorkflows(pi) {
1857
1906
  quiet: true,
1858
1907
  });
1859
1908
  }
1909
+ if (runStore.readRun(continuation.runId) !== null) {
1910
+ await ownerStore.recordContinuation(request.decisionId, continuation);
1911
+ ownerStore.markEffectApplied(request.decisionId, "decision.continue");
1912
+ }
1860
1913
  await settleHumanDecisionChannels(resolved);
1914
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
1915
+ ownerStore.close();
1861
1916
  if (activeRun !== null)
1862
1917
  break;
1863
1918
  }
@@ -1903,7 +1958,7 @@ export default function piWorkflows(pi) {
1903
1958
  }
1904
1959
  const queue = ensureRunQueueStore(ctx.cwd);
1905
1960
  const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
1906
- if (existing !== undefined) {
1961
+ if (existing !== undefined && existing.runId !== options.parentRunId) {
1907
1962
  throw new Error(`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`);
1908
1963
  }
1909
1964
  if (presentationPending !== null) {
@@ -1913,7 +1968,7 @@ export default function piWorkflows(pi) {
1913
1968
  const workflow = resolved.definition;
1914
1969
  const workflowSource = resolved.source;
1915
1970
  if (options.parentRunId !== undefined) {
1916
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
1971
+ const parent = readWorkflowRun(options.parentRunId);
1917
1972
  if (parent === null || parent.state.status !== "waiting") {
1918
1973
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
1919
1974
  }
@@ -1931,6 +1986,7 @@ export default function piWorkflows(pi) {
1931
1986
  workflowSourceRef: workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
1932
1987
  workflowSource: launchSourceIdentity(workflow, workflowSource),
1933
1988
  definitionDigest: definitionDigest(snapshot),
1989
+ definitionSnapshot: snapshot,
1934
1990
  input,
1935
1991
  launchOptions: preparedLaunchOptions(options),
1936
1992
  runnerId,
@@ -2119,7 +2175,7 @@ export default function piWorkflows(pi) {
2119
2175
  }
2120
2176
  catch (error) {
2121
2177
  const message = errorMessage(error);
2122
- notify(ctx, /workflow_run_queue_parent/.test(message)
2178
+ notify(ctx, /continuations\.parent_run_id/.test(message)
2123
2179
  ? "That checkpoint was already answered; see its continuation run."
2124
2180
  : `Could not continue workflow: ${message}`, "error");
2125
2181
  }
@@ -2266,9 +2322,10 @@ export default function piWorkflows(pi) {
2266
2322
  label: "Workflow",
2267
2323
  description: [
2268
2324
  "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.",
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.",
2270
2327
  "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.",
2328
+ "Do not start repeated work without the user's request.",
2272
2329
  ].join(" "),
2273
2330
  parameters: WorkflowToolParameters,
2274
2331
  async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
@@ -2295,7 +2352,7 @@ export default function piWorkflows(pi) {
2295
2352
  break;
2296
2353
  case "answer": {
2297
2354
  const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2298
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2355
+ const parent = readWorkflowRun(waiting.parentRunId);
2299
2356
  if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2300
2357
  throw new Error("This checkpoint requires a verified human answer from Pi UI or a configured decision channel.");
2301
2358
  }
@@ -2323,7 +2380,7 @@ export default function piWorkflows(pi) {
2323
2380
  }
2324
2381
  throw new Error("No workflow step is waiting for output.");
2325
2382
  }
2326
- // Flush the conversation into the bundle before accepting, so the
2383
+ // Flush the conversation into SQLite before accepting, so the
2327
2384
  // attempt range includes the assistant message carrying this call.
2328
2385
  await activeRun.recorder?.record(ctx).catch(() => undefined);
2329
2386
  await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
@@ -2363,22 +2420,7 @@ export default function piWorkflows(pi) {
2363
2420
  controllerContext = ctx;
2364
2421
  if (herdrEnabled)
2365
2422
  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
- }
2423
+ ensureRunQueueStore(ctx.cwd);
2382
2424
  try {
2383
2425
  await reloadDecisionChannels(ctx);
2384
2426
  }
@@ -2460,6 +2502,9 @@ export default function piWorkflows(pi) {
2460
2502
  });
2461
2503
  pi.on("turn_end", async (event, ctx) => {
2462
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);
2463
2508
  });
2464
2509
  pi.on("message_start", async (event, ctx) => {
2465
2510
  await activeRun?.recorder?.handleMessageStart(event, ctx).catch(() => undefined);
@@ -2502,9 +2547,9 @@ export default function piWorkflows(pi) {
2502
2547
  return;
2503
2548
  }
2504
2549
  }
2505
- const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2506
2550
  const run = activeRun;
2507
2551
  if (!run) {
2552
+ turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2508
2553
  presentationPending = null;
2509
2554
  runSyncPass(ctx);
2510
2555
  return;
@@ -2512,8 +2557,16 @@ export default function piWorkflows(pi) {
2512
2557
  await run.recorder?.synchronize(ctx).catch(() => undefined);
2513
2558
  run.recorder?.settleAttempt();
2514
2559
  run.executor.setStreaming(false);
2515
- 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.
2516
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
+ }
2517
2570
  runSyncPass(ctx);
2518
2571
  });
2519
2572
  pi.on("session_shutdown", async () => {