@osolmaz/pi-workflows 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -0,0 +1,452 @@
1
+ # Workflow host
2
+
3
+ Status: implemented. [Run workflows outside Pi](2026-08-30-out-of-process-workflow-host-plan.md) records the approved implementation plan.
4
+
5
+ ## Purpose
6
+
7
+ The workflow host keeps durable workflow state correct when Pi, a workflow, or the host stops unexpectedly. It owns the workflow database and supervises a separate process for each active run. Pi remains the user interface and performs interactive model turns through its documented extension APIs.
8
+
9
+ The host solves two different failures:
10
+
11
+ - A busy workflow cannot block the process that renews run claims.
12
+ - A crashed or stale runner cannot leave contradictory state or continue writing after another runner takes over.
13
+
14
+ ## Terms
15
+
16
+ - **Host:** The single user-level process that owns workflow state, claims, commands, and worker supervision.
17
+ - **Client:** A Pi extension instance or command-line process connected to the host.
18
+ - **Worker:** A child process that loads one workflow and executes one active run generation.
19
+ - **Origin session:** The Pi session that started an interactive run.
20
+ - **Claim:** A time-limited right to change one run.
21
+ - **Generation:** A number increased each time a new owner claims a run. It fences older owners.
22
+ - **Durable boundary:** A committed node or lifecycle transition from which execution can resume.
23
+ - **Interactive request:** A durable agent or assistant-message step that must run in the origin Pi session.
24
+ - **Managed effect:** A side effect reserved and settled through an idempotent durable record.
25
+
26
+ ## Boundaries
27
+
28
+ The host belongs to the `@osolmaz/pi-workflows` package. It uses the existing SQLite database at `~/.pi/agent/workflows/state.sqlite` and documented Pi extension APIs.
29
+
30
+ The design does not change Pi source, Pi session files, Pi message schemas, or private Pi APIs. It does not add a remote service or a second database. The package does not install an operating-system service.
31
+
32
+ SQLite remains local to one machine. The protocol does not provide distributed consensus or network-filesystem safety.
33
+
34
+ ## Process model
35
+
36
+ One host owns the global workflow database for one user installation.
37
+
38
+ ```text
39
+ Pi extension ─┐
40
+ Pi extension ─┼── local socket ── workflow host ── SQLite
41
+ CLI client ───┘ │
42
+ ├── run worker A
43
+ ├── run worker B ── headless pi --mode rpc
44
+ ├── controller worker
45
+ └── source resolver
46
+ ```
47
+
48
+ The host may manage runs from more than one project. Each run keeps its canonical project path and source identity.
49
+
50
+ The host process performs only bounded protocol handling, short SQLite transactions, timers, queue scheduling, and process supervision. It does not import or execute workflow definitions.
51
+
52
+ A worker loads one workflow source and executes one run generation. It cannot receive a writable `WorkflowRunStore`. It proposes changes to the host over a private child channel. This is an architectural guard against accidental writes. It is not a security sandbox against code running as the same operating-system user.
53
+
54
+ ## Host lifecycle
55
+
56
+ The package CLI owns host lifecycle commands:
57
+
58
+ ```text
59
+ pi-workflows host start
60
+ pi-workflows host status
61
+ pi-workflows host stop
62
+ pi-workflows host run
63
+ ```
64
+
65
+ `run` stays attached for direct operation and tests. `start` starts the package process on demand and waits for a ready handshake. It does not install systemd, launchd, or another persistent service.
66
+
67
+ The host uses one global lock and one host epoch. Socket creation and the SQLite host claim must agree before the host accepts commands. A second live host refuses to start. After the old host lease expires, a new host increases the epoch before it handles work. Messages from an older epoch are rejected.
68
+
69
+ The host stays alive while it has a connected client, an active worker, a scheduled wake, a pending controller, or unsettled work. An idle host may exit after a documented idle period. A later client can start it again.
70
+
71
+ ## Claim rules
72
+
73
+ A run claim contains:
74
+
75
+ - owner type and owner ID;
76
+ - token hash;
77
+ - generation;
78
+ - acquisition time;
79
+ - heartbeat time;
80
+ - expiry time.
81
+
82
+ A protected state write uses one SQLite transaction:
83
+
84
+ 1. Read the expected resource revision and lease.
85
+ 2. Compare owner type, owner ID, token hash, and generation.
86
+ 3. Require a future expiry time.
87
+ 4. Renew heartbeat and expiry for that exact claim.
88
+ 5. Apply the domain change.
89
+ 6. Add the immutable event and viewer delta.
90
+ 7. Increase the resource revision.
91
+ 8. Commit.
92
+
93
+ The transaction fails without changes when any check fails.
94
+
95
+ An expired claim cannot renew itself, even when the owner ID and token hash still match. Recovery first takes a new claim and increases the generation.
96
+
97
+ The host also renews active claims from a timer. The timer is a backup for a run with no state writes. Normal write correctness does not depend on the timer.
98
+
99
+ Claim rejection uses `ClaimLostError` with one internal reason:
100
+
101
+ - `missingAuthority`
102
+ - `expired`
103
+ - `ownerChanged`
104
+ - `tokenChanged`
105
+ - `generationChanged`
106
+
107
+ Logs may show the run ID, generation, and reason. They must not show a raw token or token hash.
108
+
109
+ ## Run lifecycle
110
+
111
+ The run and queue projections follow these states:
112
+
113
+ | Run state | Queue state | Claim | Worker | Meaning |
114
+ | ----------- | ----------- | ----- | -------- | ------------------------------------------------------- |
115
+ | `queued` | `queued` | none | none | Ready for host scheduling. |
116
+ | `running` | `starting` | host | starting | A worker launch is being recorded. |
117
+ | `running` | `running` | host | live | A worker is executing one node. |
118
+ | `running` | `parked` | none | none | Execution stopped at a durable boundary and can resume. |
119
+ | `waiting` | `parked` | none | none | A checkpoint or interactive request needs input. |
120
+ | `completed` | `done` | none | none | The run finished successfully. |
121
+ | `failed` | `failed` | none | none | The run failed with a durable error. |
122
+ | `timed_out` | `failed` | none | none | The run exceeded a declared timeout. |
123
+ | `cancelled` | `cancelled` | none | none | Cancellation completed. |
124
+
125
+ A lifecycle transaction updates the run, queue, attempt, decision, lease, event, and viewer facts that belong to one transition. The database must not commit a failed event while the run remains running, or a terminal queue row while the run remains nonterminal.
126
+
127
+ Waiting and paused work does not keep a worker or a live claim. Resume takes a new claim generation and starts a new worker from the last durable boundary.
128
+
129
+ ## Worker lifecycle
130
+
131
+ A worker launch envelope contains:
132
+
133
+ ```json
134
+ {
135
+ "schema": "pi-workflows.worker-launch.v1",
136
+ "runId": "run-id",
137
+ "generation": 2,
138
+ "workerEpoch": "opaque-id",
139
+ "projectPath": "/canonical/project/path",
140
+ "workflowSource": {
141
+ "root": {
142
+ "kind": "file",
143
+ "path": "/canonical/project/path/.pi/workflows/example.workflow.ts",
144
+ "hash": "sha256-digest"
145
+ },
146
+ "mounted": []
147
+ },
148
+ "definitionDigest": "sha256:digest",
149
+ "inputHash": "sha256:digest",
150
+ "protocolVersion": 1
151
+ }
152
+ ```
153
+
154
+ Before it loads workflow modules, the worker verifies the root identity and every saved mounted file hash or built-in revision. After loading, it also checks the complete mounted-source map against the saved map. A mismatch parks the run with `workflowSourceChanged`. The normal scheduler does not claim that run again. The operator can restore the recorded source and explicitly resume the run, or cancel it. Changed included code does not execute.
155
+
156
+ The host records a worker epoch before spawn. The child must return a ready message before the startup deadline. Every later child message includes the run ID, generation, and worker epoch.
157
+
158
+ The host records one terminal worker outcome:
159
+
160
+ - `exited`
161
+ - `cancelled`
162
+ - `timedOut`
163
+ - `crashed`
164
+ - `claimLost`
165
+ - `orphaned`
166
+
167
+ A worker exit is not automatically a run failure. The host decides from the last committed attempt and effect state whether it can resume, must park, or must fail.
168
+
169
+ ## Process supervision
170
+
171
+ Each worker starts in its own process group. A headless Pi child starts in another process group so normal worker completion can stop all Pi tool descendants without signaling the worker itself. The worker registers that direct child with the host before it sends a prompt and unregisters it only after group shutdown. The host owns the one process registry and reaps a registered child if its worker exits first.
172
+
173
+ The host enforces:
174
+
175
+ - a startup handshake deadline;
176
+ - node deadlines already declared by the workflow engine;
177
+ - bounded protocol messages;
178
+ - bounded captured stdout and stderr;
179
+ - cancellation with `SIGTERM` and bounded `SIGKILL` escalation;
180
+ - process-group cleanup;
181
+ - orphan checks after host restart;
182
+ - portable memory or process limits where Node and the operating system support them.
183
+
184
+ The child protocol must apply backpressure. A child that exceeds message or output limits fails its worker epoch with a clear infrastructure reason. The complete durable workflow result stays in SQLite within the existing value limits.
185
+
186
+ The process registry includes a process start identity, not only a PID. The host accepts a worker registration only when the PID is a direct child of that active worker. A reused PID cannot let a new host kill an unrelated process.
187
+
188
+ ## Local client protocol
189
+
190
+ Clients connect through a user-only local socket. Unix socket mode is `0600`. Other platforms use their equivalent local transport and access control.
191
+
192
+ Messages use newline-delimited canonical JSON. One message is at most 1 MiB, matching the existing durable event limit. The receiver closes only the offending connection when framing or validation fails.
193
+
194
+ Every request uses this envelope:
195
+
196
+ ```json
197
+ {
198
+ "schema": "pi-workflows.host-request.v1",
199
+ "requestId": "opaque-id",
200
+ "clientId": "opaque-id",
201
+ "operation": "run.cancel",
202
+ "runId": "run-id",
203
+ "expectedRevision": 12,
204
+ "idempotencyKey": "stable-key",
205
+ "payload": {}
206
+ }
207
+ ```
208
+
209
+ A response uses:
210
+
211
+ ```json
212
+ {
213
+ "schema": "pi-workflows.host-response.v1",
214
+ "requestId": "opaque-id",
215
+ "outcome": "accepted",
216
+ "revision": 13,
217
+ "receipt": {}
218
+ }
219
+ ```
220
+
221
+ Valid outcomes are:
222
+
223
+ - `accepted`
224
+ - `adopted`
225
+ - `rejected`
226
+ - `conflict`
227
+ - `notFound`
228
+ - `claimLost`
229
+ - `unavailable`
230
+
231
+ The host commits a command receipt before it acknowledges success. Repeating the same request ID and payload returns the stored receipt. Reusing an ID with another payload returns a conflict.
232
+
233
+ The first command set is:
234
+
235
+ - `run.start`
236
+ - `run.pause`
237
+ - `run.resume`
238
+ - `run.cancel`
239
+ - `run.status`
240
+ - `run.list`
241
+ - `checkpoint.answer`
242
+ - `decision.answer`
243
+ - `interaction.submit`
244
+ - `interaction.update`
245
+ - `notification.claim`
246
+ - `notification.deliver`
247
+ - `turn.claim`
248
+ - `turn.resolve`
249
+ - `controller.list`
250
+ - `controller.get`
251
+ - `controller.apply`
252
+ - `controller.reconcile`
253
+ - `controller.delete`
254
+ - `host.status`
255
+ - `host.stop`
256
+
257
+ Read operations may use the existing read-only store directly in viewers. Mutating Pi and CLI paths use the host.
258
+
259
+ ## Worker protocol
260
+
261
+ The private worker channel accepts these message kinds:
262
+
263
+ - `worker.ready`
264
+ - `node.started`
265
+ - `node.update`
266
+ - `node.finished`
267
+ - `node.failed`
268
+ - `run.parked`
269
+ - `run.finished`
270
+ - `interaction.requested`
271
+ - `interaction.accepted`
272
+ - `interaction.rejected`
273
+ - `notification.requested`
274
+ - `presentation.requested`
275
+ - `effect.reserve`
276
+ - `effect.settle`
277
+ - `worker.progress`
278
+ - `worker.exiting`
279
+
280
+ Every worker message includes the worker launch schema, run ID, generation, worker epoch, attempt ID when applicable, expected revision, and a stable message ID. Headless workers use `process.register` and `process.unregister` operations under `worker.progress` to attach their Pi child group to host supervision. Registration requires the live run claim. Unregistration remains valid after a terminal state releases that claim so cleanup can finish.
281
+
282
+ The host checks the generation and epoch before it reads the payload. A stale worker gets one claim-loss response and must exit. The host stores receipts for accepted state-changing messages so a retry receives the same answer.
283
+
284
+ ## Durable protocol records
285
+
286
+ Reuse current rows when they already own a fact:
287
+
288
+ - `runs`, `run_queue`, `leases`, and `events` own run lifecycle and claims.
289
+ - `node_attempts` owns node execution state and resolved wall-clock deadlines.
290
+ - `human_decisions` and resolution tables own checkpoints.
291
+ - `effects` and `effect_attempts` own side effects and ambiguous outcomes.
292
+ - `notifications` and `turn_intents` own passive and terminal Pi messages.
293
+ - `run_bindings` owns origin session and execution mode.
294
+
295
+ Add only these records if implementation proves the current rows cannot hold the contract:
296
+
297
+ ### Host commands
298
+
299
+ `host_commands` stores request ID, client ID, operation, idempotency key, request fingerprint, run ID, accepted revision, outcome, receipt or error hash, and timestamps. The unique request fingerprint prevents one request ID from naming two commands.
300
+
301
+ ### Interactive requests
302
+
303
+ `interactive_requests` stores request ID, run ID, attempt ID, target session ID, kind, contract hash, pending or settled status, accepted submission ID, and timestamps. One attempt has at most one request. The linked node attempt stores its resolved wall-clock deadline.
304
+
305
+ `interactive_submissions` stores request ID, submission ID, idempotency key, payload hash, validating, accepted, or rejected outcome, receipt hash, and submission time. Repeated keys return the same receipt.
306
+
307
+ ### Worker epochs
308
+
309
+ `run_workers` stores run ID, generation, worker epoch, launch envelope hash, process identity, status, start time, ready time, finish time, exit code, signal, and bounded diagnostic hash. One run and generation can have several sequential worker epochs, but only one may be active.
310
+
311
+ These tables remain part of `pi-workflows-state` schema version 1. The DDL digest changes in place under the alpha policy.
312
+
313
+ ## Interactive Pi execution
314
+
315
+ Agent and assistant-message steps for an interactive run execute in the origin Pi session.
316
+
317
+ The worker commits the node's resolved wall-clock deadline before it proposes `interaction.requested`. The host commits the request, changes the node attempt to waiting, parks the queue row, releases the claim, and acknowledges the worker. The worker then exits. The host continues to enforce the durable deadline while no worker exists. If the deadline passes, one control claim atomically closes the stale request and schedules a supervised timeout-resume child. The child preserves the same attempt and deadline, records `timed_out`, and follows any `$result.outcome` edge. A run with no timeout recovery edge becomes terminal and releases its session reservation. Restart recovery starts this timeout path before it schedules other work.
318
+
319
+ The extension finds pending requests during `session_start`, after `agent_settled`, and once per second while the session is open. It claims one request presentation, sends the step message through documented Pi APIs, and exposes the normal `workflow` tool contract.
320
+
321
+ A tool update or submission goes to the host. It includes the exact request, node, attempt, expected revision, and tool-call idempotency key. The host first checks this transport contract and records a provisional `validating` submission. It then schedules a supervised workflow child. Only that child loads workflow code and runs the node's `validate` function. The child reports `interaction.accepted` or `interaction.rejected` to the host. The host settles the request only after acceptance. A rejected payload leaves the same request pending and returns the stored actionable error to the model. If the child stops before it reports a result, the host rejects the provisional submission and leaves the request ready for a corrected retry.
322
+
323
+ An ordinary checkpoint accepts the model-facing `answer` action and starts a continuation run. A protected human decision never accepts that tool action. The extension displays the decision without starting a model turn, and a person answers it with `/workflow answer` through `decision.answer`. When a protected decision reaches its saved `onTimeout` deadline, the host takes a control claim on the waiting parent, atomically records the validated default, closes the pending interaction, releases the parent claim, and reserves the continuation. A human answer cannot win after that deadline.
324
+
325
+ The session keeps normal Pi entries for prompts, tools, and replies. Pi Workflows stores the public session entry ID used for presentation adoption. It does not edit the Pi session file or schema.
326
+
327
+ One session presents one workflow interaction at a time. Other requests remain ordered by creation time. A restart or reload can present an unresolved request again, but exact session-entry adoption prevents a second visible message when the first presentation was already recorded.
328
+
329
+ Notify nodes enqueue passive messages in the existing `notifications` outbox. The extension claims a message through the host, adopts an existing session entry after a crash, and marks delivery through the host. A completed run with a root `presentationPrompt` creates an ineligible `turn_intent` before the terminal commit. The same terminal transaction makes that intent eligible. The extension claims it, starts one normal Pi turn, and records the public session entry ID. No completion turn starts before the completed state is durable.
330
+
331
+ ## Detached execution
332
+
333
+ A run with headless execution mode uses the existing `pi --mode rpc` integration for agent steps. The Pi child uses a separate process group registered with the host. The worker stops that group during normal completion. Cancellation gives the worker a bounded cleanup interval, and the host reaps the registered group if the worker exits first.
334
+
335
+ The headless child receives only the workflow step prompt, configured model arguments, and the bridge extension. Its submission uses the same step and attempt contract as origin-session work. A headless run cannot use a visible assistant-message step because it has no origin Pi session.
336
+
337
+ The run binding records `interactive` or `headless` execution mode. Viewers show that mode without exposing provider credentials.
338
+
339
+ ## Pause and cancellation
340
+
341
+ Pause atomically commits `paused = 1`, parks the queue, releases the exact claim, and stores the command receipt. The fenced worker process group then stops. An uncommitted pure node can run again after resume. Resume takes a new generation and starts another worker from the last durable boundary.
342
+
343
+ Cancellation against a live worker atomically commits terminal cancellation, cancels pending attempt and interaction state, settles effect recovery state, releases the exact claim, and stores the command receipt. A pending effect becomes cancelled. An applying effect becomes ambiguous because the host cannot prove its external outcome. The host then stops the fenced worker process group. A host crash after the receipt cannot resume the cancelled run or retry the ambiguous effect. If the child does not stop by the deadline, the host kills its process group.
344
+
345
+ Cancellation against an expired running row first takes a new control claim. The claim operation must prove that the old lease is absent or expired. The new owner then cancels the active attempt, effect recovery state, and pending interaction or human decision in one lifecycle transaction.
346
+
347
+ A client cannot force-cancel a live claim through the stale recovery path.
348
+
349
+ ## Recovery
350
+
351
+ At startup the host:
352
+
353
+ 1. Takes the global host epoch.
354
+ 2. Reaps worker records that match an exact stale process identity.
355
+ 3. Finds expired running runs and active attempts.
356
+ 4. Reads managed effect state before deciding whether work can repeat.
357
+ 5. Parks uncertain effects for manual review.
358
+ 6. Makes pure and fully settled work claimable.
359
+ 7. Restores pending interactive requests and scheduled controller work.
360
+ 8. Starts supervised timeout recovery for pending interactive requests whose durable node deadlines expired.
361
+ 9. Resumes any remaining provisional `validating` submission in a new supervised child.
362
+ 10. Starts no model turn until a matching Pi session connects or headless mode is declared.
363
+
364
+ Recovery resumes from the last committed boundary. An uncommitted compute node may run again because compute is pure. An action with a stored effect receipt adopts that receipt. An effect in `ambiguous` state requires explicit recovery.
365
+
366
+ Claim loss is a handoff, not a run failure. The old owner writes no terminal event after claim loss.
367
+
368
+ ## Effects and retry safety
369
+
370
+ Compute nodes must not perform external side effects. They can repeat after a worker crash.
371
+
372
+ Side-effecting action and shell behavior must have one of these contracts:
373
+
374
+ - a managed effect with an external idempotency key;
375
+ - a managed effect with a read-back check that proves whether it applied;
376
+ - an explicit non-resumable result that becomes `ambiguous` after an uncertain crash.
377
+
378
+ The host reserves an effect before execution. The effect key includes the source resource, effect type, and author-provided idempotency key. The request fingerprint prevents key reuse with another payload.
379
+
380
+ An applied, rejected, or cancelled effect is terminal. An ambiguous effect is also terminal for automatic retry. An operator may use a separate reviewed recovery action after inspecting the external system.
381
+
382
+ ## Controllers
383
+
384
+ The global host also reconciles controllers. Controllers keep their existing resource claims, queue, effects, and child workflow request keys.
385
+
386
+ A controller child run enters the same global run queue and worker process model. It does not need an origin Pi session unless its workflow declares an interactive step. A headless child uses the declared provider path. A child that needs an origin session parks with a clear unsupported-input result unless the controller supplied an approved session binding.
387
+
388
+ Controller reconcile code runs in a supervised controller worker, not in the host event loop. Controller initialization also runs in a source resolver child. Before `controller.apply` commits, the host checks that the resolved source still matches controller discovery rules and the exact source digest.
389
+
390
+ ## Failure classification
391
+
392
+ Use separate states and messages for these failures:
393
+
394
+ - `claimLost`: another generation owns the run, or the claim expired.
395
+ - `workerCrashed`: the child exited without a terminal protocol message.
396
+ - `workerTimedOut`: the child exceeded a declared deadline.
397
+ - `hostUnavailable`: the client cannot reach or start the host.
398
+ - `sourceChanged`: the workflow source does not match the saved identity.
399
+ - `effectAmbiguous`: an external action may have applied without a receipt.
400
+ - `nodeFailed`: workflow code returned a normal failure.
401
+ - `protocolRejected`: a message failed schema, revision, attempt, or idempotency checks.
402
+
403
+ A failure in one class must not be reported as another. In particular, claim loss does not create a failed run event.
404
+
405
+ ## Status and privacy
406
+
407
+ `pi-workflows host status` reports:
408
+
409
+ - host state and epoch;
410
+ - socket availability;
411
+ - active worker count;
412
+ - queued, running, parked, and waiting counts;
413
+ - expired claim count;
414
+ - pending interaction count;
415
+ - ambiguous effect count.
416
+
417
+ It does not print actor IDs, session IDs, project paths, prompts, outputs, payloads, claim tokens, environment variables, or credentials.
418
+
419
+ Logs use bounded safe errors. Child stdout and stderr may contain private content and stay in the user-only workflow state directory. Public issue and pull-request text must use generic fixtures and no operator-specific identifiers.
420
+
421
+ ## Alpha state policy
422
+
423
+ This feature changes the current schema in place while the project is in alpha.
424
+
425
+ Keep `pi-workflows-state` and schema version 1. Change the DDL digest and current contracts directly. Add no compatibility reader, migration shim, dual read, dual write, alias, feature flag, or parallel state root.
426
+
427
+ When the installed state has the old digest, fail before mutation with the standard backup and reset instruction. Leave the old database untouched.
428
+
429
+ ## Pi API impact
430
+
431
+ - **Session state:** Pi appends normal messages and tool results. Pi Workflows does not edit session files.
432
+ - **Other persistent data:** The workflow SQLite shape changes in place and older alpha state requires reset.
433
+ - **Pi internals:** None.
434
+ - **Public API:** The extension uses documented command registration, tool registration, session lifecycle events, message sending, widgets, status, and session IDs.
435
+
436
+ ## Conformance
437
+
438
+ The implementation conforms when:
439
+
440
+ - every protected write checks and renews one live claim atomically;
441
+ - an expired or replaced owner cannot write;
442
+ - a blocked worker cannot stop host renewal;
443
+ - Pi can restart while work computes or waits;
444
+ - the host can restart and recover from committed state;
445
+ - run, queue, attempt, decision, lease, event, and viewer projections remain consistent after injected crashes;
446
+ - an expired running row can be resumed or cancelled safely;
447
+ - duplicate commands and submissions return stored receipts;
448
+ - an interactive request appears once in the origin session and survives reload;
449
+ - effects are deduplicated or marked ambiguous;
450
+ - the extension and host run no workflow or controller code in their own event loops;
451
+ - the production package contains no embedded execution fallback;
452
+ - real Pi end-to-end tests, repository checks, reviewer checks, and CI pass.
@@ -3,15 +3,18 @@
3
3
  This document covers the standards for working on pi-workflows itself. For
