@osolmaz/pi-workflows 0.14.0 → 0.15.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 (235) hide show
  1. package/README.md +78 -110
  2. package/dist/builtins/autoimplement.workflow.js +3 -1
  3. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  4. package/dist/builtins/change-verification.workflow.js +7 -2
  5. package/dist/builtins/change-verification.workflow.js.map +1 -1
  6. package/dist/builtins/metadata.d.ts +22 -0
  7. package/dist/builtins/metadata.js +10 -0
  8. package/dist/builtins/metadata.js.map +1 -0
  9. package/dist/builtins/monitor.workflow.js +4 -1
  10. package/dist/builtins/monitor.workflow.js.map +1 -1
  11. package/dist/builtins/sanity-check.workflow.js +4 -1
  12. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  13. package/dist/builtins/workspace-preparation.workflow.js +3 -1
  14. package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
  15. package/dist/controllers/index.d.ts +0 -1
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +51 -0
  19. package/dist/controllers/sqlite.js +433 -57
  20. package/dist/controllers/sqlite.js.map +1 -1
  21. package/dist/controllers/store.d.ts +1 -0
  22. package/dist/extension/controller-command.d.ts +22 -0
  23. package/dist/extension/controller-command.js +32 -0
  24. package/dist/extension/controller-command.js.map +1 -0
  25. package/dist/extension/index.d.ts +2 -9
  26. package/dist/extension/index.js +876 -3447
  27. package/dist/extension/index.js.map +1 -1
  28. package/dist/extension/message-card.d.ts +13 -0
  29. package/dist/extension/message-card.js +46 -0
  30. package/dist/extension/message-card.js.map +1 -0
  31. package/dist/extension/step-message.d.ts +5 -6
  32. package/dist/extension/step-message.js +41 -52
  33. package/dist/extension/step-message.js.map +1 -1
  34. package/dist/host/child-worker-supervisor.d.ts +50 -0
  35. package/dist/host/child-worker-supervisor.js +159 -0
  36. package/dist/host/child-worker-supervisor.js.map +1 -0
  37. package/dist/host/client.d.ts +48 -0
  38. package/dist/host/client.js +216 -0
  39. package/dist/host/client.js.map +1 -0
  40. package/dist/host/controller-worker-entry.d.ts +2 -0
  41. package/dist/host/controller-worker-entry.js +244 -0
  42. package/dist/host/controller-worker-entry.js.map +1 -0
  43. package/dist/host/controller-worker-protocol.d.ts +36 -0
  44. package/dist/host/controller-worker-protocol.js +49 -0
  45. package/dist/host/controller-worker-protocol.js.map +1 -0
  46. package/dist/host/controller-worker-supervisor.d.ts +21 -0
  47. package/dist/host/controller-worker-supervisor.js +54 -0
  48. package/dist/host/controller-worker-supervisor.js.map +1 -0
  49. package/dist/host/host-entry.d.ts +2 -0
  50. package/dist/host/host-entry.js +23 -0
  51. package/dist/host/host-entry.js.map +1 -0
  52. package/dist/host/processes.d.ts +17 -12
  53. package/dist/host/processes.js +154 -50
  54. package/dist/host/processes.js.map +1 -1
  55. package/dist/host/protocol.d.ts +38 -0
  56. package/dist/host/protocol.js +156 -0
  57. package/dist/host/protocol.js.map +1 -0
  58. package/dist/host/resolver-entry.d.ts +49 -0
  59. package/dist/host/resolver-entry.js +149 -0
  60. package/dist/host/resolver-entry.js.map +1 -0
  61. package/dist/host/rpc-executor.d.ts +14 -3
  62. package/dist/host/rpc-executor.js +63 -30
  63. package/dist/host/rpc-executor.js.map +1 -1
  64. package/dist/host/runner.d.ts +95 -29
  65. package/dist/host/runner.js +2311 -348
  66. package/dist/host/runner.js.map +1 -1
  67. package/dist/host/state.d.ts +174 -0
  68. package/dist/host/state.js +689 -0
  69. package/dist/host/state.js.map +1 -0
  70. package/dist/host/worker-entry.d.ts +10 -0
  71. package/dist/host/worker-entry.js +446 -0
  72. package/dist/host/worker-entry.js.map +1 -0
  73. package/dist/host/worker-protocol.d.ts +31 -0
  74. package/dist/host/worker-protocol.js +122 -0
  75. package/dist/host/worker-protocol.js.map +1 -0
  76. package/dist/host/worker-store.d.ts +80 -0
  77. package/dist/host/worker-store.js +91 -0
  78. package/dist/host/worker-store.js.map +1 -0
  79. package/dist/host/worker-supervisor.d.ts +22 -0
  80. package/dist/host/worker-supervisor.js +54 -0
  81. package/dist/host/worker-supervisor.js.map +1 -0
  82. package/dist/render/canvas.d.ts +2 -1
  83. package/dist/render/canvas.js +40 -15
  84. package/dist/render/canvas.js.map +1 -1
  85. package/dist/render/graph-render.d.ts +7 -3
  86. package/dist/render/graph-render.js +125 -74
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.js +3 -1
  89. package/dist/state/database.js.map +1 -1
  90. package/dist/state/index.d.ts +1 -0
  91. package/dist/state/index.js +1 -0
  92. package/dist/state/index.js.map +1 -1
  93. package/dist/state/schema.d.ts +1 -1
  94. package/dist/state/schema.js +171 -3
  95. package/dist/state/schema.js.map +1 -1
  96. package/dist/state/viewer.d.ts +46 -0
  97. package/dist/state/viewer.js +249 -0
  98. package/dist/state/viewer.js.map +1 -0
  99. package/dist/viewer/cli.d.ts +1 -1
  100. package/dist/viewer/cli.js +47 -18
  101. package/dist/viewer/cli.js.map +1 -1
  102. package/dist/workflows/composition.js +25 -2
  103. package/dist/workflows/composition.js.map +1 -1
  104. package/dist/workflows/definition.d.ts +3 -1
  105. package/dist/workflows/definition.js +25 -0
  106. package/dist/workflows/definition.js.map +1 -1
  107. package/dist/workflows/engine.d.ts +1 -0
  108. package/dist/workflows/engine.js +157 -42
  109. package/dist/workflows/engine.js.map +1 -1
  110. package/dist/workflows/errors.d.ts +3 -1
  111. package/dist/workflows/errors.js +4 -7
  112. package/dist/workflows/errors.js.map +1 -1
  113. package/dist/workflows/human-decision.d.ts +3 -0
  114. package/dist/workflows/human-decision.js +31 -0
  115. package/dist/workflows/human-decision.js.map +1 -1
  116. package/dist/workflows/index.d.ts +1 -1
  117. package/dist/workflows/index.js +1 -1
  118. package/dist/workflows/index.js.map +1 -1
  119. package/dist/workflows/schema.js +17 -6
  120. package/dist/workflows/schema.js.map +1 -1
  121. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  122. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  123. package/dist/workflows/session-reducer.js.map +1 -0
  124. package/dist/workflows/store.d.ts +87 -1
  125. package/dist/workflows/store.js +892 -93
  126. package/dist/workflows/store.js.map +1 -1
  127. package/dist/workflows/tool-input.d.ts +0 -26
  128. package/dist/workflows/tool-input.js +2 -50
  129. package/dist/workflows/tool-input.js.map +1 -1
  130. package/dist/workflows/types.d.ts +36 -4
  131. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  132. package/docs/CONTROLLERS.md +11 -11
  133. package/docs/DEFERRED_TURNS.md +23 -2
  134. package/docs/SQLITE_STATE.md +41 -31
  135. package/docs/WORKFLOW_HOST.md +452 -0
  136. package/docs/development.md +46 -30
  137. package/docs/live-replay-protocol.md +129 -100
  138. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +42 -0
  139. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  140. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  141. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  142. package/docs/tui-viewer.md +18 -3
  143. package/docs/workflows.md +162 -210
  144. package/examples/workflows/command-batch.workflow.ts +2 -0
  145. package/examples/workflows/shell.workflow.ts +2 -1
  146. package/herdr-plugin.toml +1 -1
  147. package/package.json +1 -1
  148. package/src/builtins/autoimplement.workflow.ts +3 -0
  149. package/src/builtins/change-verification.workflow.ts +7 -2
  150. package/src/builtins/metadata.ts +9 -0
  151. package/src/builtins/monitor.workflow.ts +4 -0
  152. package/src/builtins/sanity-check.workflow.ts +4 -0
  153. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  154. package/src/controllers/index.ts +0 -5
  155. package/src/controllers/sqlite.ts +709 -76
  156. package/src/controllers/store.ts +1 -0
  157. package/src/extension/controller-command.ts +45 -0
  158. package/src/extension/index.ts +944 -4163
  159. package/src/extension/message-card.ts +61 -0
  160. package/src/extension/step-message.ts +58 -63
  161. package/src/host/child-worker-supervisor.ts +183 -0
  162. package/src/host/client.ts +293 -0
  163. package/src/host/controller-worker-entry.ts +311 -0
  164. package/src/host/controller-worker-protocol.ts +104 -0
  165. package/src/host/controller-worker-supervisor.ts +79 -0
  166. package/src/host/host-entry.ts +23 -0
  167. package/src/host/processes.ts +171 -54
  168. package/src/host/protocol.ts +196 -0
  169. package/src/host/resolver-entry.ts +241 -0
  170. package/src/host/rpc-executor.ts +76 -34
  171. package/src/host/runner.ts +2813 -422
  172. package/src/host/state.ts +1160 -0
  173. package/src/host/worker-entry.ts +533 -0
  174. package/src/host/worker-protocol.ts +165 -0
  175. package/src/host/worker-store.ts +229 -0
  176. package/src/host/worker-supervisor.ts +74 -0
  177. package/src/render/canvas.ts +44 -10
  178. package/src/render/graph-render.ts +145 -90
  179. package/src/state/database.ts +2 -1
  180. package/src/state/index.ts +14 -0
  181. package/src/state/schema.ts +171 -3
  182. package/src/state/viewer.ts +356 -0
  183. package/src/viewer/cli.ts +49 -17
  184. package/src/workflows/composition.ts +36 -2
  185. package/src/workflows/definition.ts +32 -0
  186. package/src/workflows/engine.ts +157 -54
  187. package/src/workflows/errors.ts +11 -2
  188. package/src/workflows/human-decision.ts +49 -0
  189. package/src/workflows/index.ts +2 -0
  190. package/src/workflows/schema.ts +19 -6
  191. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  192. package/src/workflows/store.ts +1316 -108
  193. package/src/workflows/tool-input.ts +3 -69
  194. package/src/workflows/types.ts +32 -4
  195. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  196. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  197. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  198. package/dist/extension/controller-host.d.ts +0 -48
  199. package/dist/extension/controller-host.js +0 -110
  200. package/dist/extension/controller-host.js.map +0 -1
  201. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  202. package/dist/extension/deferred-turn-coordinator.js +0 -143
  203. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  204. package/dist/extension/deferred-turn.d.ts +0 -44
  205. package/dist/extension/deferred-turn.js +0 -110
  206. package/dist/extension/deferred-turn.js.map +0 -1
  207. package/dist/extension/executor.d.ts +0 -86
  208. package/dist/extension/executor.js +0 -311
  209. package/dist/extension/executor.js.map +0 -1
  210. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  211. package/dist/extension/follow-up-coordinator.js +0 -131
  212. package/dist/extension/follow-up-coordinator.js.map +0 -1
  213. package/dist/extension/recorder.d.ts +0 -84
  214. package/dist/extension/recorder.js +0 -528
  215. package/dist/extension/recorder.js.map +0 -1
  216. package/dist/extension/restart-policy.d.ts +0 -38
  217. package/dist/extension/restart-policy.js +0 -116
  218. package/dist/extension/restart-policy.js.map +0 -1
  219. package/dist/extension/session-events.d.ts +0 -133
  220. package/dist/extension/session-events.js +0 -61
  221. package/dist/extension/session-events.js.map +0 -1
  222. package/dist/extension/terminal-decision.d.ts +0 -51
  223. package/dist/extension/terminal-decision.js +0 -110
  224. package/dist/extension/terminal-decision.js.map +0 -1
  225. package/dist/viewer/session-reducer.js.map +0 -1
  226. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  227. package/src/extension/controller-host.ts +0 -167
  228. package/src/extension/deferred-turn-coordinator.ts +0 -171
  229. package/src/extension/deferred-turn.ts +0 -166
  230. package/src/extension/executor.ts +0 -411
  231. package/src/extension/follow-up-coordinator.ts +0 -151
  232. package/src/extension/recorder.ts +0 -655
  233. package/src/extension/restart-policy.ts +0 -163
  234. package/src/extension/session-events.ts +0 -121
  235. package/src/extension/terminal-decision.ts +0 -172
