@osolmaz/pi-workflows 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +56 -31
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +797 -142
  4. package/dist/builtins/autoimplement.workflow.js +17 -105
  5. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  6. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  7. package/dist/builtins/autoplan.workflow.js +200 -29
  8. package/dist/builtins/autoplan.workflow.js.map +1 -1
  9. package/dist/builtins/catalog.js +7 -5
  10. package/dist/builtins/catalog.js.map +1 -1
  11. package/dist/builtins/index.d.ts +4 -2
  12. package/dist/builtins/index.js +3 -1
  13. package/dist/builtins/index.js.map +1 -1
  14. package/dist/builtins/monitor.workflow.d.ts +56 -23
  15. package/dist/builtins/monitor.workflow.js +508 -329
  16. package/dist/builtins/monitor.workflow.js.map +1 -1
  17. package/dist/builtins/pi-agent-group.d.ts +72 -0
  18. package/dist/builtins/pi-agent-group.js +1087 -0
  19. package/dist/builtins/pi-agent-group.js.map +1 -0
  20. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  21. package/dist/builtins/plain-summary.workflow.js +134 -0
  22. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  23. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  24. package/dist/builtins/plan-approval.workflow.js +92 -14
  25. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  26. package/dist/builtins/plan-change.workflow.d.ts +449 -0
  27. package/dist/builtins/plan-change.workflow.js +256 -0
  28. package/dist/builtins/plan-change.workflow.js.map +1 -0
  29. package/dist/builtins/plan-presentation.js +2 -2
  30. package/dist/builtins/plan-presentation.js.map +1 -1
  31. package/dist/builtins/sanity-check.workflow.d.ts +50 -6
  32. package/dist/builtins/sanity-check.workflow.js +150 -28
  33. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  34. package/dist/controllers/effects.d.ts +3 -2
  35. package/dist/controllers/effects.js +8 -1
  36. package/dist/controllers/effects.js.map +1 -1
  37. package/dist/controllers/index.d.ts +1 -1
  38. package/dist/controllers/index.js +1 -1
  39. package/dist/controllers/index.js.map +1 -1
  40. package/dist/controllers/manager.d.ts +1 -0
  41. package/dist/controllers/manager.js +41 -17
  42. package/dist/controllers/manager.js.map +1 -1
  43. package/dist/controllers/sqlite.d.ts +107 -85
  44. package/dist/controllers/sqlite.js +1611 -1486
  45. package/dist/controllers/sqlite.js.map +1 -1
  46. package/dist/controllers/store.d.ts +9 -8
  47. package/dist/controllers/store.js +1 -31
  48. package/dist/controllers/store.js.map +1 -1
  49. package/dist/controllers/types.d.ts +3 -0
  50. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  51. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  52. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  53. package/dist/controllers/workflows.d.ts +2 -2
  54. package/dist/controllers/workflows.js +11 -10
  55. package/dist/controllers/workflows.js.map +1 -1
  56. package/dist/extension/controller-host.js +2 -2
  57. package/dist/extension/controller-host.js.map +1 -1
  58. package/dist/extension/decision-channels.d.ts +4 -2
  59. package/dist/extension/decision-channels.js +333 -146
  60. package/dist/extension/decision-channels.js.map +1 -1
  61. package/dist/extension/executor.d.ts +9 -2
  62. package/dist/extension/executor.js +89 -2
  63. package/dist/extension/executor.js.map +1 -1
  64. package/dist/extension/herdr-viewer.d.ts +0 -1
  65. package/dist/extension/herdr-viewer.js +0 -4
  66. package/dist/extension/herdr-viewer.js.map +1 -1
  67. package/dist/extension/index.js +209 -127
  68. package/dist/extension/index.js.map +1 -1
  69. package/dist/extension/recorder.d.ts +3 -10
  70. package/dist/extension/recorder.js +13 -22
  71. package/dist/extension/recorder.js.map +1 -1
  72. package/dist/extension/session-events.d.ts +2 -2
  73. package/dist/extension/step-message.d.ts +7 -2
  74. package/dist/extension/step-message.js +94 -2
  75. package/dist/extension/step-message.js.map +1 -1
  76. package/dist/extension/widget.js +29 -3
  77. package/dist/extension/widget.js.map +1 -1
  78. package/dist/host/rpc-executor.d.ts +3 -0
  79. package/dist/host/rpc-executor.js +2 -0
  80. package/dist/host/rpc-executor.js.map +1 -1
  81. package/dist/host/runner.d.ts +3 -6
  82. package/dist/host/runner.js +83 -32
  83. package/dist/host/runner.js.map +1 -1
  84. package/dist/render/graph-render.js +15 -10
  85. package/dist/render/graph-render.js.map +1 -1
  86. package/dist/state/database.d.ts +35 -0
  87. package/dist/state/database.js +287 -0
  88. package/dist/state/database.js.map +1 -0
  89. package/dist/state/index.d.ts +4 -0
  90. package/dist/state/index.js +5 -0
  91. package/dist/state/index.js.map +1 -0
  92. package/dist/state/json.d.ts +6 -0
  93. package/dist/state/json.js +38 -0
  94. package/dist/state/json.js.map +1 -0
  95. package/dist/state/mutation.d.ts +57 -0
  96. package/dist/state/mutation.js +255 -0
  97. package/dist/state/mutation.js.map +1 -0
  98. package/dist/state/schema.d.ts +6 -0
  99. package/dist/state/schema.js +473 -0
  100. package/dist/state/schema.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +2 -2
  102. package/dist/viewer/cli.js +142 -122
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/viewer/render.d.ts +4 -4
  105. package/dist/viewer/render.js +9 -23
  106. package/dist/viewer/render.js.map +1 -1
  107. package/dist/viewer/session-reducer.d.ts +1 -1
  108. package/dist/viewer/session-reducer.js +1 -1
  109. package/dist/viewer/tui.d.ts +3 -3
  110. package/dist/viewer/tui.js +15 -13
  111. package/dist/viewer/tui.js.map +1 -1
  112. package/dist/viewer/watch.d.ts +2 -5
  113. package/dist/viewer/watch.js +13 -13
  114. package/dist/viewer/watch.js.map +1 -1
  115. package/dist/workflows/catalog.js +7 -2
  116. package/dist/workflows/catalog.js.map +1 -1
  117. package/dist/workflows/composition.js +20 -2
  118. package/dist/workflows/composition.js.map +1 -1
  119. package/dist/workflows/decision-presentation.d.ts +1 -1
  120. package/dist/workflows/decision-presentation.js +51 -38
  121. package/dist/workflows/decision-presentation.js.map +1 -1
  122. package/dist/workflows/definition.d.ts +6 -2
  123. package/dist/workflows/definition.js +17 -0
  124. package/dist/workflows/definition.js.map +1 -1
  125. package/dist/workflows/engine.d.ts +7 -7
  126. package/dist/workflows/engine.js +196 -115
  127. package/dist/workflows/engine.js.map +1 -1
  128. package/dist/workflows/errors.d.ts +16 -3
  129. package/dist/workflows/errors.js +18 -3
  130. package/dist/workflows/errors.js.map +1 -1
  131. package/dist/workflows/human-decision.d.ts +45 -8
  132. package/dist/workflows/human-decision.js +641 -274
  133. package/dist/workflows/human-decision.js.map +1 -1
  134. package/dist/workflows/index.d.ts +5 -6
  135. package/dist/workflows/index.js +4 -5
  136. package/dist/workflows/index.js.map +1 -1
  137. package/dist/workflows/progress.d.ts +1 -0
  138. package/dist/workflows/progress.js +15 -3
  139. package/dist/workflows/progress.js.map +1 -1
  140. package/dist/workflows/schema.js +27 -1
  141. package/dist/workflows/schema.js.map +1 -1
  142. package/dist/workflows/store.d.ts +83 -103
  143. package/dist/workflows/store.js +907 -951
  144. package/dist/workflows/store.js.map +1 -1
  145. package/dist/workflows/types.d.ts +88 -126
  146. package/docs/CONTROLLERS.md +10 -8
  147. package/docs/DEFERRED_TURNS.md +2 -2
  148. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  149. package/docs/HUMAN_DECISIONS.md +36 -47
  150. package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
  151. package/docs/MONITOR.md +179 -237
  152. package/docs/SQLITE_STATE.md +219 -0
  153. package/docs/WORKFLOW_COMPOSITION.md +27 -26
  154. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  155. package/docs/WORKFLOW_UPDATES.md +10 -9
  156. package/docs/development.md +9 -9
  157. package/docs/live-replay-protocol.md +28 -37
  158. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  159. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  160. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  161. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  162. package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
  163. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  164. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  165. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  166. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  167. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  168. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  169. package/docs/session-event-journal.md +63 -434
  170. package/docs/tui-viewer.md +13 -15
  171. package/docs/workflows.md +127 -53
  172. package/examples/workflows/approved-plan.workflow.ts +19 -46
  173. package/examples/workflows/plain-summary.workflow.ts +1 -0
  174. package/herdr-plugin.toml +1 -1
  175. package/package.json +7 -7
  176. package/plugins/herdr/viewer.mjs +1 -11
  177. package/schemas/human-decision-accepted-v1.schema.json +15 -3
  178. package/schemas/human-decision-continuation-v1.schema.json +10 -1
  179. package/schemas/human-decision-delivery-v1.schema.json +8 -0
  180. package/schemas/human-decision-receipt-v1.schema.json +8 -0
  181. package/schemas/human-decision-request-v1.schema.json +24 -4
  182. package/skills/autoimplement/SKILL.md +27 -0
  183. package/skills/autoplan/SKILL.md +5 -4
  184. package/skills/monitor/SKILL.md +91 -135
  185. package/skills/pi-workflows/SKILL.md +9 -8
  186. package/skills/sanity-check/SKILL.md +44 -0
  187. package/src/builtins/autoimplement.workflow.ts +19 -118
  188. package/src/builtins/autoplan.workflow.ts +290 -40
  189. package/src/builtins/catalog.ts +7 -5
  190. package/src/builtins/index.ts +18 -1
  191. package/src/builtins/monitor.workflow.ts +673 -382
  192. package/src/builtins/pi-agent-group.ts +1407 -0
  193. package/src/builtins/plain-summary.workflow.ts +185 -0
  194. package/src/builtins/plan-approval.workflow.ts +157 -24
  195. package/src/builtins/plan-change.workflow.ts +321 -0
  196. package/src/builtins/plan-presentation.ts +2 -2
  197. package/src/builtins/sanity-check.workflow.ts +248 -48
  198. package/src/controllers/effects.ts +7 -0
  199. package/src/controllers/index.ts +0 -6
  200. package/src/controllers/manager.ts +72 -32
  201. package/src/controllers/sqlite.ts +2420 -1948
  202. package/src/controllers/store.ts +17 -38
  203. package/src/controllers/types.ts +3 -0
  204. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  205. package/src/controllers/workflows.ts +45 -21
  206. package/src/extension/controller-host.ts +1 -2
  207. package/src/extension/decision-channels.ts +456 -203
  208. package/src/extension/executor.ts +105 -2
  209. package/src/extension/herdr-viewer.ts +0 -5
  210. package/src/extension/index.ts +251 -151
  211. package/src/extension/recorder.ts +13 -22
  212. package/src/extension/session-events.ts +2 -2
  213. package/src/extension/step-message.ts +106 -4
  214. package/src/extension/widget.ts +32 -5
  215. package/src/host/rpc-executor.ts +4 -0
  216. package/src/host/runner.ts +93 -41
  217. package/src/render/graph-render.ts +12 -6
  218. package/src/state/database.ts +375 -0
  219. package/src/state/index.ts +29 -0
  220. package/src/state/json.ts +42 -0
  221. package/src/state/mutation.ts +435 -0
  222. package/src/state/schema.ts +475 -0
  223. package/src/viewer/cli.ts +151 -144
  224. package/src/viewer/render.ts +19 -30
  225. package/src/viewer/session-reducer.ts +1 -1
  226. package/src/viewer/tui.ts +18 -17
  227. package/src/viewer/watch.ts +13 -17
  228. package/src/workflows/catalog.ts +7 -2
  229. package/src/workflows/composition.ts +21 -3
  230. package/src/workflows/decision-presentation.ts +56 -43
  231. package/src/workflows/definition.ts +36 -3
  232. package/src/workflows/engine.ts +228 -130
  233. package/src/workflows/errors.ts +27 -3
  234. package/src/workflows/human-decision.ts +921 -349
  235. package/src/workflows/index.ts +20 -30
  236. package/src/workflows/progress.ts +18 -3
  237. package/src/workflows/schema.ts +36 -1
  238. package/src/workflows/store.ts +1300 -1163
  239. package/src/workflows/types.ts +100 -137
  240. package/dist/builtins/sanity-check-session.d.ts +0 -17
  241. package/dist/builtins/sanity-check-session.js +0 -168
  242. package/dist/builtins/sanity-check-session.js.map +0 -1
  243. package/dist/workflows/artifacts.d.ts +0 -40
  244. package/dist/workflows/artifacts.js +0 -155
  245. package/dist/workflows/artifacts.js.map +0 -1
  246. package/dist/workflows/migrate-sources.d.ts +0 -42
  247. package/dist/workflows/migrate-sources.js +0 -133
  248. package/dist/workflows/migrate-sources.js.map +0 -1
  249. package/docs/run-bundles.md +0 -481
  250. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  251. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  252. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  253. package/schemas/human-decision-request-v2.schema.json +0 -69
  254. package/schemas/human-decision-resolution-v2.schema.json +0 -27
  255. package/src/builtins/sanity-check-session.ts +0 -205
  256. package/src/workflows/artifacts.ts +0 -188
  257. package/src/workflows/migrate-sources.ts +0 -178