4
4
  authoring workflows, see [workflows.md](workflows.md).
5
5
 
6
+ The viewer follows the [incremental and virtualized viewer plan](plans/2026-08-28-piw-incremental-viewer-plan.md). Durable writers create revisioned target patches. Rust reads small run-list rows, bounded replay pages, and one shared watched-run projection. Rust and TypeScript share graph fixtures and the same retained scene contract.
7
+
6
8
  ## Layout and boundaries
7
9
 
8
10
  ```
9
11
  src/workflows/ finite graph engine: definitions, execution, SQLite stores, loader
10
12
  src/builtins/ default workflows shipped at lowest discovery precedence
11
13
  src/controllers/ durable resources, queue, reconciliation, effects, child runs
12
- src/extension/ pi integration: commands, workflow tool, controller host, widget
14
+ src/host/ global host, local protocols, resolvers, and worker supervision
15
+ src/extension/ thin Pi client: commands, workflow tool, and origin-session bridge
13
16
  src/viewer/ standalone read-only views over runs and controller resources
14
- tui/ Rust piw viewer and live replay server
17
+ tui/ Rust piw viewer, bounded SQLite projection, and replay server
15
18
  ```
16
19
 
17
20
  The dependency direction is enforced by `slophammer.yml`. `src/workflows`
