@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,13 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import path from "node:path";
3
2
  import { isDeepStrictEqual } from "node:util";
4
3
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
5
4
  import { builtinWorkflowCatalog } from "../builtins/catalog.js";
6
- import {
7
- projectControllerStorePath,
8
- SqliteControllerStore,
9
- type WorkflowRunQueueRecord,
10
- } from "../controllers/index.js";
5
+ import { SqliteControllerStore, type WorkflowRunQueueRecord } from "../controllers/index.js";
11
6
  import type {
12
7
  WorkflowTurnIntentCause,
13
8
  WorkflowTurnIntentResolution,
@@ -30,13 +25,11 @@ import {
30
25
  validateHumanDecisionResponse,
31
26
  } from "../workflows/human-decision.js";
32
27
  import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
33
- import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
34
28
  import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/progress.js";
35
29
  import {
36
30
  createRunId,
37
- listRunBundles,
38
- readLastTraceEvent,
39
- readRunBundle,
31
+ listWorkflowRuns,
32
+ readWorkflowRun,
40
33
  WorkflowRunStore,
41
34
  createDefinitionSnapshot,
42
35
  } from "../workflows/store.js";
@@ -96,6 +89,7 @@ import {
96
89
  } from "./herdr-viewer.js";
97
90
  import { SessionRecorder } from "./recorder.js";
98
91
  import {
92
+ recoverAssistantStep,
99
93
  registerWorkflowAgentStepMessageRenderer,
100
94
  WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
101
95
  WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
@@ -279,7 +273,7 @@ function safeLaunchError(error: unknown): { code: string; message: string } {
279
273
  .replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
280
274
  .replaceAll("\n", " ")
281
275
  .trim();
282
- const code = /not found|cannot find|unknown workflow/iu.test(raw)
276
+ const code = /not found|cannot find|unknown workflow|no such file or directory/iu.test(raw)
283
277
  ? "workflow_not_found"
284
278
  : /source changed|source mismatch/iu.test(raw)
285
279
  ? "source_changed"
@@ -436,11 +430,29 @@ export default function piWorkflows(pi: ExtensionAPI) {
436
430
  ...[...telegramDecisionChannels.values()].map(async (channel) => channel.settle(decision)),
437
431
  ]);
438
432
  };
439
- const migrationBlockedRuns = new Set<string>();
440
433
  const ensureRunQueueStore = (cwd: string): SqliteControllerStore => {
441
- runQueueStore ??= new SqliteControllerStore(projectControllerStorePath(cwd));
434
+ runQueueStore ??= new SqliteControllerStore(undefined, { projectPath: cwd });
442
435
  return runQueueStore;
443
436
  };
437
+ const workflowStore = (
438
+ cwd: string,
439
+ options: Omit<ConstructorParameters<typeof WorkflowRunStore>[1], "state"> = {},
440
+ ): WorkflowRunStore => {
441
+ const queue = ensureRunQueueStore(cwd);
442
+ return new WorkflowRunStore(queue.filePath, { ...options, state: queue.state });
443
+ };
444
+ const humanDecisionStore = (
445
+ cwd: string,
446
+ authorityProvider?: NonNullable<
447
+ ConstructorParameters<typeof HumanDecisionStore>[1]
448
+ >["authorityProvider"],
449
+ ): HumanDecisionStore => {
450
+ const queue = ensureRunQueueStore(cwd);
451
+ return new HumanDecisionStore(queue.filePath, {
452
+ state: queue.state,
453
+ ...(authorityProvider === undefined ? {} : { authorityProvider }),
454
+ });
455
+ };
444
456
 
445
457
  // Session-addressed delivery: each session polls only its durable outbox.
446
458
  // Run events remain an audit feed and never enter a conversation.
@@ -548,7 +560,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
548
560
  };
549
561
  let activeRun: ActiveRun | null = null;
550
562
  let systemTurnAbort: { contract: AgentStepContract; intentId?: string } | null = null;
551
- let suppressWorkflowAssistantTail = false;
552
563
  let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
553
564
  // The interactive run currently parked at a checkpoint, if any.
554
565
  let lastWaitingRunId: string | null = null;
@@ -838,7 +849,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
838
849
  workflowViewTarget = {
839
850
  runId: state.runId,
840
851
  workflowName: state.workflowName,
841
- runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
842
852
  };
843
853
  renderWidget(ctx);