@@ -1,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,
@@ -18,6 +13,7 @@ import { compositionMetadata } from "../workflows/composition.js";
18
13
  import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
19
14
  import { WorkflowEngine } from "../workflows/engine.js";
20
15
  import {
16
+ BuiltinWorkflowRevisionChangedError,
21
17
  ClaimLostError,
22
18
  errorMessage,
23
19
  isClaimLostError,
@@ -29,18 +25,17 @@ import {
29
25
  validateHumanDecisionResponse,
30
26
  } from "../workflows/human-decision.js";
31
27
  import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
32
- import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
33
28
  import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/progress.js";
34
29
  import {
35
30
  createRunId,
36
- listRunBundles,
37
- readLastTraceEvent,
38
- readRunBundle,
31
+ listWorkflowRuns,
32
+ readWorkflowRun,
39
33
  WorkflowRunStore,
40
34
  createDefinitionSnapshot,
41
35
  } from "../workflows/store.js";
42
36
  import type {
43
37
  AcceptedHumanDecision,
38
+ ResolvedHumanDecision,
44
39
  AgentStepContract,
45
40
  HumanDecisionRequest,
46
41
  HumanDecisionSubmission,
@@ -48,6 +43,7 @@ import type {
48
43
  WorkflowDefinitionSnapshot,
49
44
  WorkflowRunResult,
50
45
  WorkflowRunState,
46
+ WorkflowSource,
51
47
  WorkflowUpdateRecord,
52
48
  } from "../workflows/types.js";
53
49
  import {
@@ -93,6 +89,7 @@ import {
93
89
  } from "./herdr-viewer.js";
94
90
  import { SessionRecorder } from "./recorder.js";
95
91
  import {
92
+ recoverAssistantStep,
96
93
  registerWorkflowAgentStepMessageRenderer,
97
94
  WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
98
95
  WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
@@ -194,19 +191,14 @@ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
194
191
  value !== null && typeof value === "object"
195
192
  ? (value as { schema?: unknown }).schema
196
193
  : undefined;
197
- if (
198
- schema !== "pi-workflows.human-decision-request.v1" &&
199
- schema !== "pi-workflows.human-decision-request.v2"
200
- ) {
201
- return null;
202
- }
194
+ if (schema !== "pi-workflows.human-decision-request.v1") return null;
203
195
  return value as HumanDecisionRequest;
204
196
  }
205
197
 
206
198
  type PreparedLaunchOptions = {
207
199
  presentation?: boolean;
208
200
  parentRunId?: string;
209
- humanDecision?: AcceptedHumanDecision;
201
+ humanDecision?: ResolvedHumanDecision;
210
202
  };
211
203
 
212
204
  type StartRunOptions = {
@@ -218,8 +210,8 @@ type StartRunOptions = {
218
210
  signal?: AbortSignal;
219
211
  /** Continue a checkpointed run: input becomes the legacy answer payload. */
220
212
  parentRunId?: string;
221
- /** Accepted verified-human answer for a protected decision continuation. */
222
- humanDecision?: AcceptedHumanDecision;
213
+ /** Durable human or timeout response for a protected decision continuation. */
214
+ humanDecision?: ResolvedHumanDecision;
223
215
  /** Resume a parked run at its stopped node instead of starting fresh. */
224
216
  resume?: boolean;
225
217
  /** An existing queue claim token, when the caller already claimed the run. */
@@ -237,6 +229,29 @@ function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unkn
237
229
  };
238
230
  }
239
231
 
232
+ function continuationSourceChangedError(
233
+ runId: string,
234
+ previous: WorkflowSource,
235
+ current: WorkflowSource,
236
+ ): Error {
237
+ if (
238
+ previous.kind === "builtin" &&
239
+ current.kind === "builtin" &&
240
+ previous.id === current.id &&
241
+ previous.revision !== current.revision
242
+ ) {
243
+ return new BuiltinWorkflowRevisionChangedError({
244
+ runId,
245
+ workflowId: current.id,
246
+ previousRevision: previous.revision,
247
+ currentRevision: current.revision,
248
+ });
249
+ }
250
+ return new Error(
251
+ `Workflow source changed since run ${runId} started; revert the edit to answer its checkpoint`,
252
+ );
253
+ }
254
+
240
255
  function preparedLaunchOptions(options: StartRunOptions): PreparedLaunchOptions {
241
256
  return {
242
257
  ...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
@@ -258,7 +273,7 @@ function safeLaunchError(error: unknown): { code: string; message: string } {
258
273
  .replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
259
274
  .replaceAll("\n", " ")
260
275
  .trim();
261
- 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)
262
277
  ? "workflow_not_found"
263
278
  : /source changed|source mismatch/iu.test(raw)
264
279
  ? "source_changed"
@@ -415,11 +430,29 @@ export default function piWorkflows(pi: ExtensionAPI) {
415
430
  ...[...telegramDecisionChannels.values()].map(async (channel) => channel.settle(decision)),
416
431
  ]);
417
432
  };
418
- const migrationBlockedRuns = new Set<string>();
419
433
  const ensureRunQueueStore = (cwd: string): SqliteControllerStore => {
420
- runQueueStore ??= new SqliteControllerStore(projectControllerStorePath(cwd));
434
+ runQueueStore ??= new SqliteControllerStore(undefined, { projectPath: cwd });
421
435
  return runQueueStore;
422
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
+ };
423
456
 
424
457
  // Session-addressed delivery: each session polls only its durable outbox.
425
458
  // Run events remain an audit feed and never enter a conversation.
@@ -817,7 +850,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
817
850
  workflowViewTarget = {
818
851
  runId: state.runId,
819
852
  workflowName: state.workflowName,
820
- runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
821
853
  };
822
854
  renderWidget(ctx);
823
855
  };
@@ -1027,12 +1059,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
1027
1059
  }
1028
1060
  try {
1029
1061
  if (outcome === "park") {
1030
- runQueueStore.parkWorkflowRun({ runId: run.runId, claimToken: run.claimToken });
1031
- } else {
1032
- 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");
1033
1069
  }
1034
1070
  } catch {
1035
- // Queue bookkeeping is best-effort next to the durable bundle.
1071
+ // Queue bookkeeping is best-effort next to the durable run fact.
1036
1072
  } finally {
1037
1073
  run.claimToken = undefined;
1038
1074
  }
@@ -1047,7 +1083,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1047
1083
  activeRun = null;
1048
1084
  }