@@ -20,10 +23,12 @@ progress validation, estimation, and text formatting stay in this layer so the
20
23
  engine, extension, hosts, and viewers share one contract. `src/builtins` contains
21
24
  package-owned definitions and imports only the public workflow engine.
22
25
  `src/controllers` may import the public workflow engine for child-run
23
- scheduling. `src/extension` and `src/host` may also import the built-in catalog.
24
- The extension and viewer never import each other. The viewer reads
25
- SQLite runs and opens the controller SQLite database read-only, so it works
26
- from any process.
26
+ scheduling. `src/host` may import controller stores and workflow stores, but the
27
+ host event loop never loads workflow or controller definitions. Resolver and
28
+ worker child entry points load those definitions. The extension uses static
29
+ built-in metadata and never imports the workflow engine. The extension and
30
+ viewer never import each other. The viewer reads SQLite state in read-only mode,
31
+ so it works from any process.
27
32
 
28
33
  Within `src/render`, `graph.ts` computes a pure layered layout (ported from
29
34
  the acpx replay viewer: labelled switch expansion, DFS back-edge detection,
@@ -31,12 +36,7 @@ longest-path layering, barycenter ordering, virtual pass-through cells for
31
36
  long edges), `canvas.ts` is a character grid that merges box-drawing
32
37
  characters by connectivity, and `graph-render.ts` turns a SQLite run state plus a