@@ -1,5 +1,11 @@
1
1
  # PIW viewer experience implementation plan
2
2
 
3
+ This plan records the earlier viewer experience work. Its graph-wide card sizing and
4
+ complete-run loading decisions are superseded by the
5
+ [incremental and virtualized viewer plan](2026-08-28-piw-incremental-viewer-plan.md).
6
+ The remaining sections are kept as implementation history and must not be used as the
7
+ target architecture for new viewer work.
8
+
3
9
  ## Objective
4
10
 
5
11
  Make `piw` a state-rich, terminal-native workflow viewer with Catppuccin as
@@ -46,7 +52,11 @@ Adopt ACPX ideas that improve inspection and state comprehension:
46
52
  Do not adopt ELK, React Flow, browser card decoration, smooth zoom, animated
47
53
  edges, multiple ACP-session concepts, or character-by-character replay.
48
54
 
49
- ### Stable full node cards
55
+ ### Stable full node cards (superseded)
56
+
57
+ The graph-wide sizing rule in this section is historical. The selected replacement gives
58
+ each node bounded adaptive dimensions and keeps the complete logical graph outside the
59
+ materialized viewport.
50
60
 
51
61
  Box mode is the canonical graph presentation. Each node is a full card with a
52
62
  border, an interior surface, and enough padding to keep its outer dimensions