844
854
  };
@@ -988,7 +998,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
988
998
  resolution: "presentation",
989
999
  send: (turnIntentId) => {
990
1000
  presentationPending = run.generation;
991
- suppressWorkflowAssistantTail = false;
992
1001
  pi.sendMessage(
993
1002
  {
994
1003
  customType: PRESENTATION_MESSAGE_TYPE,
@@ -1048,12 +1057,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
1048
1057
  }
1049
1058
  try {
1050
1059
  if (outcome === "park") {
1051
- runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
1052
- } else {
1053
- runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
1060
+ if (runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
1061
+ runQueueStore.settleRunEffect(run.runId, "run.park_queue");
1062
+ }
1063
+ } else if (
1064
+ runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken })
1065
+ ) {
1066
+ runQueueStore.settleRunEffect(run.runId, "run.settle_queue");
1054
1067
  }
1055
1068
  } catch {
1056
- // Queue bookkeeping is best-effort next to the durable bundle.
1069
+ // Queue bookkeeping is best-effort next to the durable run fact.
1057
1070
  } finally {
1058
1071
  run.claimToken = undefined;
1059
1072
  }
@@ -1068,7 +1081,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1068
1081
  activeRun = null;
1069
1082
  }
1070
1083
  if (result.state.status === "running") {
1071
- // The run was parked for another runner; its bundle stays resumable
1084
+ // The run was parked for another runner; its SQLite state stays resumable
1072
1085
  // and its queue row becomes claimable. The recorder drains first: its
1073
1086
  // finalization writes share the fenced store, so the claim must stay
1074
1087
  // valid until capture is complete. Nothing else to present.
@@ -1095,7 +1108,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1095
1108
  );
1096
1109
  return;
1097
1110
  }