33
38
  replay position into the drawn graph in one of two node styles: `box`
34
- (bordered nodes, used by the viewer and the in-pi widget) or `line`
35
- (single-line nodes). The widget windows the boxed graph around the active
36
- node to stay inside pi's 10-line widget cap; `shift+↑`/`shift+↓` shortcuts
37
- (registered through pi's `registerShortcut`) scroll that window manually, and
38
- the scroll resets to follow mode when the run records a new step. `render.ts`
39
- in `src/viewer`
39
+ (bordered nodes) or `line` (single-line nodes). `render.ts` in `src/viewer`
40
40
  composes the full detail view (header, graph, step timeline, step inspector)
41
41
  and stays pure so tests can assert on rendered lines.
42
42
 
@@ -63,18 +63,16 @@ workflow shapes at every replay position; if a rendering change breaks a
63
63
  line, misplaces an arrow, or lets a label damage an edge, those tests fail
64
64
  with the offending drawing in the assertion message.
65
65
 
66
- Inside the engine, the pi-facing seam is the `AgentStepExecutor` interface.
67
- The extension implements it on top of the live conversation
68
- (`src/extension/executor.ts`), and tests implement it with a scripted fake
69
- (`test/helpers.ts`). Anything that would couple the engine to pi belongs on
70
- the extension side of that seam.
66
+ Inside the engine, the model-facing seam is the `AgentStepExecutor` interface.
67
+ The production run worker uses an interaction executor for origin-session work
68
+ and `RpcStepExecutor` for headless controller work. The worker receives a
69
+ `HostBackedWorkflowStore`; it cannot open a writable production store.
71
70
 
72
- Temporal session capture follows the same boundary. `src/extension` listens to
73
- Pi's documented `turn_*`, `message_*`, and `tool_execution_*` hooks and
74
- normalizes them before passing records to `WorkflowRunStore`. The workflows layer owns persisted shapes and schema validation. It also owns
75
- ordered append chains but never imports Pi types. High-rate hooks only stamp and enqueue bounded records;
76
- disk writes run on a separate chain. Capture failures are explicit in
77
- the `session_segments` capture status and never fail workflow execution.
71
+ The Pi extension uses documented session lifecycle, message, tool, and session
72
+ manager APIs. It writes no Pi session file. It saves host mutations through the
73
+ local protocol and uses the public session branch only to adopt the exact
74
+ visible entry for a durable interaction request. Tests can still use scripted
75
+ executors through the Pi-agnostic engine API.
78
76
 
79
77
  `src/viewer/session-reducer.ts` and `tui/src/session.rs` implement the same
80
78
  sequence-ordered fold. Shared fixtures in `fixtures/session-events/` pin their
@@ -104,6 +102,23 @@ npx slophammer-ts@latest dry .
104
102
  npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
105
103
  ```
106
104
 
105
+ ## Viewer performance checks
106
+
107
+ Create a synthetic growing database and run the release benchmark:
108
+
109
+ ```bash
110
+ mkdir -m 700 /tmp/piw-viewer-benchmark
111
+ npx tsx scripts/generate-viewer-benchmark.ts \
112
+ /tmp/piw-viewer-benchmark/state.sqlite 44 405 211 1105 1200
113
+ cargo run --release --manifest-path tui/Cargo.toml \
114
+ --example refresh_benchmark -- \
115
+ /tmp/piw-viewer-benchmark/state.sqlite 1000
116
+ ```
117
+
118
+ The generator prints only row counts and database size. The benchmark prints run count, query and load counts, payload rows, raw tick times, and peak RSS. It does not print run IDs or payload text.
119
+
120
+ The deterministic gate is zero payload reads after the first bounded selected window during unchanged idle checks. The measured gates are p99 main-thread checks below 16 ms and peak RSS below 185 MB on a database close to the registered 97 MB fixture. Run the benchmark several times and report the median, range, p99, maximum, and peak memory. Do not select extra complexity from one best run.
121
+
107
122
  ## End-to-end tests
108
123
 
109
124
  ```bash
@@ -120,10 +135,12 @@ the real pi CLI from `devDependencies` in RPC mode with:
120
135
  - `HOME` pointed at a temporary home containing the canonical workflow database,
121
136
  - the extension loaded from source with `-e src/extension/index.ts`.
122
137
 
123
- It drives `/workflow` over the RPC protocol and asserts on the resulting SQLite
124
- rows, including temporal events, final entry linkage, capture integrity, and
125
- terminal immutability, then renders the finished run through the viewer CLI.
126
- Nothing outside the temp directories is touched, and no real model is called.
138
+ It drives `/workflow` over the RPC protocol and checks the global host,
139
+ durable interaction request, origin-session presentation, accepted submission,
140
+ worker recovery, final SQLite state, and viewer output. It also restarts the
141
+ real Pi process while one request is pending and proves that the same request is
142
+ not inserted twice. Nothing outside the temp directories is touched, and no
143
+ real model is called.
127
144
 
128
145
  ## Publishing
129
146
 
@@ -155,9 +172,8 @@ before publishing.
155
172
  ## Conventions
156
173
 
157
174
  - Conventional Commits for commit messages and PR titles.
158
- - Persisted JSON uses camelCase keys and versioned `schema` identifiers; see
159
- [SQLITE_STATE.md](SQLITE_STATE.md). Breaking a persisted shape means bumping
160
- the schema version string.
175
+ - Persisted JSON uses camelCase keys and current `schema` identifiers; see
176
+ [SQLITE_STATE.md](SQLITE_STATE.md). During alpha, a breaking shape changes the current contract in place and uses the clear reset failure. Do not add a compatibility path or `v2` only to preserve old alpha state.
161
177
  - Every exported API of the engine (`src/workflows/index.ts`) is covered by
162
178
  unit tests; new node types or edge semantics need tests in `test/` and a
163
179
  section in [workflows.md](workflows.md).