@@ -1,5 +1,10 @@
1
1
  # Rust TUI viewer — implementation plan
2
2
 
3
+ This is the historical plan for the first Rust viewer and its former run-bundle reader.
4
+ The selected replacement is the
5
+ [incremental and virtualized viewer plan](2026-08-28-piw-incremental-viewer-plan.md).
6
+ Use that plan for state revisions, paging, multiple clients, and graph sizing.
7
+
3
8
  Goal: `piw`, a Rust TUI in `tui/` for live viewing and replaying workflow
4
9
  runs, per [tui-viewer.md](../tui-viewer.md) and
5
10
  [live-replay-protocol.md](../live-replay-protocol.md).
@@ -5,6 +5,16 @@ the same graph as the bundled TypeScript viewer, pinned by the golden fixtures
5
5
  under `fixtures/layout/`, and adds live following, replay, detailed inspection,
6
6
  a recorded Pi conversation, themes, and remote viewing.
7
7
 
8
+ ## Incremental projection
9
+
10
+ The viewer uses the [incremental and virtualized viewer design](plans/2026-08-28-piw-incremental-viewer-plan.md).
11
+
12
+ The run browser reads small metadata rows. It does not load trace, step, session, settings, or follow-up payloads. The local viewer checks SQLite `data_version` on its timer. When the value is unchanged, the timer does not scan runs or read payloads.
13
+
14
+ The selected run contains bounded pages. Step, trace, session-entry, and session-event pages contain at most 256 rows. Replay can jump to any position. The viewer loads the page that contains that position and keeps only the current windows. A compact graph projection keeps the latest attempt for each node and the taken transitions up to the replay point.
15
+
16
+ Local page reads run outside input and drawing through one overwrite-only request slot. A newer selection replaces pending work. A failed first read leaves the run browser usable. A failed refresh keeps the last good view and marks it stale.
17
+
8
18
  ## Install
9
19
 
10
20
  The crates.io package uses the project name and installs the shorter `piw`
@@ -44,6 +54,10 @@ show the shortcut.
44
54
 
45
55
  ## Layout
46
56
 
57
+ Boxed graph cards use only their own content. Their outer width is 24 through 32 cells. Their height is 7 through 10 rows. A switch with more than three branches shows its first two branch names and a `+N branches` row. Edge labels and the inspector keep the complete branch information.
58
+
59
+ Ranks use the tallest card in that rank. Cards are top-aligned. Edge ports, clipping, centering, keyboard movement, and mouse hits use each card's exact bounds. The server sends one retained language-neutral graph scene per watched run. The TUI reuses that scene across status changes and turns only viewport rows and columns into Ratatui spans.
60
+
47
61
  The normal layout contains a run browser, graph, inspector, and two-line replay
48
62
  timeline. Short terminals use a compact one-line transport. Terminals below 100
49
63
  columns start with the run browser collapsed to a status rail; `b` toggles it.
@@ -195,7 +209,9 @@ at the bottom until the user moves to an older message and returns with End.
195
209
  The client reconnects automatically with bounded backoff. It restores the run
196
210
  listing and selected-run subscription after the server returns. Cached content
197
211
  stays visible but is labeled reconnecting or disconnected, never current.
198
- Revision gaps still force a fresh snapshot.
212
+ Revision gaps force a bounded snapshot.
213
+
214
+ `piw serve` keeps one loaded projection and one graph scene for each watched run. The first watcher loads it. Later watchers reuse it. The last unwatch or disconnect releases it. Clients keep separate revision and page cursors, so one client's replay jump does not move another client. A lagged client gets a bounded snapshot instead of an unbounded patch backlog.
199
215
 
200
216
  Expanded prompt and output fields come from content-addressed SQLite blobs.
201
217
  The local reader uses query-only mode. Remote snapshots carry the same bounded
@@ -214,8 +230,7 @@ semantic view and do not expose a filesystem path.
214
230
  - Browser: `b` expands or collapses it. Up/Down or `j`/`k` selects a run. Drag
215
231
  its right border to resize it.
216
232
  - Inspector: Enter expands the selected step, trace payload, or conversation
217
- entry. In Trace, `v` changes scope. Page Up/Down scrolls long content. Drag
218
- its top border to resize the bottom panel.
233
+ entry. In Trace, `v` changes scope. In Info, `<` and `>` load the previous or next settings, follow-up, and current-update pages. Page Up/Down scrolls long content. Drag its top border to resize the bottom panel.
219
234
  - Theme: `,` opens the picker; arrows or `j`/`k` preview. Click `[✓ Apply]` or
220
235
  `[× Cancel]`; Enter and Escape remain the keyboard equivalents.
221
236
  - `q` or Ctrl-C quits.
package/docs/workflows.md CHANGED
@@ -19,14 +19,12 @@ Pi Workflows includes built-in `plain-summary`, `autoplan`, `autodoc`,
19
19
  `autoimplement`, `plan-approval`, `sanity-check`, and `monitor` workflows. `autoplan` is the current name for the
20
20
  planning workflow that was first released as `autodevise`; the old command and
21
21
  export are not retained. A project or global file named `monitor.workflow.ts`