1049
1085
  if (result.state.status === "running") {
1050
- // The run was parked for another runner; its bundle stays resumable
1086
+ // The run was parked for another runner; its SQLite state stays resumable
1051
1087
  // and its queue row becomes claimable. The recorder drains first: its
1052
1088
  // finalization writes share the fenced store, so the claim must stay
1053
1089
  // valid until capture is complete. Nothing else to present.
@@ -1074,7 +1110,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1074
1110
  );
1075
1111
  return;
1076
1112
  }
1077
- releaseClaim(run, "done");
1113
+ releaseClaim(run, result.state.status === "waiting" ? "park" : "done");
1078
1114
  recordRunEvent({
1079
1115
  runId: run.runId,
1080
1116
  workflowRef: run.workflowName,
@@ -1202,7 +1238,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1202
1238
  // refused continuation (edited source, missing parent) must not consume
1203
1239
  // the parent's one-continuation slot.
1204
1240
  if (options.parentRunId !== undefined) {
1205
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
1241
+ const parent = readWorkflowRun(options.parentRunId);
1206
1242
  if (parent === null || parent.state.status !== "waiting") {
1207
1243
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
1208
1244
  }
@@ -1210,8 +1246,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
1210
1246
  parent.state.workflowSource !== undefined &&
1211
1247
  !isDeepStrictEqual(parent.state.workflowSource, workflowSource)
1212
1248
  ) {
1213
- throw new Error(
1214
- `Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`,
1249
+ throw continuationSourceChangedError(
1250
+ options.parentRunId,
1251
+ parent.state.workflowSource,
1252
+ workflowSource,
1215
1253
  );
1216
1254
  }
1217
1255
  }
@@ -1235,6 +1273,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1235
1273
  : workflowSource.path,
1236
1274
  workflowSource: launchSourceIdentity(workflow, workflowSource),
1237
1275
  definitionDigest: definitionDigest(snapshot),
1276
+ definitionSnapshot: snapshot,
1238
1277
  input,
1239
1278
  launchOptions: preparedLaunchOptions(options),
1240
1279
  runnerId,
@@ -1316,16 +1355,20 @@ export default function piWorkflows(pi: ExtensionAPI) {
1316
1355
  },
1317
1356
  conversation: {
1318
1357
  beginAttempt: (contract) => run.recorder?.beginAttempt(contract),
1358
+ recoverAssistant: (contract) =>
1359
+ recoverAssistantStep(ctx.sessionManager.getBranch(), contract),
1319
1360
  mark: () => run.recorder?.mark() ?? 0,
1320
1361
  rangeSince: (mark) => run.recorder?.rangeSince(mark),
1321
1362
  },
1322
1363
  });