1098
- releaseClaim(run, "done");
1111
+ releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
1099
1112
  recordRunEvent({
1100
1113
  runId: run.runId,
1101
1114
  workflowRef: run.workflowName,
@@ -1223,7 +1236,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1223
1236
  // refused continuation (edited source, missing parent) must not consume
1224
1237
  // the parent's one-continuation slot.
1225
1238
  if (options.parentRunId !== undefined) {
1226
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
1239
+ const parent = readWorkflowRun(options.parentRunId);
1227
1240
  if (parent === null || parent.state.status !== "waiting") {
1228
1241
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
1229
1242
  }
@@ -1258,6 +1271,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1258
1271
  : workflowSource.path,
1259
1272
  workflowSource: launchSourceIdentity(workflow, workflowSource),
1260
1273
  definitionDigest: definitionDigest(snapshot),
1274
+ definitionSnapshot: snapshot,
1261
1275
  input,
1262
1276
  launchOptions: preparedLaunchOptions(options),
1263
1277
  runnerId,
@@ -1339,16 +1353,20 @@ export default function piWorkflows(pi: ExtensionAPI) {
1339
1353
  },
1340
1354
  conversation: {
1341
1355
  beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
1356
+ recoverAssistant: (contract) =>
1357
+ recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
1342
1358
  mark: () => run.recorder?.mark() ?? 0,
1343
1359
  rangeSince: (mark) => run.recorder?.rangeSince(mark),
1344
1360
  },
1345
1361
  });
1346
- // The store is shared between the engine and the session recorder so the
1347
- // trace sequence stays single-writer (see docs/run-bundles.md). Queued
1348
- // runs are fenced: every write proves the claim is still ours first.
1349
- const store = new WorkflowRunStore(
1350
- undefined,
1351
- fence === undefined ? {} : { fenceProvider: () => fence },
1362
+ // The engine and session recorder share one transactional SQLite store.
1363
+ const store = workflowStore(
1364
+ ctx.cwd,
1365
+ queueStore === null || claimToken === undefined
1366
+ ? {}
1367
+ : {
1368
+ authorityProvider: () => queueStore.workflowRunAuthority(runId, claimToken),
1369
+ },
1352
1370
  );
1353
1371
  const engine = new WorkflowEngine({
1354
1372
  executor,
@@ -1373,8 +1391,8 @@ export default function piWorkflows(pi: ExtensionAPI) {
1373
1391
  },
1374
1392
  // Awaited by the engine after run_started is persisted, so the session
1375
1393
  // binding and its trace event always precede node and terminal events.
1376
- onRunStarted: async (runDir, state) => {
1377
- const recorder = new SessionRecorder(store, runDir, state.runId);
1394
+ onRunStarted: async (runId) => {
1395
+ const recorder = new SessionRecorder(store, runId);
1378
1396
  try {
1379
1397
  await recorder.bind(ctx);
1380
1398
  run.recorder = recorder;
@@ -1474,23 +1492,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
1474
1492
  // once the cause is fixed, and say so in the feed.
1475
1493
  let resumeFailed = run.resume === true && !isClaimLostError(error);
1476
1494
  let terminalStatus: string | undefined;
1477
- // Re-parking only makes sense while the bundle is still resumable.
1478
- // A terminal or waiting bundle closes the queue row instead, or
1479
- // every session start would retry it forever — and the feed reports
1480
- // the bundle's real state, not a bogus failure.
1495
+ // Re-parking only makes sense while the run is still resumable.
1496
+ // A terminal or waiting run closes the queue row instead, and the feed
1497
+ // reports the authoritative run state.
1481
1498
  if (resumeFailed) {
1482
1499
  try {
1483
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1500
+ const bundle = readWorkflowRun(runId);
1484
1501
  if (bundle === null || bundle.state.status !== "running") {
1485
1502
  resumeFailed = false;
1486
1503
  terminalStatus = bundle?.state.status;
1487
1504
  }
1488
1505
  } catch {
1489
- // Unreadable bundles close the row too.
1506
+ // Unreadable run state closes the row too.
1490
1507
  resumeFailed = false;
1491
1508
  }
1492
1509
  }
1493
- // A continuation that failed before its bundle exists frees the
1510
+ // A continuation that failed before its run row exists frees the
1494
1511
  // parent's continuation slot instead of consuming it forever.
1495
1512
  let continuationSlotFreed = false;
1496
1513
  if (
@@ -1499,7 +1516,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1499
1516
  !isClaimLostError(error)
1500
1517
  ) {
1501
1518
  try {
1502
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1519
+ const bundle = readWorkflowRun(runId);
1503
1520
  if (bundle === null) {
1504
1521
  continuationSlotFreed =
1505
1522
  runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
@@ -1577,7 +1594,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1577
1594
  runnerId,
1578
1595
  claimToken,
1579
1596
  leaseMs: RUN_CLAIM_LEASE_MS,
1580
- excludeRunIds: [...migrationBlockedRuns],
1597
+ excludeRunIds: [],
1581
1598
  sessionId: ctx.sessionManager.getSessionId(),
1582
1599
  });
1583
1600
  if (claimed === undefined) {
@@ -1585,7 +1602,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
1585
1602
  }
1586
1603
  let started: string | undefined;
1587
1604
  try {
1588
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(claimed.runId));
1605
+ const bundle = readWorkflowRun(claimed.runId);
1606
+ if (bundle?.state.status === "waiting") {
1607
+ queueStore.parkWorkflowRun({ runId: claimed.runId, claimToken });
1608
+ return;
1609
+ }
1589
1610
  const sourceRef =
1590
1611
  bundle?.state.workflowSource === undefined
1591
1612
  ? claimed.workflowSourceRef
@@ -1595,7 +1616,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1595
1616
  const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
1596
1617
  started = await startRun(ctx, sourceRef, claimed.input, {
1597
1618
  ...launchOptions,
1598
- resume: bundle !== null,
1619
+ resume: claimed.initialized,
1599
1620
  runId: claimed.runId,
1600
1621
  claimToken,
1601
1622
  });
@@ -1623,16 +1644,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
1623
1644
  ? { state: "running", runId: activeRun.runId }
1624
1645
  : workflowSchedulerResult(activeRun.lastState);
1625
1646
  }
1626
- const store = new WorkflowRunStore();
1627
- const bundle = await readRunBundle(store.runDirFor(request.runId));
1647
+ const store = workflowStore(controllerContext?.cwd ?? process.cwd());
1648
+ const bundle = store.readRun(request.runId);
1628
1649
  if (bundle !== null) {
1629
1650
  const recovered =
1630
1651
  bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
1631
1652
  if (recovered !== null) {
1632
- const lastTraceEvent = await readLastTraceEvent(
1633
- recovered.runDir,
1634
- recovered.manifest.paths.trace,
1635
- );
1653
+ const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
1636
1654
  return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
1637
1655
  }
1638
1656
  return { state: "pending" };
@@ -1640,7 +1658,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
1640
1658
  if (controllerContext === null) {
1641
1659
  return { state: "pending" };
1642
1660
  }
1643
- await store.quarantineIncompleteRun(request.runId);
1644
1661
  const runId = await startRun(controllerContext, request.workflow, request.input, {
1645
1662
  runId: request.runId,
1646
1663
  childKey,
@@ -1746,11 +1763,17 @@ export default function piWorkflows(pi: ExtensionAPI) {
1746
1763
  notify(ctx, result.message, result.level);
1747
1764
  };
1748
1765
 
1749
- const cancelHumanDecision = async (request: HumanDecisionRequest): Promise<void> => {
1750
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1766
+ const cancelHumanDecision = async (
1767
+ ctx: ExtensionContext,
1768
+ request: HumanDecisionRequest,
1769
+ ): Promise<void> => {
1770
+ const store = humanDecisionStore(ctx.cwd);
1751
1771
  await store.cancel(request, "cancelled");
1752
1772
  const cancellation = await store.readCancellation(request.decisionId);
1753
- if (cancellation !== null) await settleHumanDecisionChannels(cancellation);
1773
+ if (cancellation !== null) {
1774
+ await settleHumanDecisionChannels(cancellation);
1775
+ store.markEffectApplied(request.decisionId, "decision.settle_presentations");
1776
+ }
1754
1777
  lastWaitingRunId = null;
1755
1778
  };
1756
1779
 
@@ -1767,7 +1790,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1767
1790
  const continued = new Set(
1768
1791
  rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
1769
1792
  );
1770
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1793
+ const bundles = listWorkflowRuns();
1771
1794
  for (const bundle of bundles) {
1772
1795
  if (
1773
1796
  bundle.state.status !== "waiting" ||
@@ -1833,7 +1856,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1833
1856
  const { state } = widgetSource;
1834
1857
  const request = humanDecisionRequest(state.finalOutput);
1835
1858
  if (state.status === "waiting" && request !== null) {
1836
- await cancelHumanDecision(request);
1859
+ await cancelHumanDecision(ctx, request);
1837
1860
  clearWidgetTimer();
1838
1861
  clearWidget(ctx);
1839
1862
  return {
@@ -1854,7 +1877,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1854
1877
  }
1855
1878
  const recovered = await findOwnedWaitingHumanDecision(ctx);
1856
1879
  if (recovered !== null) {
1857
- await cancelHumanDecision(recovered.request);
1880
+ await cancelHumanDecision(ctx, recovered.request);
1858
1881
  return {
1859
1882
  message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
1860
1883
  details: {
@@ -1959,9 +1982,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
1959
1982
  runId?: string,
1960
1983
  ): Promise<WorkflowControlResult> => {
1961
1984
  if (runId !== undefined) {
1962
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1985
+ const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1986
+ if (
1987
+ launch !== undefined &&
1988
+ ["queued", "starting", "failed", "cancelled"].includes(launch.status)
1989
+ ) {
1990
+ return workflowLaunchStatus(launch);
1991
+ }
1992
+ const bundle = readWorkflowRun(runId);
1963
1993
  if (bundle === null) {
1964
- const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1965
1994
  if (launch === undefined) throw new Error(`Workflow run not found: ${runId}`);
1966
1995
  return workflowLaunchStatus(launch);
1967
1996
  }
@@ -2008,7 +2037,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2008
2037
  const continued = new Set(
2009
2038
  rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
2010
2039
  );
2011
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
2040
+ const bundles = listWorkflowRuns();
2012
2041
  parentRunId =
2013
2042
  bundles.find(
2014
2043
  (bundle) =>
@@ -2029,7 +2058,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2029
2058
  ) {
2030
2059
  throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
2031
2060
  }
2032
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(parentRunId));
2061
+ const parent = readWorkflowRun(parentRunId);
2033
2062
  if (
2034
2063
  parent === null ||
2035
2064
  parent.state.status !== "waiting" ||
@@ -2056,7 +2085,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2056
2085
  verified?: HumanDecisionChannelAnswer,
2057
2086
  ): Promise<WorkflowControlResult> => {
2058
2087
  const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
2059
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2088
+ const parent = readWorkflowRun(waiting.parentRunId);
2060
2089
  if (parent === null) throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
2061
2090
  const request = humanDecisionRequest(parent.state.finalOutput);
2062
2091
  let accepted: AcceptedHumanDecision | undefined;
@@ -2082,7 +2111,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2082
2111
  },
2083
2112
  idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
2084
2113
  };
2085
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
2114
+ const store = humanDecisionStore(ctx.cwd);
2086
2115
  const acceptance = await store.accept(request, submission);
2087
2116
  if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
2088
2117
  throw new Error("That human decision was already answered differently.");
@@ -2110,22 +2139,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
2110
2139
  };
2111
2140
  }
2112
2141
  }
2113
- const decisionStore = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
2114
- if (request !== null && continuationRunId !== undefined && accepted !== undefined) {
2115
- await decisionStore.recordContinuation(request.decisionId, {
2116
- schema: "pi-workflows.human-decision-continuation.v1",
2117
- decisionId: request.decisionId,
2118
- requestDigest: request.requestDigest,
2119
- provenance: accepted.provenance,
2120
- parentRunId: waiting.parentRunId,
2121
- runId: continuationRunId,
2122
- createdAt: accepted.acceptedAt,
2123
- });
2124
- }
2142
+ const decisionStore = humanDecisionStore(ctx.cwd);
2125
2143
  if (continuationRunId !== undefined) {
2126
- const existingContinuation = await readRunBundle(
2127
- new WorkflowRunStore().runDirFor(continuationRunId),
2128
- );
2144
+ const existingContinuation = readWorkflowRun(continuationRunId);
2129
2145
  if (existingContinuation !== null) {
2130
2146
  if (accepted !== undefined) await settleHumanDecisionChannels(accepted);
2131
2147
  lastWaitingRunId = null;
@@ -2149,7 +2165,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
2149
2165
  throw new Error("Could not start the checkpoint continuation.");
2150
2166
  }
2151
2167
  if (request !== null && accepted !== undefined) {
2168
+ await decisionStore.recordContinuation(request.decisionId, {
2169
+ schema: "pi-workflows.human-decision-continuation.v1",
2170
+ decisionId: request.decisionId,
2171
+ requestDigest: request.requestDigest,
2172
+ provenance: accepted.provenance,
2173
+ parentRunId: waiting.parentRunId,
2174
+ runId: continued,
2175
+ createdAt: accepted.acceptedAt,
2176
+ });
2177
+ decisionStore.markEffectApplied(request.decisionId, "decision.continue");
2152
2178
  await settleHumanDecisionChannels(accepted);
2179
+ decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2153
2180
  }
2154
2181
  lastWaitingRunId = null;
2155
2182
  return {
@@ -2170,7 +2197,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2170
2197
  const channel = new PiDecisionChannel({
2171
2198
  actorId: ctx.sessionManager.getSessionId(),
2172
2199
  ui: ctx.ui,
2173
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
2200
+ store: humanDecisionStore(ctx.cwd),
2174
2201
  onAnswer: async (answer) => {
2175
2202
  const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
2176
2203
  notify(ctx, result.message, result.level);
@@ -2207,7 +2234,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2207
2234
  config: loaded.channels,
2208
2235
  credentials: loaded.credentials,
2209
2236
  configDir: loaded.configDir,
2210
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
2237
+ store: humanDecisionStore(ctx.cwd),
2211
2238
  onAnswer: async (answer) => {
2212
2239
  await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
2213
2240
  },
@@ -2221,13 +2248,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
2221
2248
  ctx: ExtensionContext,
2222
2249
  deliverPending = true,
2223
2250
  ): Promise<void> => {
2224
- const runStore = new WorkflowRunStore();
2225
- const store = new HumanDecisionStore(runStore.outputRoot);
2251
+ const runStore = workflowStore(ctx.cwd);
2252
+ const store = humanDecisionStore(ctx.cwd);
2226
2253
  const requests = (await store.listRequests()).sort((left, right) =>
2227
2254
  left.createdAt.localeCompare(right.createdAt),
2228
2255
  );
2229
2256
  for (const request of requests) {
2230
- const parent = await readRunBundle(runStore.runDirFor(request.runId));
2257
+ const parent = runStore.readRun(request.runId);
2231
2258
  if (parent === null || parent.state.status !== "waiting") continue;
2232
2259
  const currentRequest = humanDecisionRequest(parent.state.finalOutput);
2233
2260
  if (
@@ -2242,6 +2269,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
2242
2269
  queueRecord !== undefined &&
2243
2270
  (queueRecord.originSessionId === null ||
2244
2271
  queueRecord.originSessionId === ctx.sessionManager.getSessionId());
2272
+ if (!ownedBySession) continue;
2273
+ const recoveryToken = randomUUID();
2274
+ const recoveryQueue = ensureRunQueueStore(ctx.cwd);
2275
+ const claimed = recoveryQueue.claimWorkflowRun({
2276
+ runId: request.runId,
2277
+ runnerId: ctx.sessionManager.getSessionId(),
2278
+ claimToken: recoveryToken,
2279
+ leaseMs: RUN_CLAIM_LEASE_MS,
2280
+ });
2281
+ if (claimed === undefined) continue;
2282
+ const ownerStore = humanDecisionStore(ctx.cwd, () =>
2283
+ recoveryQueue.workflowRunAuthority(request.runId, recoveryToken),
2284
+ );
2245
2285
  let resolved = await store.readResolved(request.decisionId);
2246
2286
  if (resolved === null) {
2247
2287
  let cancellation = await store.readCancellation(request.decisionId);
@@ -2249,11 +2289,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
2249
2289
  request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
2250
2290
  if (cancellation === null && expired) {
2251
2291
  if (request.defaultResponse === undefined) {
2252
- await store.cancel(request, "expired");
2253
- cancellation = await store.readCancellation(request.decisionId);
2292
+ await ownerStore.cancel(request, "expired");
2293
+ cancellation = await ownerStore.readCancellation(request.decisionId);
2254
2294
  } else {
2255
2295
  try {
2256
- resolved = (await store.resolveTimeout(request)).decision;
2296
+ resolved = (await ownerStore.resolveTimeout(request)).decision;
2257
2297
  } catch {
2258
2298
  cancellation = await store.readCancellation(request.decisionId);
2259
2299
  resolved = await store.readResolved(request.decisionId);
@@ -2261,11 +2301,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
2261
2301
  }
2262
2302
  }
2263
2303
  if (cancellation !== null) {
2304
+ recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2305
+ ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
2264
2306
  await settleHumanDecisionChannels(cancellation);
2307
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2308
+ ownerStore.close();
2265
2309
  continue;
2266
2310
  }
2267
2311
  if (resolved === null) {
2268
- if (!deliverPending || !ownedBySession) continue;
2312
+ if (!deliverPending) {
2313
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2314
+ ownerStore.close();
2315
+ continue;
2316
+ }
2269
2317
  const channels = audienceChannels(decisionChannelConfig, request.audience);
2270
2318
  for (const channelId of channels) {
2271
2319
  const channel = telegramDecisionChannels.get(channelId);
@@ -2275,13 +2323,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
2275
2323
  lastWaitingRunId = request.runId;
2276
2324
  queueMicrotask(() => void promptHumanDecision(ctx, request));
2277
2325
  }
2326
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2327
+ ownerStore.close();
2278
2328
  continue;
2279
2329
  }
2280
2330
  }
2281
2331
 
2282
- if (!ownedBySession) continue;
2283
- const currentParent = await readRunBundle(runStore.runDirFor(request.runId));
2284
- if (currentParent === null || currentParent.state.status !== "waiting") continue;
2332
+ const currentParent = runStore.readRun(request.runId);
2333
+ if (currentParent === null || currentParent.state.status !== "waiting") {
2334
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2335
+ ownerStore.close();
2336
+ continue;
2337
+ }
2285
2338
  const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
2286
2339
  const continuation = (await store.readContinuation(request.decisionId)) ?? {
2287
2340
  schema: "pi-workflows.human-decision-continuation.v1" as const,
@@ -2292,10 +2345,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
2292
2345
  runId,
2293
2346
  createdAt: resolved.acceptedAt,
2294
2347
  };
2295
- await store.recordContinuation(request.decisionId, continuation);
2296
- const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
2348
+ recoveryQueue.parkWorkflowRun({
2349
+ runId: request.runId,
2350
+ claimToken: recoveryToken,
2351
+ });
2352
+ const existing = runStore.readRun(continuation.runId);
2297
2353
  if (existing === null && activeRun === null) {
2298
- if (currentParent.state.workflowSource === undefined) continue;
2354
+ if (currentParent.state.workflowSource === undefined) {
2355
+ ownerStore.close();
2356
+ continue;
2357
+ }
2299
2358
  const workflowRef =
2300
2359
  currentParent.state.workflowSource.kind === "builtin"
2301
2360
  ? `builtin:${currentParent.state.workflowSource.id}`
@@ -2307,7 +2366,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
2307
2366
  quiet: true,
2308
2367
  });
2309
2368
  }
2369
+ if (runStore.readRun(continuation.runId) !== null) {
2370
+ await ownerStore.recordContinuation(request.decisionId, continuation);
2371
+ ownerStore.markEffectApplied(request.decisionId, "decision.continue");
2372
+ }
2310
2373
  await settleHumanDecisionChannels(resolved);
2374
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2375
+ ownerStore.close();
2311
2376
  if (activeRun !== null) break;
2312
2377
  }
2313
2378
  };
@@ -2368,7 +2433,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2368
2433
  }
2369
2434
  const queue = ensureRunQueueStore(ctx.cwd);
2370
2435
  const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
2371
- if (existing !== undefined) {
2436
+ if (existing !== undefined && existing.runId !== options.parentRunId) {
2372
2437
  throw new Error(
2373
2438
  `Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`,
2374
2439
  );
@@ -2380,7 +2445,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2380
2445
  const workflow = resolved.definition;
2381
2446
  const workflowSource = resolved.source;
2382
2447
  if (options.parentRunId !== undefined) {
2383
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
2448
+ const parent = readWorkflowRun(options.parentRunId);
2384
2449
  if (parent === null || parent.state.status !== "waiting") {
2385
2450
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
2386
2451
  }
@@ -2405,6 +2470,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2405
2470
  workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
2406
2471
  workflowSource: launchSourceIdentity(workflow, workflowSource),
2407
2472
  definitionDigest: definitionDigest(snapshot),
2473
+ definitionSnapshot: snapshot,
2408
2474
  input,
2409
2475
  launchOptions: preparedLaunchOptions(options),
2410
2476
  runnerId,
@@ -2613,7 +2679,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2613
2679
  const message = errorMessage(error);
2614
2680
  notify(
2615
2681
  ctx,
2616
- /workflow_run_queue_parent/.test(message)
2682
+ /continuations\.parent_run_id/.test(message)
2617
2683
  ? "That checkpoint was already answered; see its continuation run."
2618
2684
  : `Could not continue workflow: ${message}`,
2619
2685
  "error",
@@ -2768,9 +2834,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
2768
2834
  label: "Workflow",
2769
2835
  description: [
2770
2836
  "List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
2771
- "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.",
2837
+ "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.",
2838
+ "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.",
2772
2839
  "Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
2773
- "Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
2840
+ "Do not start repeated work without the user's request.",
2774
2841
  ].join(" "),
2775
2842
  parameters: WorkflowToolParameters,
2776
2843
  async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
@@ -2797,7 +2864,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2797
2864
  break;
2798
2865
  case "answer": {
2799
2866
  const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2800
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2867
+ const parent = readWorkflowRun(waiting.parentRunId);
2801
2868
  if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2802
2869
  throw new Error(
2803
2870
  "This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
@@ -2835,7 +2902,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2835
2902
  }
2836
2903
  throw new Error("No workflow step is waiting for output.");
2837
2904
  }
2838
- // Flush the conversation into the bundle before accepting, so the
2905
+ // Flush the conversation into SQLite before accepting, so the
2839
2906
  // attempt range includes the assistant message carrying this call.
2840
2907
  await activeRun.recorder?.record(ctx).catch(() => undefined);
2841
2908
  await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
@@ -2847,7 +2914,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
2847
2914
  if (!result.accepted) {
2848
2915
  throw new Error(result.message);
2849
2916
  }
2850
- suppressWorkflowAssistantTail = true;
2851
2917
  return {
2852
2918
  content: [{ type: "text", text: result.message }],
2853
2919
  details: { action: "submit", step: params.step, accepted: true },
@@ -2882,24 +2948,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2882
2948
  sessionClosed = false;
2883
2949
  controllerContext = ctx;
2884
2950
  if (herdrEnabled) void refreshHerdrCapability(ctx);
2885
- try {
2886
- const queue = ensureRunQueueStore(ctx.cwd);
2887
- const migration = await migrateLegacyWorkflowSources({
2888
- catalog: builtinWorkflowCatalog,
2889
- queue,
2890
- });
2891
- migrationBlockedRuns.clear();
2892
- for (const blocked of migration.blocked) migrationBlockedRuns.add(blocked.runId);
2893
- if (migration.blocked.length > 0) {
2894
- notify(
2895
- ctx,
2896
- `Could not migrate ${migration.blocked.length} legacy workflow source(s).`,
2897
- "warning",
2898
- );
2899
- }
2900
- } catch (error) {
2901
- notify(ctx, `Could not migrate legacy workflow sources: ${errorMessage(error)}`, "warning");
2902
- }
2951
+ ensureRunQueueStore(ctx.cwd);
2903
2952
  try {
2904
2953
  await reloadDecisionChannels(ctx);
2905
2954
  } catch {
@@ -2942,7 +2991,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
2942
2991
  });
2943
2992
 
2944
2993
  pi.on("agent_start", () => {
2945
- suppressWorkflowAssistantTail = false;
2946
2994
  if (!activeRun && presentationPending === null && presentationAbort) {
2947
2995
  // A normal user turn started while an async presentation prompt was
2948
2996
  // still resolving. The user's new request supersedes that old result.
@@ -2953,7 +3001,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
2953
3001
  });
2954
3002
 
2955
3003
  pi.on("agent_end", (event, ctx) => {
2956
- suppressWorkflowAssistantTail = false;
2957
3004
  const aborted = event.messages.some(
2958
3005
  (message) =>
2959
3006
  typeof message === "object" &&
@@ -2991,6 +3038,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
2991
3038
 
2992
3039
  pi.on("turn_end", async (event, ctx) => {
2993
3040
  await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
3041
+ // turn_end observes the message after message_end replacement handlers,
3042
+ // so this is the exact assistant message that remains visible.
3043
+ activeRun?.executor.handleMessageEnd(event.message);
2994
3044
  });
2995
3045
 
2996
3046
  pi.on("message_start", async (event, ctx) => {
@@ -3002,14 +3052,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
3002
3052
  });
3003
3053
 
3004
3054
  pi.on("message_end", (event) => {
3005
- if (suppressWorkflowAssistantTail && event.message.role === "assistant") {
3006
- const message = {
3007
- ...event.message,
3008
- content: event.message.content.filter((part) => part.type !== "text"),
3009
- };
3010
- activeRun?.recorder?.handleMessageEnd({ ...event, message });
3011
- return { message };
3012
- }
3013
3055
  activeRun?.recorder?.handleMessageEnd(event);
3014
3056
  });
3015
3057
 
@@ -3045,11 +3087,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
3045
3087
  return;
3046
3088
  }
3047
3089
  }
3048
- const flushedNatural = turnCoordinator.flushNatural(
3049
- ctx.isIdle() && !sessionClosed && !runHeld(),
3050
- );
3051
3090
  const run = activeRun;
3052
3091
  if (!run) {
3092
+ turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
3053
3093
  presentationPending = null;
3054
3094
  runSyncPass(ctx);
3055
3095
  return;
@@ -3057,7 +3097,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
3057
3097
  await run.recorder?.synchronize(ctx).catch(() => undefined);
3058
3098
  run.recorder?.settleAttempt();
3059
3099
  run.executor.setStreaming(false);
3060
- if (flushedNatural === 0) run.executor.handleAgentSettled();
3100
+ if (run.executor.pendingCompletion === "assistant") {
3101
+ // The visible response is the step output. Resolve it before any
3102
+ // unrelated deferred turn can start and replace the captured message.
3103
+ run.executor.handleAgentSettled();
3104
+ } else {
3105
+ const flushedNatural = turnCoordinator.flushNatural(
3106
+ ctx.isIdle() && !sessionClosed && !runHeld(),
3107
+ );
3108
+ if (flushedNatural === 0) run.executor.handleAgentSettled();
3109
+ }
3061
3110
  runSyncPass(ctx);
3062
3111
  });
3063
3112
 
@@ -3065,7 +3114,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
3065
3114
  sessionClosed = true;
3066
3115
  herdrProbeGeneration += 1;
3067
3116
  systemTurnAbort = null;
3068
- suppressWorkflowAssistantTail = false;
3069
3117
  turnCoordinator.clearDeferred();
3070
3118
  supersedePresentation();
3071
3119
  const run = activeRun;