22
- replaces the built-in monitor. The package registers each built-in in
23
- a process-local catalog with a stable reference such as `builtin:monitor` and
24
- an explicit revision. Built-ins are imported with the engine when a Pi process
25
- starts. They are not read from the package directory when a run starts or
26
- resumes. Updating the package on disk cannot mix a new built-in with that
27
- process's old engine; reload or restart Pi to use the new built-in. A revision
28
- mismatch refuses resume. Project and global workflow files still reload on
29
- each run and use their path and SHA-256 hash as their source identity.
22
+ replaces the built-in monitor. Each built-in has a stable reference such as
23
+ `builtin:monitor` and an explicit revision. A resolver child snapshots the
24
+ selected built-in before start, and each run worker verifies that identity
25
+ before execution. A revision mismatch refuses resume. Project and global
26
+ workflow files also use their absolute path and SHA-256 hash as source
27
+ identity.
30
28
 
31
29
  The workflow's command name is the file stem, so `.pi/workflows/triage.workflow.ts`
32
30
  runs as `/workflow triage`. A direct path also works: `/workflow ./somewhere/x.workflow.ts`.
@@ -40,7 +38,6 @@ import { agent, compute, defineWorkflow } from "@osolmaz/pi-workflows";
40
38
  export default defineWorkflow({
41
39
  name: "example",
42
40
  title: ({ input }) => `example: ${(input as { task?: string }).task}`,
43
- presentationPrompt: "Present the final answer clearly and concisely.",
44
41
  startAt: "ask",
45
42
  maxSteps: 50,
46
43
  nodes: {
@@ -56,20 +53,19 @@ export default defineWorkflow({
56
53
 
57
54
  Top-level fields:
58
55
 
59
- | Field | Type | Notes |
60
- | -------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
61
- | `name` | `string` | Required. Used in run ids and the step contract. `answer`, `cancel`, `list`, `pause`, `resume`, and `status` are reserved for `/workflow` subcommands. |
62
- | `source` | `string` | Optional `import.meta.url` for exact provenance when another TypeScript workflow imports this definition directly. |
63
- | `contractId` | `string` | Optional stable input-and-exit contract identity. Dynamic overrides must match it. |
64
- | `input` | `function` | Optional runtime input normalizer and validator. Its return type is the workflow input type. |
65
- | `title` | `string` or function | Optional run title, resolved once at start from `{ input, workflowName }`. Async resolution is bounded (30s) and cancellable. |
66
- | `presentationPrompt` | `string` or function | Optional instructions for a normal assistant response after the run. A function receives `{ state, finalOutput, signal }` and may return a prompt or `undefined`. See [Result presentation](#result-presentation). |
67
- | `startAt` | `string` | Required. Id of the first node. |
68
- | `nodes` | `Record<string, node>` | Required, non-empty. Node ids must match `[A-Za-z_][A-Za-z0-9_-]*`. |
69
- | `includes` | `Record<string, include>` | Optional imported or dynamically resolved child workflows. |
70
- | `exits` | `Record<string, exit>` | Optional named successful terminal nodes used when another workflow includes this workflow. |
71
- | `edges` | `WorkflowEdge[]` | Required. See routing below. |
72
- | `maxSteps` | `number` | Optional loop bound, default 100. The run fails when exceeded. |
56
+ | Field | Type | Notes |
57
+ | ------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
58
+ | `name` | `string` | Required. Used in run ids and the step contract. `answer`, `cancel`, `list`, `pause`, `resume`, and `status` are reserved for `/workflow` subcommands. |
59
+ | `source` | `string` | Optional `import.meta.url` for exact provenance when another TypeScript workflow imports this definition directly. |
60
+ | `contractId` | `string` | Optional stable input-and-exit contract identity. Dynamic overrides must match it. |
61
+ | `input` | `function` | Optional runtime input normalizer and validator. Its return type is the workflow input type. |
62
+ | `title` | `string` or function | Optional run title, resolved once at start from `{ input, workflowName }`. Async resolution is bounded (30s) and cancellable. |
63
+ | `startAt` | `string` | Required. Id of the first node. |
64
+ | `nodes` | `Record<string, node>` | Required, non-empty. Node ids must match `[A-Za-z_][A-Za-z0-9_-]*`. |
65
+ | `includes` | `Record<string, include>` | Optional imported or dynamically resolved child workflows. |
66
+ | `exits` | `Record<string, exit>` | Optional named successful terminal nodes used when another workflow includes this workflow. |
67
+ | `edges` | `WorkflowEdge[]` | Required. See routing below. |
68
+ | `maxSteps` | `number` | Optional loop bound, default 100. The run fails when exceeded. |
73
69
 
74
70
  `defineWorkflow` validates the shape eagerly (node ids, edge shapes, function
75
71
  fields) and validates the graph (unknown targets, duplicate outgoing edges,
@@ -112,36 +108,51 @@ Function actions receive `WorkflowActionContext`, which adds
112
108
 
113
109
  ## Durable runs, parking, and resume
114
110
 
115
- Every interactive `/workflow` run is tracked in the project run queue (see
116
- [CONTROLLERS.md](CONTROLLERS.md) for the store). The session that starts a run
117
- claims it and owns it while it executes; every owner-only SQLite write proves the claim
118
- first (write fencing).
111
+ Every run enters one global SQLite queue. One package-owned host claims runs,
112
+ renews live claims, commits state, and supervises one child process for each
113
+ active run generation. The extension is a local host client. It does not run
114
+ the workflow engine or workflow definitions.
119
115
 
120
- Closing the Pi session mid-run no longer cancels the run. The engine **parks**:
121
- it stops without a terminal event, releases the claim, and leaves the run
122
- resumable. When a runner is available again (a reopened Pi session or the
123
- standalone host), the run **resumes** at the node it stopped on. Completed
124
- nodes replay from the recorded state; only the interrupted node and everything
125
- downstream rerun. Resume repairs a torn trace tail, drops trace events the
126
- state projection never recorded, and refuses to continue if the workflow
127
- source changed since the run started (a forced resume records the mismatch).
128
-
129
- The standalone host runs without any Pi session:
116
+ The host starts on demand when a Pi or CLI client needs it. These commands
117
+ control the same user-level host for all projects:
130
118
 
131
119
  ```bash
132
- pi-workflows host --project /path/to/project
120
+ pi-workflows host start
121
+ pi-workflows host status
122
+ pi-workflows host stop
123
+ pi-workflows host run
133
124
  ```
134
125
 
135
- The host claims parked runs, resumes them, and reconciles durable controllers.
136
- Agent nodes that submit through the workflow tool execute in headless
137
- `pi --mode rpc` children that load a small bridge extension. An agent node with
138
- `expectedOutput: assistantMessage()` parks before prompting and waits for the
139
- origin Pi session because its result must be a visible assistant message. A
140
- detached run without an origin session fails clearly. The host is a foreground process: start it in a terminal and
141
- stop it with Ctrl-C. A second host for the same project refuses to start, and
142
- a host that dies has its orphaned children reaped by the next one. While the host works, reports enter a durable outbox addressed to the Pi
143
- session that started the run. They remain pending while that session is closed
144
- and never enter another conversation in the same project.
126
+ `host run` stays attached. The other commands start, inspect, or stop the
127
+ on-demand process. No command installs an operating-system service.
128
+
129
+ A worker verifies the root and all mounted source identities before it loads
130
+ workflow modules. It then checks the resolved mounted-source map and executes
131
+ from committed state through a host-backed store. A source mismatch parks the
132
+ run with `workflowSourceChanged`; normal scheduling does not retry it. Restore
133
+ the recorded source and explicitly resume, or cancel the run. A headless Pi
134
+ child uses its own registered process group. The worker stops that group on
135
+ normal completion, and the host reaps it if the worker exits first. If the
136
+ worker stops for another recoverable reason, pure work can run again. A
137
+ protected write checks and renews the exact live token and generation in one
138
+ transaction. An expired or replaced owner cannot revive itself.
139
+
140
+ Interactive agent and assistant-message steps do not run headlessly for a Pi
141
+ session. The worker commits a durable interaction request and parks. The origin
142
+ session presents the request through documented Pi APIs and submits the exact
143
+ request, node, attempt, and revision. The host records submitted output as
144
+ provisional. A new supervised worker loads the workflow and runs its `validate`
145
+ function before the host accepts the submission. A validation error leaves the
146
+ same request pending and returns the error to the model. Closing Pi leaves that
147
+ request pending; reopening the same session adopts the existing session entry
148
+ or presents it once. Notifications use the durable session outbox. A root
149
+ `presentationPrompt` creates a durable terminal turn only after completion is
150
+ committed. A controller child without an origin session can use a supervised
151
+ headless `pi --mode rpc` child for structured agent steps.
152
+
153
+ Pause stops the worker and parks at the last durable boundary. Resume takes a
154
+ new generation. Cancellation can stop a live worker or atomically claim and
155
+ cancel an expired running row. Resume refuses changed workflow source.
145
156
 
146
157
  ## Node types
147
158
 
@@ -179,10 +190,13 @@ agent({
179
190
  because an invalid response is already visible and must not be retried.
180
191
 
181
192
  For submitted output, the engine appends the existing workflow-tool contract.
182
- The output passes through tolerant JSON normalization and then `validate`.
183
- Rejected submissions can retry in the same step. If the model settles without
184
- submitting, the extension nudges it twice by default and then fails the step.
185
- For assistant-message output, the engine appends a normal-response contract,
193
+ The host checks the durable transport identifiers, stores a `validating`
194
+ submission, and starts a supervised worker. In that worker, the output passes
195
+ through tolerant JSON normalization and then `validate`. The tool reports
196
+ success only after this check accepts the output. Rejected submissions return
197
+ the validation error and can retry in the same step. If the model settles
198
+ without submitting, the durable request stays pending until it receives valid
199
+ output, times out, or is cancelled. For assistant-message output, the engine appends a normal-response contract,
186
200
  waits for `agent_settled`, rejects empty, failed, aborted, or tool-only results,
187
201
  and never suppresses the visible text. Timeout and cancellation abort either
188
202
  form's active Pi turn.
@@ -225,20 +239,34 @@ controller resource instead.
225
239
 
226
240
  ### action
227
241
 
228
- Performs a side effect. Two forms exist. The function form runs arbitrary
229
- TypeScript:
242
+ Performs managed work. Every function action and shell action must declare how
243
+ the host recovers if the worker exits after the external operation but before
244
+ it saves a receipt.
245
+
246
+ Use `idempotentEffect(type)` only when the operation has a stable external
247
+ idempotency key or a read-back check that makes another attempt safe:
230
248
 
231
249
  ```typescript
232
- action({ run: async ({ input }) => await deployPreview(input) });
250
+ import { action, idempotentEffect } from "@osolmaz/pi-workflows";
251
+
252
+ action({
253
+ effect: idempotentEffect("preview.deploy"),
254
+ run: async ({ input }) => await deployPreview(input),
255
+ });
233
256
  ```
234
257
 
258
+ Use `manualEffect(type)` when the external system cannot prove whether an
259
+ uncertain request applied. An uncertain worker exit marks that effect
260
+ `ambiguous`, parks the run, and requires explicit operator recovery. The host
261
+ does not retry it automatically.
262
+
235
263
  The shell form (`shell` is a synonym that requires `exec`) runs a command owned
236
- by the runtime, so the workflow author decides exactly what executes, with a
237
- timeout and captured output:
264
+ by the workflow definition, with a timeout and captured output:
238
265
 
239
266
  ```typescript
240
267
  shell({
241
- exec: ({ input }) => ({
268
+ effect: idempotentEffect("repository.status"),
269
+ exec: () => ({
242
270
  command: "git",
243
271
  args: ["status", "--porcelain"],
244
272
  cwd: "/path/to/repo",
@@ -261,6 +289,7 @@ A function action can publish a durable update without completing the node:
261
289
 
262
290
  ```typescript
263
291
  action({
292
+ effect: idempotentEffect("dataset.process"),
264
293
  run: async ({ publishUpdate }) => {
265
294
  await publishUpdate({
266
295
  type: "progress",
@@ -283,6 +312,12 @@ keeps normal output capture. Lines and update data are each limited to 64 KiB.
283
312
  See [WORKFLOW_UPDATES.md](WORKFLOW_UPDATES.md) for the envelope, progress
284
313
  schema, limits, estimation, and error rules.
285
314
 
315
+ The host reserves the effect before it lets the action run. A repeated key with
316
+ the same request adopts the durable record; the same key with another request
317
+ is a conflict. A normal caught error settles the attempt as rejected. After an
318
+ uncertain process exit, an idempotent effect returns to pending for retry, while
319
+ a manual effect becomes ambiguous. This is not an exactly-once claim.
320
+
286
321
  ### checkpoint
287
322
 
288
323
  Ends the run in a `waiting` state for human review. The checkpoint run is
@@ -333,9 +368,9 @@ humanDecision({
333
368
  });
334
369
  ```
335
370
 
336
- The waiting run stores a versioned request and asks every channel configured for the logical audience. The structured `subject` remains machine data. Channels receive only the normalized `presentation`, title, choices, input prompts, and any deadline policy. The first valid verified human answer wins. When `onTimeout` is present and no human answer wins before the saved deadline, recovery applies the validated response with `timeout` provenance. This policy can continue without a configured channel. A continuation preserves the original workflow input and exposes the resolved response as the checkpoint output. `humanDecisionEdge()` provides exhaustive routing for the choices. Existing `body` requests remain a legacy compatibility form and use deterministic readable formatting.
371
+ The waiting run stores a versioned request and asks every channel configured for the logical audience. The structured `subject` remains machine data. Channels receive only the normalized `presentation`, title, choices, input prompts, and any deadline policy. The first valid verified human answer wins. When `onTimeout` is present and no human answer wins before the saved deadline, the host takes a control claim on the waiting parent and atomically applies the validated response with `timeout` provenance, closes the interaction, and reserves the continuation. This policy can continue without a configured channel. A continuation preserves the original workflow input and exposes the resolved response as the checkpoint output. `humanDecisionEdge()` provides exhaustive routing for the choices. Existing `body` requests remain a legacy compatibility form and use deterministic readable formatting.
337
372
 
338
- The model-facing workflow tool cannot answer a protected human decision. Pi interactive UI and configured external channels use a host-owned answer path. Ordinary checkpoints keep the existing `/workflow answer` behavior.
373
+ The model-facing workflow tool cannot answer a protected human decision. The origin Pi session displays the request without starting a model turn. A person uses `/workflow answer` to send the answer through the host-owned path. Ordinary checkpoints can also use the model-facing `answer` action.
339
374
 
340
375
  See [Human decisions](HUMAN_DECISIONS.md) for channels, recovery, persistence, and plan approval.
341
376
 
@@ -428,72 +463,31 @@ The run records every mounted source and a digest of the resolved graph. Resume
428
463
 
429
464
  The model sees one `workflow` tool. Its `action` field supports:
430
465
 
431
- - `list` for discovered workflow names and sources.
432
- - `start` with a workflow name or path and structured input.
433
- - `restart` with a terminal run ID. It creates a new run from the exact stored workflow reference and input.
434
- - `status` for the active run or a supplied run ID.
435
- - `pause`, `resume`, and `cancel` for the active run.
436
- - `answer` with ordinary checkpoint input and an optional run ID. Protected `humanDecision()` gates reject this model-facing action.
437
- - `change-settings` with an RFC 6902 patch, optional scope ID, and optional expected change number.
438
- - `queue-follow-up` to save one ordered normal user prompt for after successful completion.
439
- - `remove-follow-up` to remove an unsent prompt created by the same model source.
440
- - `update` for a non-completing update from the current agent attempt.
466
+ - `list` for discovered workflow names and sources;
467
+ - `start` with a workflow name or path and structured input;
468
+ - `status` for the active run or a supplied run ID;
469
+ - `pause` and `resume` for the active session run;
470
+ - `cancel` for the active run or a supplied run ID;
471
+ - `answer` with checkpoint input and an optional run ID;
472
+ - `update` for a non-completing update from the current agent attempt;
441
473
  - `submit` for the current workflow step contract.
442
474
 
443
- A direct user request to continue or resume the active workflow maps to the
444
- `resume` action immediately. The model does not call `status` instead of
445
- `resume`, and it does not use `status` as a prerequisite.
446
-
447
- `resume` is idempotent while a run is active. A held, pausing, or paused run is
448
- released and reports `resumed: true`. An active run that is already executing
449
- returns normal success with `resumed: false` and `alreadyRunning: true`. It does
450
- not change the run state. With no active run, `resume` still returns a warning.
451
-
452
- Model-facing `status` keeps `status` as the durable workflow lifecycle state.
453
- It also reports the host action fields `paused`, `workState`, and `resumable`.
454
- For the current active run, `paused` is true when the host has requested or
455
- applied a hold, or when the durable run state has `paused: true`. `workState`
456
- is `running`, `pausing`, or `paused` for that active host run and `inactive`
457
- when no current host run can act on the durable state. `resumable` is true only
458
- when `resume` can release the current active run. Queue-only status uses its
459
- launch state, such as `queued` or `starting`, as `workState`; queue-only and
460
- no-run results report `paused: false` and `resumable: false`. Thus, a durable
461
- `status: "running"` can correctly appear with `workState: "pausing"` or
462
- `workState: "paused"`, and the status message names that actionable state
463
- instead of saying only that the workflow is running.
464
-
465
- Restart uses this contract:
475
+ A direct user request to continue or resume the active workflow maps to
476
+ `resume` immediately. The model does not call `status` instead of `resume` or
477
+ use it as a prerequisite. An already active run adopts the resume request. A
478
+ paused or parked run gets a new claim generation and worker. With no resumable
479
+ run, the host rejects the request.
466
480
 
467
- ```json
468
- {
469
- "action": "restart",
470
- "runId": "terminal-run-id"
471
- }
472
- ```
481
+ `status` reports the durable queue projection. A host command succeeds only
482
+ after its transaction commits. The protocol stores request fingerprints and
483
+ receipts, so an exact duplicate adopts the committed result and conflicting
484
+ reuse is rejected.
473
485
 
474
- The terminal run must belong to the current Pi session and must not be active,
475
- waiting, or explicitly cancelled. Its source and revision must still resolve
476
- exactly. Restart creates a new immutable run and leaves the terminal run
477
- unchanged. It copies the stored input and safe launch settings; it does not
478
- reconstruct input from conversation history.
479
-
480
- A model-started run is queued until the model's current turn settles. A terminal
481
- decision turn can reserve one restart, Monitor run, or other workflow start.
482
- A second workflow launch from that turn fails. The first workflow prompt then
483
- starts a new turn. This keeps the requesting turn outside
484
- the workflow's first attempt and prevents an early missing-submission reminder.
485
- The normal extension offers all actions. The headless RPC bridge offers only
486
- `update` and `submit`, so a workflow child cannot recursively control other
487
- runs. Direct `/workflow change-settings`, `queue-follow-up`, and
488
- `remove-follow-up` commands use verified interactive provenance. Controller
489
- code can use the matching `ctx.workflows` methods. All surfaces call the same
490
- SQLite operations.
491
-
492
- Follow-up prompts stay separate from workflow settings. Successful terminal
493
- state is saved before delivery. A final presentation settles first. The Pi
494
- extension then sends prompts in order as normal user messages without
495
- reactivating the completed run. See [Continue normal work after a workflow
496
- finishes](2026-08-25-workflow-follow-ups.md).
486
+ The normal extension offers these actions through the origin Pi session. The
487
+ headless RPC bridge offers only `update` and `submit`, so a controller child
488
+ cannot recursively control unrelated runs. Controller code can use its narrow
489
+ `ctx.workflows` methods for child runs, settings, and follow-up records. Those
490
+ methods also commit through the global host.
497
491
 
498
492
  ### Built-in plain summary
499
493
 
@@ -597,7 +591,7 @@ Only bounded final assistant text and safe operational facts leave a live child
597
591
 
598
592
  The workflow publishes aggregate and per-agent `pi-workflows.progress.v1` tracks under `agents/review/*` and `agents/verification/*`. Progress contains role, the verified actual model when known, state, elapsed facts, and safe phases such as `thinking` or `tool: read`. The Pi widget shows the aggregate plus failed and active children within its ten-line limit. `piw` shows every durable child track and its samples. Both views use existing progress records, so no child workflow run or new persisted schema is needed.
599
593
 
600
- Serial mode still uses two child sessions, and parallel mode still uses five. Review prompts, review areas, strict result validation, verdicts, and progress stay unchanged. After verification, an assistant-message agent shows the full bounded report verbatim. A mismatch stops before summary generation. The graph then includes `plain-summary`, which shows a short plain-language explanation with the verdict and the most important next action. The detailed response always settles before the summary starts. A final compute node returns the original strict result, so presentation cannot change the verdict. Sanity Check uses no final notification or root `presentationPrompt`.
594
+ Serial mode still uses two child sessions, and parallel mode still uses five. Review prompts, review areas, strict result validation, verdicts, and progress stay unchanged. After verification, an assistant-message agent shows the full bounded report verbatim. A mismatch stops before summary generation. The graph then includes `plain-summary`, which shows a short plain-language explanation with the verdict and the most important next action. The detailed response always settles before the summary starts. A final compute node returns the original strict result, so presentation cannot change the verdict. Sanity Check creates no extra terminal model turn.
601
595
 
602
596
  The CLI, JSON or RPC stream, temporary prompt file, standard-output cap, subprocess fallback, shared child runtime, and blanket child-extension ban are not retained. See [the Sanity Check plan](plans/2026-08-21-sanity-check-plan.md) for the selected implementation and test boundaries.
603
597
 
@@ -688,59 +682,22 @@ use an internal turn-intent contract instead of the notification outbox. See
688
682
  [WORKFLOW_STEP_MESSAGES.md](WORKFLOW_STEP_MESSAGES.md) for the step-message contract
689
683
  and [Deferred workflow turns](DEFERRED_TURNS.md) for the successor-turn contract.
690
684
 
691
- ## Result presentation
685
+ ## Visible responses
692
686
 
693
687
  Workflow nodes normally produce structured values for routing and persistence.
694
- When a person should see a normal prose response only after the root run, add
695
- `presentationPrompt` at the top level:
688
+ When a person must receive normal prose, use an agent node with
689
+ `expectedOutput: assistantMessage()`. The worker parks and records the exact
690
+ step request. The origin Pi session starts the model turn, and the visible
691
+ assistant text becomes the node output after the turn settles.
696
692
 
697
- ```typescript
698
- export default defineWorkflow({
699
- name: "report",
700
- presentationPrompt: ({ state, finalOutput }) =>
701
- state.status === "waiting"
702
- ? `Explain this recommendation and ask the user to decide: ${JSON.stringify(finalOutput)}`
703
- : "Summarize the completed result and any remaining limitations.",
704
- // ...startAt, nodes, and edges
705
- });
706
- ```
693
+ The request keeps its node and attempt ID across Pi reload. The extension first
694
+ looks for an existing session entry with the durable request ID. It inserts a
695
+ new visible message only when no adopted entry exists. A repeated submission
696
+ returns its stored receipt.
707
697
 
708
- After a top-level interactive run becomes terminal, the Pi extension gives the
709
- model one normal terminal decision turn. The message contains the workflow name
710
- and revision, terminal run ID, exact stored input, bounded result, terminal
711
- state and reason, restart count, and earlier terminal outcomes in the restart
712
- chain. A completed state does not prove that the user's larger task is complete.
713
- The model uses the current Pi conversation to stop, restart safely, start
714
- Monitor for an authorized external wait, ask for a decision or authority, or
715
- take another safe authorized action.
716
-
717
- `presentationPrompt` adds workflow-specific presentation instructions to this
718
- shared terminal decision message for completed runs. Returning `undefined`,
719
- returning an empty string, omitting `presentationPrompt`, or ending in failure,
720
- timeout, or cancellation uses the factual terminal fallback instead. Normal
721
- presentation and fallback claim the same terminal turn intent, so races,
722
- reload, crash recovery, and compaction cannot create a second decision turn.
723
- Async prompt builders have 30 seconds to finish and receive an `AbortSignal`
724
- that fires on timeout, session shutdown, or when a new workflow or normal user
725
- turn starts; stale presentations are discarded.
726
-
727
- Waiting checkpoints are not terminal and do not create a terminal decision
728
- turn. Controller child runs and internally owned runs report to their owner and
729
- do not create competing turns. Explicit cancellation produces terminal facts,
730
- but its decision instruction defaults to stopping and the `restart` shortcut
731
- rejects it.
732
-
733
- An agent with `expectedOutput: assistantMessage()` is different. Its visible
734
- assistant response is the node output, can appear before later nodes, and also
735
- works inside an included workflow. A root `presentationPrompt` would add a
736
- second response, so workflows that end with assistant-message output normally
737
- omit it.
738
-
739
- Presentation and terminal decisions are outside the workflow graph: they cannot
740
- route to another node, change the terminal run, or alter its result. A selected
741
- restart always creates a new run. Workflow definitions need no opt-in,
742
- continuation node, or restart prompt. Pi owns conversation history. Pi Workflows
743
- does not identify, hash, copy, or store an original user message.
698
+ A headless controller child cannot produce a visible assistant message without
699
+ an approved origin-session binding. Use structured agent output for detached
700
+ work. Terminal run state does not create an extra model turn.
744
701
 
745
702
  ## Runtime behavior
746
703
 
@@ -751,41 +708,31 @@ possible. Defaults worth knowing:
751
708
  - Node timeout is 15 minutes unless the node sets `timeoutMs` to a positive
752
709
  number or context callback. A timed-out node has outcome `timed_out` and can
753
710
  be routed with `$result.outcome`. A timed-out agent node also aborts its Pi
754
- turn, and late output for that attempt is rejected.
711
+ turn, and late output for that attempt is rejected. Interactive runs save the
712
+ resolved wall-clock deadline before they park. The host enforces that deadline
713
+ while Pi is closed and after host restart.
755
714
  - `maxSteps` (workflow-level, default 100) bounds loops built from cycles in
756
715
  the graph.
757
- - `/workflow pause` requests a pause: the current step finishes normally,
758
- then the run holds at the step boundary (`paused: true` in the run state,
759
- `run_paused` in the trace) until `/workflow resume` or `/workflow cancel`.
760
- Pausing never interrupts a node mid-flight.
761
- - Interrupting a turn (escape) auto-pauses the run: the pending agent step is
762
- held without nudges and the engine pauses at the next boundary. Node
763
- timeouts keep ticking while held, so a long-abandoned step still times out.
764
- `/workflow resume` re-delivers the pending step prompt.
765
- - Resuming an active run that is already running succeeds without changing the
766
- engine, executor, widget, or durable workflow state. This makes duplicate
767
- `resume` calls safe.
768
- - A model-started workflow is persisted as `queued` with its final run ID before the start tool
769
- returns. Activation waits for the initiating agent turn to settle, then moves through `starting`
770
- and `running`. `workflow status` and `workflow cancel` accept the run ID before a SQLite run state
771
- exists.
772
- - If deferred activation fails, the queue stores a bounded safe error, releases the session
773
- reservation, and creates one terminal turn intent for the initiating session. A workflow that
774
- reports `started` and then crashes before its first prompt follows the same path. The model gets
775
- one factual decision turn after settlement. Pi Workflows does not retry automatically.
776
- - Agent-issued and direct `workflow cancel` actions that cancel an active or queued run create or
777
- settle one terminal turn intent. The resulting decision defaults to stopping, and `restart`
778
- rejects the cancelled run. When no run is live but the widget still shows a parked or finished
779
- run, the command clears the widget.
780
- - A restart chain allows at most three restart actions after the original run. The terminal
781
- fingerprint excludes timestamps and run IDs. If the same workflow revision, exact input, state,
782
- result or error, and reason occur again in that chain, another restart fails immediately. A
783
- changed outcome can remain restartable until the chain limit. Starting Monitor does not consume
784
- a restart.
785
- - One workflow runs per session at a time.
786
- - After the workflow tool accepts an agent-step submission, any assistant text that follows remains visible. The next workflow message continues the graph. A deferred intent makes a workflow prompt, presentation, and factual fallback compete to provide one successor turn, so an abort cannot produce two continuation turns.
787
- - Agent nudges: if the model ends its turn without submitting the pending
788
- step, it gets a reminder, twice by default, then the step fails.
716
+ - `/workflow pause` atomically parks the run with `paused: true`, stores the
717
+ receipt, and fences the worker before process-group shutdown. `/workflow
718
+ resume` takes a new generation and reruns only work after the last durable
719
+ boundary.
720
+ - Resuming an active run adopts the existing work. Duplicate start, control,
721
+ update, and submission messages return their stored receipts.
722
+ - A start is committed as `queued` with its final run ID before the command
723
+ reports success. Cancellation can use that run ID before its scheduled worker
724
+ starts. Active cancellation commits its terminal state and command receipt
725
+ together before worker shutdown. It cancels effects that have not started and
726
+ marks applying effects ambiguous for explicit recovery. `workflow status` and
727
+ `workflow cancel` can use the run ID immediately.
728
+ - One interactive workflow request is presented per Pi session. Other requests
729
+ remain durable and ordered.
730
+ - Each protected write renews only its exact live token and generation in the
731
+ same transaction. Claim loss does not write a failed run event.
732
+ - An uncommitted pure or idempotent node can run again after a worker crash. An
733
+ uncertain manual effect parks as ambiguous and never retries automatically.
734
+ - Host status reports safe counts and timestamps. It does not report session
735
+ IDs, project paths, prompts, payloads, tokens, process IDs, or credentials.
789
736
 
790
737
  ## Workflows started by controllers
791
738
 
@@ -810,14 +757,16 @@ if (run.state !== "succeeded") {
810
757
  }
811
758
  ```
812
759
 
813
- Child workflow completion queues the parent resource again. A running child left by a stopped host is recorded as a failed SQLite run state with a `run_interrupted` event. The controller treats that child attempt as interrupted, and the next parent reconciliation starts another immutable attempt. Consequential external mutations should use the controller effect API so uncertain results are observed before retry.
760
+ Child workflow completion queues the parent resource again. The global host runs the child through the same queue and supervised worker model as any other run. A host or worker crash resumes the existing durable run when its committed effect state makes that safe. Consequential external mutations belong in the workflow or controller effect API; an uncertain result stops for explicit recovery.
814
761
 
815
762
  See [CONTROLLERS.md](CONTROLLERS.md) for controller definitions and the full recovery contract.
816
763
 
817
- ## Using the engine outside pi
764
+ ## Using the engine outside Pi
818
765
 
819
- The engine is pi-agnostic. `WorkflowEngine` takes any `AgentStepExecutor`, so
820
- tests (and other hosts) can script agent steps:
766
+ The engine remains Pi-agnostic. `WorkflowEngine` takes any `AgentStepExecutor`,
767
+ so tests and custom library integrations can script agent steps. The package's
768
+ production extension does not use this as a selectable embedded runtime; it
769
+ always sends runs to the global host.
821
770
 
822
771
  ```typescript
823
772
  import { WorkflowEngine, type AgentStepExecutor } from "@osolmaz/pi-workflows";
@@ -830,6 +779,9 @@ const executor: AgentStepExecutor = {
830
779
  },
831
780
  };
832
781
 
833
- const engine = new WorkflowEngine({ executor, outputRoot: "/tmp/runs" });
782
+ const engine = new WorkflowEngine({
783
+ executor,
784
+ databasePath: "/tmp/workflow-state.sqlite",
785
+ });
834
786
  const { state } = await engine.run(workflow, { task: "..." });
835
787
  ```