1323
- // The store is shared between the engine and the session recorder so the
1324
- // trace sequence stays single-writer (see docs/run-bundles.md). Queued
1325
- // runs are fenced: every write proves the claim is still ours first.
1326
- const store = new WorkflowRunStore(
1327
- undefined,
1328
- 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
+ },
1329
1372
  );
1330
1373
  const engine = new WorkflowEngine({
1331
1374
  executor,
@@ -1350,8 +1393,8 @@ export default function piWorkflows(pi: ExtensionAPI) {
1350
1393
  },
1351
1394
  // Awaited by the engine after run_started is persisted, so the session
1352
1395
  // binding and its trace event always precede node and terminal events.
1353
- onRunStarted: async (runDir, state) => {
1354
- const recorder = new SessionRecorder(store, runDir, state.runId);
1396
+ onRunStarted: async (runId) => {
1397
+ const recorder = new SessionRecorder(store, runId);
1355
1398
  try {
1356
1399
  await recorder.bind(ctx);
1357
1400
  run.recorder = recorder;
@@ -1451,23 +1494,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
1451
1494
  // once the cause is fixed, and say so in the feed.
1452
1495
  let resumeFailed = run.resume === true && !isClaimLostError(error);
1453
1496
  let terminalStatus: string | undefined;
1454
- // Re-parking only makes sense while the bundle is still resumable.
1455
- // A terminal or waiting bundle closes the queue row instead, or
1456
- // every session start would retry it forever — and the feed reports
1457
- // 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.
1458
1500
  if (resumeFailed) {
1459
1501
  try {
1460
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1502
+ const bundle = readWorkflowRun(runId);
1461
1503
  if (bundle === null || bundle.state.status !== "running") {
1462
1504
  resumeFailed = false;
1463
1505
  terminalStatus = bundle?.state.status;
1464
1506
  }
1465
1507
  } catch {
1466
- // Unreadable bundles close the row too.
1508
+ // Unreadable run state closes the row too.
1467
1509
  resumeFailed = false;
1468
1510
  }
1469
1511
  }
1470
- // A continuation that failed before its bundle exists frees the
1512
+ // A continuation that failed before its run row exists frees the
1471
1513
  // parent's continuation slot instead of consuming it forever.
1472
1514
  let continuationSlotFreed = false;
1473
1515
  if (
@@ -1476,7 +1518,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1476
1518
  !isClaimLostError(error)
1477
1519
  ) {
1478
1520
  try {
1479
- const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
1521
+ const bundle = readWorkflowRun(runId);
1480
1522
  if (bundle === null) {
1481
1523
  continuationSlotFreed =
1482
1524
  runQueueStore?.deleteWorkflowRun({ runId, claimToken: run.claimToken }) === true;
@@ -1554,7 +1596,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1554
1596
  runnerId,
1555
1597
  claimToken,
1556
1598
  leaseMs: RUN_CLAIM_LEASE_MS,
1557
- excludeRunIds: [...migrationBlockedRuns],
1599
+ excludeRunIds: [],
1558
1600
  sessionId: ctx.sessionManager.getSessionId(),
1559
1601
  });
1560
1602
  if (claimed === undefined) {
@@ -1562,7 +1604,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
1562
1604
  }
1563
1605
  let started: string | undefined;
1564
1606
  try {
1565
- 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
+ }
1566
1612
  const sourceRef =
1567
1613
  bundle?.state.workflowSource === undefined
1568
1614
  ? claimed.workflowSourceRef
@@ -1572,7 +1618,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1572
1618
  const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
1573
1619
  started = await startRun(ctx, sourceRef, claimed.input, {
1574
1620
  ...launchOptions,
1575
- resume: bundle !== null,
1621
+ resume: claimed.initialized,
1576
1622
  runId: claimed.runId,
1577
1623
  claimToken,
1578
1624
  });
@@ -1600,16 +1646,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
1600
1646
  ? { state: "running", runId: activeRun.runId }
1601
1647
  : workflowSchedulerResult(activeRun.lastState);
1602
1648
  }
1603
- const store = new WorkflowRunStore();
1604
- const bundle = await readRunBundle(store.runDirFor(request.runId));
1649
+ const store = workflowStore(controllerContext?.cwd ?? process.cwd());
1650
+ const bundle = store.readRun(request.runId);
1605
1651
  if (bundle !== null) {
1606
1652
  const recovered =
1607
1653
  bundle.state.status === "running" ? await store.markRunInterrupted(request.runId) : bundle;
1608
1654
  if (recovered !== null) {
1609
- const lastTraceEvent = await readLastTraceEvent(
1610
- recovered.runDir,
1611
- recovered.manifest.paths.trace,
1612
- );
1655
+ const lastTraceEvent = recovered.traceEvents?.at(-1) ?? null;
1613
1656
  return workflowSchedulerResult(recovered.state, lastTraceEvent?.type === "run_interrupted");
1614
1657
  }
1615
1658
  return { state: "pending" };
@@ -1617,7 +1660,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
1617
1660
  if (controllerContext === null) {
1618
1661
  return { state: "pending" };
1619
1662
  }
1620
- await store.quarantineIncompleteRun(request.runId);
1621
1663
  const runId = await startRun(controllerContext, request.workflow, request.input, {
1622
1664
  runId: request.runId,
1623
1665
  childKey,
@@ -1723,11 +1765,17 @@ export default function piWorkflows(pi: ExtensionAPI) {
1723
1765
  notify(ctx, result.message, result.level);
1724
1766
  };
1725
1767
 
1726
- const cancelHumanDecision = async (request: HumanDecisionRequest): Promise<void> => {
1727
- 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);
1728
1773
  await store.cancel(request, "cancelled");
1729
1774
  const cancellation = await store.readCancellation(request.decisionId);
1730
- if (cancellation !== null) await settleHumanDecisionChannels(cancellation);
1775
+ if (cancellation !== null) {
1776
+ await settleHumanDecisionChannels(cancellation);
1777
+ store.markEffectApplied(request.decisionId, "decision.settle_presentations");
1778
+ }
1731
1779
  lastWaitingRunId = null;
1732
1780
  };
1733
1781
 
@@ -1744,7 +1792,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1744
1792
  const continued = new Set(
1745
1793
  rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
1746
1794
  );
1747
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
1795
+ const bundles = listWorkflowRuns();
1748
1796
  for (const bundle of bundles) {
1749
1797
  if (
1750
1798
  bundle.state.status !== "waiting" ||
@@ -1810,7 +1858,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1810
1858
  const { state } = widgetSource;
1811
1859
  const request = humanDecisionRequest(state.finalOutput);
1812
1860
  if (state.status === "waiting" && request !== null) {
1813
- await cancelHumanDecision(request);
1861
+ await cancelHumanDecision(ctx, request);
1814
1862
  clearWidgetTimer();
1815
1863
  clearWidget(ctx);
1816
1864
  return {
@@ -1831,7 +1879,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1831
1879
  }
1832
1880
  const recovered = await findOwnedWaitingHumanDecision(ctx);
1833
1881
  if (recovered !== null) {
1834
- await cancelHumanDecision(recovered.request);
1882
+ await cancelHumanDecision(ctx, recovered.request);
1835
1883
  return {
1836
1884
  message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
1837
1885
  details: {
@@ -1936,9 +1984,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
1936
1984
  runId?: string,
1937
1985
  ): Promise<WorkflowControlResult> => {
1938
1986
  if (runId !== undefined) {
1939
- 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);
1940
1995
  if (bundle === null) {
1941
- const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
1942
1996
  if (launch === undefined) throw new Error(`Workflow run not found: ${runId}`);
1943
1997
  return workflowLaunchStatus(launch);
1944
1998
  }
@@ -1985,7 +2039,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
1985
2039
  const continued = new Set(
1986
2040
  rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
1987
2041
  );
1988
- const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
2042
+ const bundles = listWorkflowRuns();
1989
2043
  parentRunId =
1990
2044
  bundles.find(
1991
2045
  (bundle) =>
@@ -2006,7 +2060,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2006
2060
  ) {
2007
2061
  throw new Error(`Workflow run ${parentRunId} belongs to another Pi session.`);
2008
2062
  }
2009
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(parentRunId));
2063
+ const parent = readWorkflowRun(parentRunId);
2010
2064
  if (
2011
2065
  parent === null ||
2012
2066
  parent.state.status !== "waiting" ||
@@ -2033,7 +2087,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2033
2087
  verified?: HumanDecisionChannelAnswer,
2034
2088
  ): Promise<WorkflowControlResult> => {
2035
2089
  const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
2036
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2090
+ const parent = readWorkflowRun(waiting.parentRunId);
2037
2091
  if (parent === null) throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
2038
2092
  const request = humanDecisionRequest(parent.state.finalOutput);
2039
2093
  let accepted: AcceptedHumanDecision | undefined;
@@ -2059,9 +2113,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
2059
2113
  },
2060
2114
  idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
2061
2115
  };
2062
- const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
2116
+ const store = humanDecisionStore(ctx.cwd);
2063
2117
  const acceptance = await store.accept(request, submission);
2064
- if (acceptance.status === "conflict") {
2118
+ if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
2065
2119
  throw new Error("That human decision was already answered differently.");
2066
2120
  }
2067
2121
  accepted = acceptance.decision;
@@ -2087,21 +2141,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
2087
2141
  };
2088
2142
  }
2089
2143
  }
2090
- const decisionStore = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
2091
- if (request !== null && continuationRunId !== undefined && accepted !== undefined) {
2092
- await decisionStore.recordContinuation(request.decisionId, {
2093
- schema: "pi-workflows.human-decision-continuation.v1",
2094
- decisionId: request.decisionId,
2095
- requestDigest: request.requestDigest,
2096
- parentRunId: waiting.parentRunId,
2097
- runId: continuationRunId,
2098
- createdAt: accepted.acceptedAt,
2099
- });
2100
- }
2144
+ const decisionStore = humanDecisionStore(ctx.cwd);
2101
2145
  if (continuationRunId !== undefined) {
2102
- const existingContinuation = await readRunBundle(
2103
- new WorkflowRunStore().runDirFor(continuationRunId),
2104
- );
2146
+ const existingContinuation = readWorkflowRun(continuationRunId);
2105
2147
  if (existingContinuation !== null) {
2106
2148
  if (accepted !== undefined) await settleHumanDecisionChannels(accepted);
2107
2149
  lastWaitingRunId = null;
@@ -2125,7 +2167,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
2125
2167
  throw new Error("Could not start the checkpoint continuation.");
2126
2168
  }
2127
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");
2128
2180
  await settleHumanDecisionChannels(accepted);
2181
+ decisionStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2129
2182
  }
2130
2183
  lastWaitingRunId = null;
2131
2184
  return {
@@ -2146,7 +2199,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2146
2199
  const channel = new PiDecisionChannel({
2147
2200
  actorId: ctx.sessionManager.getSessionId(),
2148
2201
  ui: ctx.ui,
2149
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
2202
+ store: humanDecisionStore(ctx.cwd),
2150
2203
  onAnswer: async (answer) => {
2151
2204
  const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
2152
2205
  notify(ctx, result.message, result.level);
@@ -2183,7 +2236,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2183
2236
  config: loaded.channels,
2184
2237
  credentials: loaded.credentials,
2185
2238
  configDir: loaded.configDir,
2186
- store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
2239
+ store: humanDecisionStore(ctx.cwd),
2187
2240
  onAnswer: async (answer) => {
2188
2241
  await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
2189
2242
  },
@@ -2197,13 +2250,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
2197
2250
  ctx: ExtensionContext,
2198
2251
  deliverPending = true,
2199
2252
  ): Promise<void> => {
2200
- const runStore = new WorkflowRunStore();
2201
- const store = new HumanDecisionStore(runStore.outputRoot);
2253
+ const runStore = workflowStore(ctx.cwd);
2254
+ const store = humanDecisionStore(ctx.cwd);
2202
2255
  const requests = (await store.listRequests()).sort((left, right) =>
2203
2256
  left.createdAt.localeCompare(right.createdAt),
2204
2257
  );
2205
2258
  for (const request of requests) {
2206
- const parent = await readRunBundle(runStore.runDirFor(request.runId));
2259
+ const parent = runStore.readRun(request.runId);
2207
2260
  if (parent === null || parent.state.status !== "waiting") continue;
2208
2261
  const currentRequest = humanDecisionRequest(parent.state.finalOutput);
2209
2262
  if (
@@ -2218,60 +2271,110 @@ export default function piWorkflows(pi: ExtensionAPI) {
2218
2271
  queueRecord !== undefined &&
2219
2272
  (queueRecord.originSessionId === null ||
2220
2273
  queueRecord.originSessionId === ctx.sessionManager.getSessionId());
2221
- const accepted = await store.readAccepted(request.decisionId);
2222
- if (accepted === null) {
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
+ );
2287
+ let resolved = await store.readResolved(request.decisionId);
2288
+ if (resolved === null) {
2223
2289
  let cancellation = await store.readCancellation(request.decisionId);
2224
- if (
2225
- cancellation === null &&
2226
- request.expiresAt !== undefined &&
2227
- Date.parse(request.expiresAt) <= Date.now()
2228
- ) {
2229
- await store.cancel(request, "expired");
2230
- cancellation = await store.readCancellation(request.decisionId);
2290
+ const expired =
2291
+ request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
2292
+ if (cancellation === null && expired) {
2293
+ if (request.defaultResponse === undefined) {
2294
+ await ownerStore.cancel(request, "expired");
2295
+ cancellation = await ownerStore.readCancellation(request.decisionId);
2296
+ } else {
2297
+ try {
2298
+ resolved = (await ownerStore.resolveTimeout(request)).decision;
2299
+ } catch {
2300
+ cancellation = await store.readCancellation(request.decisionId);
2301
+ resolved = await store.readResolved(request.decisionId);
2302
+ }
2303
+ }
2231
2304
  }
2232
2305
  if (cancellation !== null) {
2306
+ recoveryQueue.completeWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2307
+ ownerStore.markEffectApplied(request.decisionId, "decision.cancel_parent");
2233
2308
  await settleHumanDecisionChannels(cancellation);
2309
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2310
+ ownerStore.close();
2234
2311
  continue;
2235
2312
  }
2236
- if (!deliverPending || !ownedBySession) continue;
2237
- const channels = audienceChannels(decisionChannelConfig, request.audience);
2238
- for (const channelId of channels) {
2239
- const channel = telegramDecisionChannels.get(channelId);
2240
- if (channel !== undefined) await channel.deliver(humanDecisionChannelRequest(request));
2241
- }
2242
- if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
2243
- lastWaitingRunId = request.runId;
2244
- queueMicrotask(() => void promptHumanDecision(ctx, request));
2313
+ if (resolved === null) {
2314
+ if (!deliverPending) {
2315
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2316
+ ownerStore.close();
2317
+ continue;
2318
+ }
2319
+ const channels = audienceChannels(decisionChannelConfig, request.audience);
2320
+ for (const channelId of channels) {
2321
+ const channel = telegramDecisionChannels.get(channelId);
2322
+ if (channel !== undefined) await channel.deliver(humanDecisionChannelRequest(request));
2323
+ }
2324
+ if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
2325
+ lastWaitingRunId = request.runId;
2326
+ queueMicrotask(() => void promptHumanDecision(ctx, request));
2327
+ }
2328
+ recoveryQueue.parkWorkflowRun({ runId: request.runId, claimToken: recoveryToken });
2329
+ ownerStore.close();
2330
+ continue;
2245
2331
  }
2246
- continue;
2247
2332
  }
2248
2333
 
2249
- if (!ownedBySession) 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
+ }
2250
2340
  const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
2251
2341
  const continuation = (await store.readContinuation(request.decisionId)) ?? {
2252
2342
  schema: "pi-workflows.human-decision-continuation.v1" as const,
2253
2343
  decisionId: request.decisionId,
2254
2344
  requestDigest: request.requestDigest,
2345
+ provenance: resolved.provenance,
2255
2346
  parentRunId: request.runId,
2256
2347
  runId,
2257
- createdAt: accepted.acceptedAt,
2348
+ createdAt: resolved.acceptedAt,
2258
2349
  };
2259
- await store.recordContinuation(request.decisionId, continuation);
2260
- 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);
2261
2355
  if (existing === null && activeRun === null) {
2262
- if (parent.state.workflowSource === undefined) continue;
2356
+ if (currentParent.state.workflowSource === undefined) {
2357
+ ownerStore.close();
2358
+ continue;
2359
+ }
2263
2360
  const workflowRef =
2264
- parent.state.workflowSource.kind === "builtin"
2265
- ? `builtin:${parent.state.workflowSource.id}`
2266
- : parent.state.workflowSource.path;
2267
- await startRun(ctx, workflowRef, parent.state.input, {
2361
+ currentParent.state.workflowSource.kind === "builtin"
2362
+ ? `builtin:${currentParent.state.workflowSource.id}`
2363
+ : currentParent.state.workflowSource.path;
2364
+ await startRun(ctx, workflowRef, currentParent.state.input, {
2268
2365
  parentRunId: request.runId,
2269
- humanDecision: accepted,
2366
+ humanDecision: resolved,
2270
2367
  runId: continuation.runId,
2271
2368
  quiet: true,
2272
2369
  });
2273
2370
  }
2274
- await settleHumanDecisionChannels(accepted);
2371
+ if (runStore.readRun(continuation.runId) !== null) {
2372
+ await ownerStore.recordContinuation(request.decisionId, continuation);
2373
+ ownerStore.markEffectApplied(request.decisionId, "decision.continue");
2374
+ }
2375
+ await settleHumanDecisionChannels(resolved);
2376
+ ownerStore.markEffectApplied(request.decisionId, "decision.settle_presentations");
2377
+ ownerStore.close();
2275
2378
  if (activeRun !== null) break;
2276
2379
  }
2277
2380
  };
@@ -2332,7 +2435,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2332
2435
  }
2333
2436
  const queue = ensureRunQueueStore(ctx.cwd);
2334
2437
  const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
2335
- if (existing !== undefined) {
2438
+ if (existing !== undefined && existing.runId !== options.parentRunId) {
2336
2439
  throw new Error(
2337
2440
  `Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`,
2338
2441
  );
@@ -2344,7 +2447,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2344
2447
  const workflow = resolved.definition;
2345
2448
  const workflowSource = resolved.source;
2346
2449
  if (options.parentRunId !== undefined) {
2347
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
2450
+ const parent = readWorkflowRun(options.parentRunId);
2348
2451
  if (parent === null || parent.state.status !== "waiting") {
2349
2452
  throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
2350
2453
  }
@@ -2352,8 +2455,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
2352
2455
  parent.state.workflowSource !== undefined &&
2353
2456
  !isDeepStrictEqual(parent.state.workflowSource, workflowSource)
2354
2457
  ) {
2355
- throw new Error(
2356
- `Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`,
2458
+ throw continuationSourceChangedError(
2459
+ options.parentRunId,
2460
+ parent.state.workflowSource,
2461
+ workflowSource,
2357
2462
  );
2358
2463
  }
2359
2464
  }
@@ -2367,6 +2472,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2367
2472
  workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
2368
2473
  workflowSource: launchSourceIdentity(workflow, workflowSource),
2369
2474
  definitionDigest: definitionDigest(snapshot),
2475
+ definitionSnapshot: snapshot,
2370
2476
  input,
2371
2477
  launchOptions: preparedLaunchOptions(options),
2372
2478
  runnerId,
@@ -2575,7 +2681,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2575
2681
  const message = errorMessage(error);
2576
2682
  notify(
2577
2683
  ctx,
2578
- /workflow_run_queue_parent/.test(message)
2684
+ /continuations\.parent_run_id/.test(message)
2579
2685
  ? "That checkpoint was already answered; see its continuation run."
2580
2686
  : `Could not continue workflow: ${message}`,
2581
2687
  "error",
@@ -2730,9 +2836,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
2730
2836
  label: "Workflow",
2731
2837
  description: [
2732
2838
  "List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
2733
- "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.",
2734
2841
  "Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
2735
- "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.",
2736
2843
  ].join(" "),
2737
2844
  parameters: WorkflowToolParameters,
2738
2845
  async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
@@ -2759,7 +2866,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2759
2866
  break;
2760
2867
  case "answer": {
2761
2868
  const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2762
- const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
2869
+ const parent = readWorkflowRun(waiting.parentRunId);
2763
2870
  if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2764
2871
  throw new Error(
2765
2872
  "This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
@@ -2797,7 +2904,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2797
2904
  }
2798
2905
  throw new Error("No workflow step is waiting for output.");
2799
2906
  }
2800
- // Flush the conversation into the bundle before accepting, so the
2907
+ // Flush the conversation into SQLite before accepting, so the
2801
2908
  // attempt range includes the assistant message carrying this call.
2802
2909
  await activeRun.recorder?.record(ctx).catch(() => undefined);
2803
2910
  await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
@@ -2844,24 +2951,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2844
2951
  sessionClosed = false;
2845
2952
  controllerContext = ctx;
2846
2953
  if (herdrEnabled) void refreshHerdrCapability(ctx);
2847
- try {
2848
- const queue = ensureRunQueueStore(ctx.cwd);
2849
- const migration = await migrateLegacyWorkflowSources({
2850
- catalog: builtinWorkflowCatalog,
2851
- queue,
2852
- });
2853
- migrationBlockedRuns.clear();
2854
- for (const blocked of migration.blocked) migrationBlockedRuns.add(blocked.runId);
2855
- if (migration.blocked.length > 0) {
2856
- notify(
2857
- ctx,
2858
- `Could not migrate ${migration.blocked.length} legacy workflow source(s).`,
2859
- "warning",
2860
- );
2861
- }
2862
- } catch (error) {
2863
- notify(ctx, `Could not migrate legacy workflow sources: ${errorMessage(error)}`, "warning");
2864
- }
2954
+ ensureRunQueueStore(ctx.cwd);
2865
2955
  try {
2866
2956
  await reloadDecisionChannels(ctx);
2867
2957
  } catch {
@@ -2953,6 +3043,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
2953
3043
 
2954
3044
  pi.on("turn_end", async (event, ctx) => {
2955
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);
2956
3049
  });
2957
3050
 
2958
3051
  pi.on("message_start", async (event, ctx) => {
@@ -3007,11 +3100,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
3007
3100
  return;
3008
3101
  }
3009
3102
  }
3010
- const flushedNatural = turnCoordinator.flushNatural(
3011
- ctx.isIdle() && !sessionClosed && !runHeld(),
3012
- );
3013
3103
  const run = activeRun;
3014
3104
  if (!run) {
3105
+ turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
3015
3106
  presentationPending = null;
3016
3107
  runSyncPass(ctx);
3017
3108
  return;
@@ -3019,7 +3110,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
3019
3110
  await run.recorder?.synchronize(ctx).catch(() => undefined);
3020
3111
  run.recorder?.settleAttempt();
3021
3112
  run.executor.setStreaming(false);
3022
- 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
+ }
3023
3123
  runSyncPass(ctx);
3024
3124
  });
3025
3125