@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,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.
@@ -838,7 +850,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
838
850
  workflowViewTarget = {
839
851
  runId: state.runId,
840
852
  workflowName: state.workflowName,
841
- runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
842
853
  };
843
854
  renderWidget(ctx);
844
855
  };
@@ -1048,12 +1059,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
1048
1059
  }
1049
1060
  try {
1050
1061
  if (outcome === "park") {
1051
- runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
1052
- } else {
1053
- runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
1062
+ if (runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken })) {
1063
+ runQueueStore.settleRunEffect(run.runId, "run.park_queue");
1064
+ }
1065
+ } else if (
1066
+ runQueueStore.completeWorkflowRun({ runId: run.runId, claimToken: run.claimToken })
1067
+ ) {
1068
+ runQueueStore.settleRunEffect(run.runId, "run.settle_queue");
1054
1069
  }
1055
1070
  } catch {
1056
- // Queue bookkeeping is best-effort next to the durable bundle.
1071
+ // Queue bookkeeping is best-effort next to the durable run fact.
1057
1072
  } finally {
1058
1073
  run.claimToken = undefined;
1059
1074
  }
@@ -1068,7 +1083,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1068
1083
  activeRun = null;
1069
1084
  }
1070
1085
  if (result.state.status === "running") {
1071
- // The run was parked for another runner; its bundle stays resumable
1086
+ // The run was parked for another runner; its SQLite state stays resumable
1072
1087
  // and its queue row becomes claimable. The recorder drains first: its
1073
1088
  // finalization writes share the fenced store, so the claim must stay
1074
1089
  // valid until capture is complete. Nothing else to present.
@@ -1095,7 +1110,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1095
1110
  );
1096
1111
  return;
1097
1112
  }
1098
- releaseClaim(run, "done");
1113
+ releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
1099
1114
  recordRunEvent({
1100
1115
  runId: run.runId,
1101
1116
  workflowRef: run.workflowName,
@@ -1223,7 +1238,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1223
1238
  // refused continuation (edited source, missing parent) must not consume
1224
1239
  // the parent's one-continuation slot.
1225
1240
  if (options.parentRunId !== undefined) {
1226
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
1241
+ const parent = readWorkflowRun(options.parentRunId);
1227
1242
  if (parent === null || parent.state.status !== "waiting") {
1228
1243
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
1229
1244
  }
@@ -1258,6 +1273,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1258
1273
  : workflowSource.path,
1259
1274
  workflowSource: launchSourceIdentity(workflow, workflowSource),
1260
1275
  definitionDigest: definitionDigest(snapshot),
1276
+ definitionSnapshot: snapshot,
1261
1277
  input,
1262
1278
  launchOptions: preparedLaunchOptions(options),
1263
1279
  runnerId,
@@ -1339,16 +1355,20 @@ export default function piWorkflows(pi: ExtensionAPI) {
1339
1355
  },
1340
1356
  conversation: {
1341
1357
  beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
1358
+ recoverAssistant: (contract) =>
1359
+ recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
1342
1360
  mark: () => run.recorder?.mark() ?? 0,
1343
1361
  rangeSince: (mark) => run.recorder?.rangeSince(mark),
1344
1362
  },
1345
1363
  });
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 },
1364
+ // The engine and session recorder share one transactional SQLite store.
1365
+ const store = workflowStore(
1366
+ ctx.cwd,
1367
+ queueStore === null || claimToken === undefined
1368
+ ? {}
1369
+ : {
1370
+ authorityProvider: () => queueStore.workflowRunAuthority(runId, claimToken),
1371
+ },
1352
1372
  );
1353
1373
  const engine = new WorkflowEngine({
1354
1374
  executor,
@@ -1373,8 +1393,8 @@ export default function piWorkflows(pi: ExtensionAPI) {
1373
1393
  },
1374
1394
  // Awaited by the engine after run_started is persisted, so the session
1375
1395
  // 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);
1396
+ onRunStarted: async (runId) => {
1397
+ const recorder = new SessionRecorder(store, runId);
1378
1398
  try {
1379
1399
  await recorder.bind(ctx);
1380
1400
  run.recorder = recorder;
@@ -1474,23 +1494,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
1474
1494
  // once the cause is fixed, and say so in the feed.
1475
1495
  let resumeFailed = run.resume === true && !isClaimLostError(error);
1476
1496
  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.
1497
+ // Re-parking only makes sense while the run is still resumable.
1498
+ // A terminal or waiting run closes the queue row instead, and the feed
1499
+ // reports the authoritative run state.
1481
1500
  if (resumeFailed) {
1482
1501
  try {
1483
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1502
+ const bundle = readWorkflowRun(runId);
1484
1503
  if (bundle === null || bundle.state.status !== "running") {
1485
1504
  resumeFailed = false;
1486
1505
  terminalStatus = bundle?.state.status;
1487
1506
  }
1488
1507
  } catch {
1489
- // Unreadable bundles close the row too.
1508
+ // Unreadable run state closes the row too.
1490
1509
  resumeFailed = false;
1491
1510
  }
1492
1511
  }
1493
- // A continuation that failed before its bundle exists frees the
1512
+ // A continuation that failed before its run row exists frees the
1494
1513
  // parent's continuation slot instead of consuming it forever.
1495
1514
  let continuationSlotFreed = false;
1496
1515
  if (
@@ -1499,7 +1518,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1499
1518
  !isClaimLostError(error)
1500
1519
  ) {
1501
1520
  try {
1502
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1521
+ const bundle = readWorkflowRun(runId);
1503
1522
  if (bundle === null) {
1504
1523
  continuationSlotFreed =
1505
1524
  runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
@@ -1577,7 +1596,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1577
1596
  runnerId,
1578
1597
  claimToken,
1579
1598
  leaseMs: RUN_CLAIM_LEASE_MS,
1580
- excludeRunIds: [...migrationBlockedRuns],
1599
+ excludeRunIds: [],
1581
1600
  sessionId: ctx.sessionManager.getSessionId(),
1582
1601
  });
1583
1602
  if (claimed === undefined) {
@@ -1585,7 +1604,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
1585
1604
  }
1586
1605
  let started: string | undefined;
1587
1606
  try {
1588
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(claimed.runId));
1607
+ const bundle = readWorkflowRun(claimed.runId);
1608
+ if (bundle?.state.status === "waiting") {
1609
+ queueStore.parkWorkflowRun({ runId: claimed.runId, claimToken });
1610
+ return;
1611
+ }
1589
1612
  const sourceRef =
1590
1613
  bundle?.state.workflowSource === undefined
1591
1614
  ? claimed.workflowSourceRef
@@ -1595,7 +1618,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1595
1618
  const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
1596
1619
  started = await startRun(ctx, sourceRef, claimed.input, {
1597
1620
  ...launchOptions,
1598
- resume: bundle !== null,
1621
+ resume: claimed.initialized,
1599
1622
  runId: claimed.runId,
1600
1623
  claimToken,
1601
1624
  });
@@ -1623,16 +1646,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
1623
1646
  ? { state: "running", runId: activeRun.runId }
1624
1647
  : workflowSchedulerResult(activeRun.lastState);
1625
1648
  }
1626
- const store = new WorkflowRunStore();
1627
- const bundle = await readRunBundle(store.runDirFor(request.runId));
1649
+ const store = workflowStore(controllerContext?.cwd ?? process.cwd());
1650
+ const bundle = store.readRun(request.runId);
1628
1651
  if (bundle !== null) {
1629
1652
  const recovered =
1630
1653
  bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
1631
1654
  if (recovered !== null) {
1632
- const lastTraceEvent = await readLastTraceEvent(
1633
- recovered.runDir,
1634
- recovered.manifest.paths.trace,
1635
- );
1655
+ const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
1636
1656
  return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
1637
1657
  }
1638
1658
  return { state: "pending" };
@@ -1640,7 +1660,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
1640
1660
  if (controllerContext === null) {
1641
1661
  return { state: "pending" };
1642
1662
  }
1643
- await store.quarantineIncompleteRun(request.runId);
1644
1663
  const runId = await startRun(controllerContext, request.workflow, request.input, {
1645
1664
  runId: request.runId,
1646
1665
  childKey,
@@ -1746,11 +1765,17 @@ export default function piWorkflows(pi: ExtensionAPI) {
1746
1765
  notify(ctx, result.message, result.level);
1747
1766
  };
1748
1767
 
1749
- const cancelHumanDecision = async (request: HumanDecisionRequest): Promise<void> => {
1750
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
1768
+ const cancelHumanDecision = async (
1769
+ ctx: ExtensionContext,
1770
+ request: HumanDecisionRequest,
1771
+ ): Promise<void> => {
1772
+ const store = humanDecisionStore(ctx.cwd);
1751
1773
  await store.cancel(request, "cancelled");
1752
1774
  const cancellation = await store.readCancellation(request.decisionId);
1753
- if (cancellation !== null) await settleHumanDecisionChannels(cancellation);
1775
+ if (cancellation !== null) {
1776
+ await settleHumanDecisionChannels(cancellation);
1777
+ store.markEffectApplied(request.decisionId, "decision.settle_presentations");
1778
+ }
1754
1779
  lastWaitingRunId = null;
1755
1780
  };
1756
1781
 
@@ -1767,7 +1792,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1767
1792
  const continued = new Set(
1768
1793
  rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
1769
1794
  );
1770
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1795
+ const bundles = listWorkflowRuns();
1771
1796
  for (const bundle of bundles) {
1772
1797
  if (
1773
1798
  bundle.state.status !== "waiting" ||
@@ -1833,7 +1858,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1833
1858
  const { state } = widgetSource;
1834
1859
  const request = humanDecisionRequest(state.finalOutput);
1835
1860
  if (state.status === "waiting" && request !== null) {
1836
- await cancelHumanDecision(request);
1861
+ await cancelHumanDecision(ctx, request);
1837
1862
  clearWidgetTimer();
1838
1863
  clearWidget(ctx);
1839
1864
  return {
@@ -1854,7 +1879,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1854
1879
  }
1855
1880
  const recovered = await findOwnedWaitingHumanDecision(ctx);
1856
1881
  if (recovered !== null) {
1857
- await cancelHumanDecision(recovered.request);
1882
+ await cancelHumanDecision(ctx, recovered.request);
1858
1883
  return {
1859
1884
  message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
1860
1885
  details: {
@@ -1959,9 +1984,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
1959
1984
  runId?: string,
1960
1985
  ): Promise<WorkflowControlResult> => {
1961
1986
  if (runId !== undefined) {
1962
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1987
+ const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1988
+ if (
1989
+ launch !== undefined &&
1990
+ ["queued", "starting", "failed", "cancelled"].includes(launch.status)
1991
+ ) {
1992
+ return workflowLaunchStatus(launch);
1993
+ }
1994
+ const bundle = readWorkflowRun(runId);
1963
1995
  if (bundle === null) {
1964
- const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1965
1996
  if (launch === undefined) throw new Error(`Workflow run not found: ${runId}`);
1966
1997
  return workflowLaunchStatus(launch);
1967
1998
  }
@@ -2008,7 +2039,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2008
2039
  const continued = new Set(
2009
2040
  rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
2010
2041
  );
2011
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
2042
+ const bundles = listWorkflowRuns();
2012
2043
  parentRunId =
2013
2044
  bundles.find(
2014
2045
  (bundle) =>
@@ -2029,7 +2060,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2029
2060
  ) {
2030
2061
  throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
2031
2062
  }
2032
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(parentRunId));
2063
+ const parent = readWorkflowRun(parentRunId);
2033
2064
  if (
2034
2065
  parent === null ||
2035
2066
  parent.state.status !== "waiting" ||
@@ -2056,7 +2087,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2056
2087
  verified?: HumanDecisionChannelAnswer,
2057
2088
  ): Promise<WorkflowControlResult> => {
2058
2089
  const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
2059
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2090
+ const parent = readWorkflowRun(waiting.parentRunId);
2060
2091
  if (parent === null) throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
2061
2092
  const request = humanDecisionRequest(parent.state.finalOutput);
2062
2093
  let accepted: AcceptedHumanDecision | undefined;
@@ -2082,7 +2113,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2082
2113
  },
2083
2114
  idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
2084
2115
  };
2085
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
2116
+ const store = humanDecisionStore(ctx.cwd);
2086
2117
  const acceptance = await store.accept(request, submission);
2087
2118
  if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
2088
2119
  throw new Error("That human decision was already answered differently.");
@@ -2110,22 +2141,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
2110
2141
  };
2111
2142
  }
2112
2143
  }
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
- }
2144
+ const decisionStore = humanDecisionStore(ctx.cwd);
2125
2145
  if (continuationRunId !== undefined) {
2126
- const existingContinuation = await readRunBundle(
2127
- new WorkflowRunStore().runDirFor(continuationRunId),
2128
- );
2146
+ const existingContinuation = readWorkflowRun(continuationRunId);
2129
2147
  if (existingContinuation !== null) {
2130
2148
  if (accepted !== undefined) await settleHumanDecisionChannels(accepted);
2131
2149
  lastWaitingRunId = null;
@@ -2149,7 +2167,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
2149
2167
  throw new Error("Could not start the checkpoint continuation.");
2150
2168
  }
2151
2169
  if (request !== null && accepted !== undefined) {
2170
+ await decisionStore.recordContinuation(request.decisionId, {
2171
+ schema: "pi-workflows.human-decision-continuation.v1",
2172
+ decisionId: request.decisionId,
2173
+ requestDigest: request.requestDigest,
2174
+ provenance: accepted.provenance,
2175
+ parentRunId: waiting.parentRunId,
2176
+ runId: continued,
2177
+ createdAt: accepted.acceptedAt,
2178
+ });
2179
+ decisionStore.markEffectApplied(request.decisionId, "decision.continue");
2152
2180
  await settleHumanDecisionChannels(accepted);
2181
+ decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2153
2182
  }
2154
2183
  lastWaitingRunId = null;
2155
2184
  return {
@@ -2170,7 +2199,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2170
2199
  const channel = new PiDecisionChannel({
2171
2200
  actorId: ctx.sessionManager.getSessionId(),
2172
2201
  ui: ctx.ui,
2173
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
2202
+ store: humanDecisionStore(ctx.cwd),
2174
2203
  onAnswer: async (answer) => {
2175
2204
  const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
2176
2205
  notify(ctx, result.message, result.level);
@@ -2207,7 +2236,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2207
2236
  config: loaded.channels,
2208
2237
  credentials: loaded.credentials,
2209
2238
  configDir: loaded.configDir,
2210
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
2239
+ store: humanDecisionStore(ctx.cwd),
2211
2240
  onAnswer: async (answer) => {
2212
2241
  await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
2213
2242
  },
@@ -2221,13 +2250,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
2221
2250
  ctx: ExtensionContext,
2222
2251
  deliverPending = true,
2223
2252
  ): Promise<void> => {
2224
- const runStore = new WorkflowRunStore();
2225
- const store = new HumanDecisionStore(runStore.outputRoot);
2253
+ const runStore = workflowStore(ctx.cwd);
2254
+ const store = humanDecisionStore(ctx.cwd);
2226
2255
  const requests = (await store.listRequests()).sort((left, right) =>
2227
2256
  left.createdAt.localeCompare(right.createdAt),
2228
2257
  );
2229
2258
  for (const request of requests) {
2230
- const parent = await readRunBundle(runStore.runDirFor(request.runId));
2259
+ const parent = runStore.readRun(request.runId);
2231
2260
  if (parent === null || parent.state.status !== "waiting") continue;
2232
2261
  const currentRequest = humanDecisionRequest(parent.state.finalOutput);
2233
2262
  if (
@@ -2242,6 +2271,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
2242
2271
  queueRecord !== undefined &&
2243
2272
  (queueRecord.originSessionId === null ||
2244
2273
  queueRecord.originSessionId === ctx.sessionManager.getSessionId());
2274
+ if (!ownedBySession) continue;
2275
+ const recoveryToken = randomUUID();
2276
+ const recoveryQueue = ensureRunQueueStore(ctx.cwd);
2277
+ const claimed = recoveryQueue.claimWorkflowRun({
2278
+ runId: request.runId,
2279
+ runnerId: ctx.sessionManager.getSessionId(),
2280
+ claimToken: recoveryToken,
2281
+ leaseMs: RUN_CLAIM_LEASE_MS,
2282
+ });
2283
+ if (claimed === undefined) continue;
2284
+ const ownerStore = humanDecisionStore(ctx.cwd, () =>
2285
+ recoveryQueue.workflowRunAuthority(request.runId, recoveryToken),
2286
+ );
2245
2287
  let resolved = await store.readResolved(request.decisionId);
2246
2288
  if (resolved === null) {
2247
2289
  let cancellation = await store.readCancellation(request.decisionId);
@@ -2249,11 +2291,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
2249
2291
  request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
2250
2292
  if (cancellation === null && expired) {
2251
2293
  if (request.defaultResponse === undefined) {
2252
- await store.cancel(request, "expired");
2253
- cancellation = await store.readCancellation(request.decisionId);
2294
+ await ownerStore.cancel(request, "expired");
2295
+ cancellation = await ownerStore.readCancellation(request.decisionId);
2254
2296
  } else {
2255
2297
  try {
2256
- resolved = (await store.resolveTimeout(request)).decision;
2298
+ resolved = (await ownerStore.resolveTimeout(request)).decision;
2257
2299
  } catch {
2258
2300
  cancellation = await store.readCancellation(request.decisionId);
2259
2301
  resolved = await store.readResolved(request.decisionId);
@@ -2261,11 +2303,19 @@ export default function piWorkflows(pi: ExtensionAPI) {
2261
2303
  }
2262
2304
  }
2263
2305
  if (cancellation !== null) {
2306
+ recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2307
+ ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
2264
2308
  await settleHumanDecisionChannels(cancellation);
2309
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2310
+ ownerStore.close();
2265
2311
  continue;
2266
2312
  }
2267
2313
  if (resolved === null) {
2268
- if (!deliverPending || !ownedBySession) continue;
2314
+ if (!deliverPending) {
2315
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2316
+ ownerStore.close();
2317
+ continue;
2318
+ }
2269
2319
  const channels = audienceChannels(decisionChannelConfig, request.audience);
2270
2320
  for (const channelId of channels) {
2271
2321
  const channel = telegramDecisionChannels.get(channelId);
@@ -2275,13 +2325,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
2275
2325
  lastWaitingRunId = request.runId;
2276
2326
  queueMicrotask(() => void promptHumanDecision(ctx, request));
2277
2327
  }
2328
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2329
+ ownerStore.close();
2278
2330
  continue;
2279
2331
  }
2280
2332
  }
2281
2333
 
2282
- if (!ownedBySession) continue;
2283
- const currentParent = await readRunBundle(runStore.runDirFor(request.runId));
2284
- if (currentParent === null || currentParent.state.status !== "waiting") continue;
2334
+ const currentParent = runStore.readRun(request.runId);
2335
+ if (currentParent === null || currentParent.state.status !== "waiting") {
2336
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2337
+ ownerStore.close();
2338
+ continue;
2339
+ }
2285
2340
  const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
2286
2341
  const continuation = (await store.readContinuation(request.decisionId)) ?? {
2287
2342
  schema: "pi-workflows.human-decision-continuation.v1" as const,
@@ -2292,10 +2347,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
2292
2347
  runId,
2293
2348
  createdAt: resolved.acceptedAt,
2294
2349
  };
2295
- await store.recordContinuation(request.decisionId, continuation);
2296
- const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
2350
+ recoveryQueue.parkWorkflowRun({
2351
+ runId: request.runId,
2352
+ claimToken: recoveryToken,
2353
+ });
2354
+ const existing = runStore.readRun(continuation.runId);
2297
2355
  if (existing === null && activeRun === null) {
2298
- if (currentParent.state.workflowSource === undefined) continue;
2356
+ if (currentParent.state.workflowSource === undefined) {
2357
+ ownerStore.close();
2358
+ continue;
2359
+ }
2299
2360
  const workflowRef =
2300
2361
  currentParent.state.workflowSource.kind === "builtin"
2301
2362
  ? `builtin:${currentParent.state.workflowSource.id}`
@@ -2307,7 +2368,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
2307
2368
  quiet: true,
2308
2369
  });
2309
2370
  }
2371
+ if (runStore.readRun(continuation.runId) !== null) {
2372
+ await ownerStore.recordContinuation(request.decisionId, continuation);
2373
+ ownerStore.markEffectApplied(request.decisionId, "decision.continue");
2374
+ }
2310
2375
  await settleHumanDecisionChannels(resolved);
2376
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2377
+ ownerStore.close();
2311
2378
  if (activeRun !== null) break;
2312
2379
  }
2313
2380
  };
@@ -2368,7 +2435,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2368
2435
  }
2369
2436
  const queue = ensureRunQueueStore(ctx.cwd);
2370
2437
  const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
2371
- if (existing !== undefined) {
2438
+ if (existing !== undefined && existing.runId !== options.parentRunId) {
2372
2439
  throw new Error(
2373
2440
  `Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`,
2374
2441
  );
@@ -2380,7 +2447,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2380
2447
  const workflow = resolved.definition;
2381
2448
  const workflowSource = resolved.source;
2382
2449
  if (options.parentRunId !== undefined) {
2383
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
2450
+ const parent = readWorkflowRun(options.parentRunId);
2384
2451
  if (parent === null || parent.state.status !== "waiting") {
2385
2452
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
2386
2453
  }
@@ -2405,6 +2472,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2405
2472
  workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
2406
2473
  workflowSource: launchSourceIdentity(workflow, workflowSource),
2407
2474
  definitionDigest: definitionDigest(snapshot),
2475
+ definitionSnapshot: snapshot,
2408
2476
  input,
2409
2477
  launchOptions: preparedLaunchOptions(options),
2410
2478
  runnerId,
@@ -2613,7 +2681,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2613
2681
  const message = errorMessage(error);
2614
2682
  notify(
2615
2683
  ctx,
2616
- /workflow_run_queue_parent/.test(message)
2684
+ /continuations\.parent_run_id/.test(message)
2617
2685
  ? "That checkpoint was already answered; see its continuation run."
2618
2686
  : `Could not continue workflow: ${message}`,
2619
2687
  "error",
@@ -2768,9 +2836,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
2768
2836
  label: "Workflow",
2769
2837
  description: [
2770
2838
  "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.",
2839
+ "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.",
2840
+ "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
2841
  "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.",
2842
+ "Do not start repeated work without the user's request.",
2774
2843
  ].join(" "),
2775
2844
  parameters: WorkflowToolParameters,
2776
2845
  async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
@@ -2797,7 +2866,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2797
2866
  break;
2798
2867
  case "answer": {
2799
2868
  const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2800
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2869
+ const parent = readWorkflowRun(waiting.parentRunId);
2801
2870
  if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2802
2871
  throw new Error(
2803
2872
  "This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
@@ -2835,7 +2904,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2835
2904
  }
2836
2905
  throw new Error("No workflow step is waiting for output.");
2837
2906
  }
2838
- // Flush the conversation into the bundle before accepting, so the
2907
+ // Flush the conversation into SQLite before accepting, so the
2839
2908
  // attempt range includes the assistant message carrying this call.
2840
2909
  await activeRun.recorder?.record(ctx).catch(() => undefined);
2841
2910
  await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
@@ -2882,24 +2951,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2882
2951
  sessionClosed = false;
2883
2952
  controllerContext = ctx;
2884
2953
  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
- }
2954
+ ensureRunQueueStore(ctx.cwd);
2903
2955
  try {
2904
2956
  await reloadDecisionChannels(ctx);
2905
2957
  } catch {
@@ -2991,6 +3043,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
2991
3043
 
2992
3044
  pi.on("turn_end", async (event, ctx) => {
2993
3045
  await activeRun?.recorder?.handleTurnEnd(event, ctx).catch(() => undefined);
3046
+ // turn_end observes the message after message_end replacement handlers,
3047
+ // so this is the exact assistant message that remains visible.
3048
+ activeRun?.executor.handleMessageEnd(event.message);
2994
3049
  });
2995
3050
 
2996
3051
  pi.on("message_start", async (event, ctx) => {
@@ -3045,11 +3100,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
3045
3100
  return;
3046
3101
  }
3047
3102
  }
3048
- const flushedNatural = turnCoordinator.flushNatural(
3049
- ctx.isIdle() && !sessionClosed && !runHeld(),
3050
- );
3051
3103
  const run = activeRun;
3052
3104
  if (!run) {
3105
+ turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
3053
3106
  presentationPending = null;
3054
3107
  runSyncPass(ctx);
3055
3108
  return;
@@ -3057,7 +3110,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
3057
3110
  await run.recorder?.synchronize(ctx).catch(() => undefined);
3058
3111
  run.recorder?.settleAttempt();
3059
3112
  run.executor.setStreaming(false);
3060
- if (flushedNatural === 0) run.executor.handleAgentSettled();
3113
+ if (run.executor.pendingCompletion === "assistant") {
3114
+ // The visible response is the step output. Resolve it before any
3115
+ // unrelated deferred turn can start and replace the captured message.
3116
+ run.executor.handleAgentSettled();
3117
+ } else {
3118
+ const flushedNatural = turnCoordinator.flushNatural(
3119
+ ctx.isIdle() && !sessionClosed && !runHeld(),
3120
+ );
3121
+ if (flushedNatural === 0) run.executor.handleAgentSettled();
3122
+ }
3061
3123
  runSyncPass(ctx);
3062
3124
  });
3063
3125