@osolmaz/pi-workflows 0.13.4 → 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 (237) hide show
  1. package/README.md +146 -160
  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 +1 -2
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +85 -31
  19. package/dist/controllers/sqlite.js +541 -126
  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 -2928
  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/prune.js +36 -10
  94. package/dist/state/prune.js.map +1 -1
  95. package/dist/state/schema.d.ts +1 -1
  96. package/dist/state/schema.js +171 -3
  97. package/dist/state/schema.js.map +1 -1
  98. package/dist/state/viewer.d.ts +46 -0
  99. package/dist/state/viewer.js +249 -0
  100. package/dist/state/viewer.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +1 -1
  102. package/dist/viewer/cli.js +47 -18
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/workflows/composition.js +25 -2
  105. package/dist/workflows/composition.js.map +1 -1
  106. package/dist/workflows/definition.d.ts +3 -1
  107. package/dist/workflows/definition.js +25 -0
  108. package/dist/workflows/definition.js.map +1 -1
  109. package/dist/workflows/engine.d.ts +1 -0
  110. package/dist/workflows/engine.js +157 -42
  111. package/dist/workflows/engine.js.map +1 -1
  112. package/dist/workflows/errors.d.ts +3 -1
  113. package/dist/workflows/errors.js +4 -7
  114. package/dist/workflows/errors.js.map +1 -1
  115. package/dist/workflows/human-decision.d.ts +3 -0
  116. package/dist/workflows/human-decision.js +31 -0
  117. package/dist/workflows/human-decision.js.map +1 -1
  118. package/dist/workflows/index.d.ts +1 -1
  119. package/dist/workflows/index.js +1 -1
  120. package/dist/workflows/index.js.map +1 -1
  121. package/dist/workflows/schema.js +17 -6
  122. package/dist/workflows/schema.js.map +1 -1
  123. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  124. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  125. package/dist/workflows/session-reducer.js.map +1 -0
  126. package/dist/workflows/store.d.ts +87 -1
  127. package/dist/workflows/store.js +892 -93
  128. package/dist/workflows/store.js.map +1 -1
  129. package/dist/workflows/tool-input.d.ts +0 -22
  130. package/dist/workflows/tool-input.js +1 -44
  131. package/dist/workflows/tool-input.js.map +1 -1
  132. package/dist/workflows/types.d.ts +36 -4
  133. package/docs/2026-08-25-workflow-follow-ups.md +8 -6
  134. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  135. package/docs/CONTROLLERS.md +11 -11
  136. package/docs/DEFERRED_TURNS.md +61 -27
  137. package/docs/HUMAN_DECISIONS.md +12 -4
  138. package/docs/SQLITE_STATE.md +42 -8
  139. package/docs/WORKFLOW_HOST.md +452 -0
  140. package/docs/development.md +46 -30
  141. package/docs/live-replay-protocol.md +129 -100
  142. package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
  143. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
  144. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  145. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  146. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  147. package/docs/tui-viewer.md +18 -3
  148. package/docs/workflows.md +164 -156
  149. package/examples/workflows/command-batch.workflow.ts +2 -0
  150. package/examples/workflows/shell.workflow.ts +2 -1
  151. package/herdr-plugin.toml +1 -1
  152. package/package.json +1 -1
  153. package/skills/autodoc/SKILL.md +1 -1
  154. package/skills/autoimplement/SKILL.md +1 -1
  155. package/skills/autoplan/SKILL.md +1 -1
  156. package/skills/pi-workflows/SKILL.md +2 -0
  157. package/src/builtins/autoimplement.workflow.ts +3 -0
  158. package/src/builtins/change-verification.workflow.ts +7 -2
  159. package/src/builtins/metadata.ts +9 -0
  160. package/src/builtins/monitor.workflow.ts +4 -0
  161. package/src/builtins/sanity-check.workflow.ts +4 -0
  162. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  163. package/src/controllers/index.ts +3 -5
  164. package/src/controllers/sqlite.ts +929 -225
  165. package/src/controllers/store.ts +1 -0
  166. package/src/extension/controller-command.ts +45 -0
  167. package/src/extension/index.ts +948 -3506
  168. package/src/extension/message-card.ts +61 -0
  169. package/src/extension/step-message.ts +58 -63
  170. package/src/host/child-worker-supervisor.ts +183 -0
  171. package/src/host/client.ts +293 -0
  172. package/src/host/controller-worker-entry.ts +311 -0
  173. package/src/host/controller-worker-protocol.ts +104 -0
  174. package/src/host/controller-worker-supervisor.ts +79 -0
  175. package/src/host/host-entry.ts +23 -0
  176. package/src/host/processes.ts +171 -54
  177. package/src/host/protocol.ts +196 -0
  178. package/src/host/resolver-entry.ts +241 -0
  179. package/src/host/rpc-executor.ts +76 -34
  180. package/src/host/runner.ts +2813 -422
  181. package/src/host/state.ts +1160 -0
  182. package/src/host/worker-entry.ts +533 -0
  183. package/src/host/worker-protocol.ts +165 -0
  184. package/src/host/worker-store.ts +229 -0
  185. package/src/host/worker-supervisor.ts +74 -0
  186. package/src/render/canvas.ts +44 -10
  187. package/src/render/graph-render.ts +145 -90
  188. package/src/state/database.ts +2 -1
  189. package/src/state/index.ts +14 -0
  190. package/src/state/prune.ts +35 -9
  191. package/src/state/schema.ts +171 -3
  192. package/src/state/viewer.ts +356 -0
  193. package/src/viewer/cli.ts +49 -17
  194. package/src/workflows/composition.ts +36 -2
  195. package/src/workflows/definition.ts +32 -0
  196. package/src/workflows/engine.ts +157 -54
  197. package/src/workflows/errors.ts +11 -2
  198. package/src/workflows/human-decision.ts +49 -0
  199. package/src/workflows/index.ts +2 -0
  200. package/src/workflows/schema.ts +19 -6
  201. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  202. package/src/workflows/store.ts +1316 -108
  203. package/src/workflows/tool-input.ts +2 -60
  204. package/src/workflows/types.ts +32 -4
  205. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  206. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  207. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  208. package/dist/extension/controller-host.d.ts +0 -48
  209. package/dist/extension/controller-host.js +0 -110
  210. package/dist/extension/controller-host.js.map +0 -1
  211. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  212. package/dist/extension/deferred-turn-coordinator.js +0 -143
  213. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  214. package/dist/extension/deferred-turn.d.ts +0 -44
  215. package/dist/extension/deferred-turn.js +0 -110
  216. package/dist/extension/deferred-turn.js.map +0 -1
  217. package/dist/extension/executor.d.ts +0 -86
  218. package/dist/extension/executor.js +0 -311
  219. package/dist/extension/executor.js.map +0 -1
  220. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  221. package/dist/extension/follow-up-coordinator.js +0 -131
  222. package/dist/extension/follow-up-coordinator.js.map +0 -1
  223. package/dist/extension/recorder.d.ts +0 -84
  224. package/dist/extension/recorder.js +0 -528
  225. package/dist/extension/recorder.js.map +0 -1
  226. package/dist/extension/session-events.d.ts +0 -133
  227. package/dist/extension/session-events.js +0 -61
  228. package/dist/extension/session-events.js.map +0 -1
  229. package/dist/viewer/session-reducer.js.map +0 -1
  230. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  231. package/src/extension/controller-host.ts +0 -167
  232. package/src/extension/deferred-turn-coordinator.ts +0 -171
  233. package/src/extension/deferred-turn.ts +0 -166
  234. package/src/extension/executor.ts +0 -411
  235. package/src/extension/follow-up-coordinator.ts +0 -151
  236. package/src/extension/recorder.ts +0 -655
  237. package/src/extension/session-events.ts +0 -121
@@ -19,7 +19,7 @@ The controller runtime provides:
19
19
  - Recoverable records for external effects.
20
20
  - Child workflow runs with stable request keys.
21
21
  - Conditions and generations, with cleanup and structured events.
22
- - Local interactive use through the Pi extension and headless use through the engine API.
22
+ - Local resource control through the Pi extension and reconciliation through the global host.
23
23
 
24
24
  The first production use case is pull request automation. A controller can observe a pull request, start a review or repair workflow, wait for checks, validate the current head, and apply an approved change through deterministic code.
25
25
 
@@ -27,7 +27,7 @@ The first production use case is pull request automation. A controller can obser
27
27
 
28
28
  The graph engine remains the execution layer for finite work. A finite workflow can include another finite workflow in the same run through `includeWorkflow()`. Use a controller child run when work needs an independent retry history, stable request key, parallel lifecycle, or indefinite reconciliation. The graph engine does not import the controller runtime. The controller runtime may start workflows through a narrow scheduler interface.
29
29
 
30
- The Pi extension is a host. It discovers definitions, displays status, and supplies the conversation-backed agent executor while Pi is running. A headless host can use the same controller runtime with another `AgentStepExecutor`.
30
+ The Pi extension is a thin client. It resolves controller initialization in a dedicated child process and sends declarative resource commands to the one global host. The host schedules reconciliation but does not load controller definitions in its event loop. A supervised controller worker loads one definition and proposes bounded state changes through the host.
31
31
 
32
32
  External events are wake-up hints. An event enqueues a resource key and carries no transition command. The reconciler reloads the resource and the external system before deciding what to do.
33
33
 
@@ -155,15 +155,15 @@ export type EffectRecord = {
155
155
 
156
156
  The key names one intended effect. Reusing the key with another request fingerprint is an error. The next reconciliation observes the external system before retrying an existing pending or indeterminate effect. The effect can be treated as effectively once when the provider offers an idempotency token, a conditional request, or a reliable way to observe the requested result. The runtime does not promise generic exactly-once execution.
157
157
 
158
- Mutation policy stays in deterministic effect drivers. Agent workflows return findings or artifacts for deterministic code to check and apply. The in-process Pi host shares its process environment with agent tools, so it does not provide credential isolation. Deployments that require that boundary should put authenticated effects behind a separate broker or controller host.
158
+ Mutation policy stays in deterministic effect drivers. Agent workflows return findings or artifacts for deterministic code to check and apply. Worker processes run as the same operating-system user and are not credential sandboxes. Deployments that require credential isolation should put authenticated effects behind a separate broker.
159
159
 
160
160
  ## Child workflows
161
161
 
162
162
  `ctx.workflows.ensure()` creates or finds a workflow run by a stable request key and input fingerprint. Repeated reconciliations find the same active or completed request. A changed input must use a new key. An asynchronous child completion validates the reserved request and run IDs through a separate scheduler-completion command; it never reuses the controller claim that launched the child. The controller transaction reserves and saves each attempt's run ID before the scheduler starts it, so recovery can find the run row.
163
163
 
164
- A child run is one immutable attempt. Its existing SQLite run state remains the execution record. The parent resource points to the current run, and workflow completion enqueues the parent key. A host restart can record an abandoned attempt and create another attempt for the same stable request.
164
+ A child run has one durable execution record. The parent resource points to the current run, and workflow completion enqueues the parent key. The global host runs it through the same queue and supervised worker protocol as other workflows.
165
165
 
166
- The workflow scheduler records an abandoned running run as `failed` with a final `run_interrupted` event. The controller store treats that child attempt as `interrupted`, so the next reconciliation starts another immutable attempt. Compute work can run again, while consequential external actions belong in the effect API so recovery observes them before retrying.
166
+ A stopped worker does not make the child failed by itself. The host reads the last committed node and effect state. It resumes pure or idempotent work in a new worker epoch. An uncertain manual effect becomes ambiguous and blocks automatic retry. A changed input still requires a new stable request key.
167
167
 
168
168
  ## Deletion and cleanup
169
169
 
@@ -175,9 +175,9 @@ Controllers should add finalizers only when they own something that needs cleanu
175
175
 
176
176
  A source maps an external event to one or more resource keys. Sources include filesystem watches, webhooks, scheduled polling, and child workflow completion. They share the same enqueue API.
177
177
 
178
- `ControllerManager` sets global and per-controller worker limits. A reconciliation deadline stops lease renewal and requeues the key even when controller code ignores its abort signal. JavaScript cannot stop that non-cooperative promise, so reconciler code must pass the signal to provider calls and keep consequential writes inside guarded effect drivers. The local store supports expiring claims from the start, while the first release can run one process. Leader election belongs in a remote store implementation if several hosts later share the same resources.
178
+ The global host claims controller keys and starts a supervised process group for each active reconciliation. A reconciliation deadline stops and requeues the child even when controller code ignores its abort signal. Reconciler code must still pass the signal to provider calls and keep consequential writes inside guarded effect drivers. One host owns the local database. Distributed leader election remains outside this local runtime.
179
179
 
180
- The Pi extension starts local workers during `session_start` and closes them during `session_shutdown`. It can reconcile only while Pi is running. Another program can host `ControllerManager` through the public engine API. The package does not install a service.
180
+ The extension starts the package host on demand. Reconciliation continues when the Pi session closes. The package installs no operating-system service.
181
181
 
182
182
  ## Observability
183
183
 
@@ -202,13 +202,13 @@ A production controller must follow these rules:
202
202
 
203
203
  The controller API is exported from `@osolmaz/pi-workflows/controllers`. Controller definitions use a `.controller.ts` suffix. Project definitions live under `.pi/controllers/`; global definitions live under `~/.pi/agent/controllers/`.
204
204
 
205
- The implementation uses documented Pi extension APIs only. Commands and tools use `registerCommand` and `registerTool`. Session lifecycle uses `session_start` and `session_shutdown`. Workflow prompts use `sendUserMessage`, while status uses `setWidget` and `setStatus`.
205
+ The implementation uses documented Pi extension APIs only. `/controller` lists and inspects resources, applies specs, and requests reconciliation or deletion. There are no extension-local worker start or stop controls.
206
206
 
207
- The `/controller` command lists and inspects resources, applies specs, requests reconciliation or deletion, and starts or stops local workers. Stopping workers records an active child as interrupted, so a later worker can create another attempt.
207
+ For `apply`, a source resolver child discovers the named controller, verifies its exported name, computes `initialStatus(spec)`, and hashes the source. The host accepts the proposal only while the path still follows discovery rules and the exact digest still matches. Reconcile code runs only in a supervised controller worker.
208
208
 
209
- Controller resources use the canonical [SQLite state](SQLITE_STATE.md) database. `projects` separates repository-local resources by canonical project path. Controller claims use the shared lease generation, token, expiry, and expected resource revision. Effects and child workflows use the shared transactional outbox and deterministic receipts.
209
+ Controller resources use the canonical [SQLite state](SQLITE_STATE.md) database. `projects` separates repository-local resources by canonical project path. Controller claims use the shared lease generation, token, expiry, and expected resource revision. Effects and child workflows use durable request keys and receipts.
210
210
 
211
- The same database backs the standalone host (`pi-workflows host`). The host reconciles controllers without a Pi session and claims parked interactive runs from `run_queue`. Conversation children execute in headless `pi --mode rpc` sessions. A Pi session and the host can share the database safely because SQLite transactions and durable leases arbitrate ownership, but only one owner can mutate a resource at a time.
211
+ One global host owns that database for all Pi sessions and projects. The extension and mutating CLI paths are local protocol clients; viewers remain read-only. A controller child without an origin session uses a headless `pi --mode rpc` process for structured agent steps.
212
212
 
213
213
  Normal workflow prompts, tool calls, and replies remain part of the Pi session. No Pi internal type, private API, or persistent Pi schema changes.
214
214
 
@@ -1,8 +1,8 @@
1
1
  # Deferred workflow turns
2
2
 
3
- This specification defines how Pi Workflows schedules one successor agent turn after a workflow event stops or strands the current turn. It covers cancellation, timeout, terminal failure, launch failure, controller interruption, and claim loss.
3
+ This specification defines how Pi Workflows schedules one successor agent turn after a workflow event stops or strands the current turn. It covers every top-level interactive terminal result, cancellation, timeout, launch failure, controller interruption, and claim loss.
4
4
 
5
- The implementation plan is [Guarantee one successor turn after workflow interruption](plans/2026-08-21-deferred-turn-intents-plan.md).
5
+ The implementation plans are [Guarantee one successor turn after workflow interruption](plans/2026-08-21-deferred-turn-intents-plan.md) and [Workflow terminal decision and restart](plans/2026-08-27-workflow-terminal-restart-plan.md).
6
6
 
7
7
  ## Terms
8
8
 
@@ -18,10 +18,10 @@ The implementation plan is [Guarantee one successor turn after workflow interrup
18
18
  Each eligible source event creates at most one turn intent. Exactly one of these message paths can resolve it:
19
19
 
20
20
  1. a workflow agent prompt;
21
- 2. a completed or waiting result presentation;
21
+ 2. a result presentation;
22
22
  3. a factual fallback.
23
23
 
24
- All three paths claim the same intent before sending. A resolved intent cannot start another turn.
24
+ Every top-level interactive terminal run owns one intent. Its presentation and fallback claim that same intent before sending. A waiting presentation can resolve an earlier interruption intent, but waiting state does not create a terminal intent. A resolved intent cannot start another turn.
25
25
 
26
26
  Cancellation and process termination remain immediate. Pi Workflows does not keep the old assistant turn alive and does not wait for the successor before stopping active work.
27
27
 
@@ -56,25 +56,25 @@ Resolution records message delivery into the Pi session or the presence of the s
56
56
  Valid causes are:
57
57
 
58
58
  ```text
59
- agentCancelled | timedOut | failed | launchFailed | controllerInterrupted | claimLost
59
+ agentCancelled | timedOut | failed | launchFailed | controllerInterrupted | claimLost | terminal | cancelled
60
60
  ```
61
61
 
62
62
  The event policy is:
63
63
 
64
- | Event | Intent | Fallback rule |
65
- | -------------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------- |
66
- | Agent calls `workflow cancel` during an active workflow turn | Create before `ctx.abort()` when storage is available. | Make eligible after durable terminal cancellation. |
67
- | Agent step times out | Create before `ctx.abort()` when storage is available. | Keep ineligible while a recovery prompt can arrive. Make eligible on terminal timeout. |
68
- | Active workflow turn ends in terminal failure | Create or reuse the abort intent. | Make eligible after durable terminal failure. |
69
- | Workflow reports started, then crashes before its first prompt | Create after durable failure. | Create as eligible. |
70
- | Queued launch activation fails | Create after the queue row is durably failed. | Create as eligible. |
71
- | Controller interrupts an active workflow turn | Create before the turn abort when possible. | Durable terminal or handoff state decides eligibility. |
72
- | Active workflow turn loses its queue claim | Create before the turn abort when possible. | Keep ineligible while a new owner can continue. |
73
- | Completed or waiting result has a presentation | Do not create a new intent. | Resolve an existing intent through presentation. |
74
- | Direct `/workflow cancel` | Do not create. | No automatic model turn. |
75
- | Workflow pause | Do not create. | No automatic model turn. |
76
- | User Escape or held workflow | Do not create. | No automatic model turn. |
77
- | Session shutdown | Do not create. | A closing session cannot start another turn. |
64
+ | Event | Intent | Fallback rule |
65
+ | -------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------- |
66
+ | Top-level interactive run completes | Create one terminal intent. | Presentation and factual fallback compete for the intent. |
67
+ | Top-level interactive run fails or times out | Create or reuse the abort intent. | Make eligible after the terminal state is durable. |
68
+ | Agent or user cancels a top-level run | Create before turn abort when needed. | Make eligible after durable cancellation; the decision defaults to stopping. |
69
+ | Workflow reports started, then crashes before its first prompt | Create after durable failure. | Create as eligible. |
70
+ | Queued launch activation fails | Create after the queue row is durably failed. | Create as eligible. |
71
+ | Controller interrupts an active workflow turn | Create before the turn abort when possible. | Durable terminal or handoff state decides eligibility. |
72
+ | Active workflow turn loses its queue claim | Create before the turn abort when possible. | Keep ineligible while a new owner can continue. |
73
+ | Waiting result has a presentation | Do not create a terminal intent. | It can resolve an earlier interruption intent through presentation. |
74
+ | Controller child or internally owned run ends | Do not create a terminal intent. | Its owner receives the result. |
75
+ | Workflow pause | Do not create. | No automatic model turn. |
76
+ | User Escape or held workflow | Do not create. | No automatic model turn. |
77
+ | Session shutdown | Do not create. | A closing session cannot start another turn. |
78
78
 
79
79
  A terminal failure after a successful start tool result is eligible even when it did not first call `ctx.abort()`. This rule covers asynchronous runtime validation and startup failures that the user can see in the UI but the model cannot see in its current context.
80
80
 
@@ -219,10 +219,21 @@ The fallback custom message uses this details object:
219
219
  "schema": "pi-workflows.deferred-turn-message.v1",
220
220
  "turnIntentId": "deferred-turn:...",
221
221
  "runId": "20260821T081731Z-autoimplement-407480dd",
222
- "cause": "failed"
222
+ "cause": "failed",
223
+ "presentation": {
224
+ "workflowName": "autoimplement",
225
+ "state": "failed",
226
+ "reasonKind": "maxSteps",
227
+ "restart": {
228
+ "count": 0,
229
+ "limit": 3
230
+ }
231
+ }
223
232
  }
224
233
  ```
225
234
 
235
+ `presentation` contains small, bounded display fields. `reasonKind` and `restart` are present only when the run records provide them. These fields do not replace or shorten the model prompt.
236
+
226
237
  The message type is `pi-workflows-deferred-turn`. Delivery uses:
227
238
 
228
239
  ```ts
@@ -232,10 +243,32 @@ The message type is `pi-workflows-deferred-turn`. Delivery uses:
232
243
  }
233
244
  ```
234
245
 
235
- The visible content reports observed facts and asks the agent to inspect durable state before it decides whether an authorized correction is needed. It does not claim that recovery occurred, resume the old run, or retry work.
246
+ ### Compact TUI card
247
+
248
+ Interactive Pi registers a custom renderer for `pi-workflows-deferred-turn`. The collapsed card shows the workflow name, state or cause, run identity, and restart count when it is available. It does not show the terminal facts JSON, exact input, result, fingerprint, or model instructions.
249
+
250
+ The renderer reads only the structured message details for its compact fields. It sanitizes workflow-derived text and uses Pi's standard TUI components, theme colors, and `expanded` state. A message with missing or invalid details renders as a safe generic workflow card.
251
+
252
+ Expanding the card shows the complete existing message content. The model and session history receive that same content whether the card is collapsed or expanded. Restored messages use the same renderer and do not create another entry or model turn.
253
+
254
+ This display behavior uses the documented `pi.sendMessage()` and `pi.registerMessageRenderer()` APIs. It does not change headless or RPC delivery. Presentation messages that already use `display: false` stay hidden. It adds no Pi core change, private API, database table, migration, store, or external resource.
255
+
256
+ For a terminal run, the complete content contains the workflow identity and revision, terminal run ID, exact stored input, bounded result, terminal state and reason, restart count, and earlier terminal outcomes in the chain. It tells the model to use the current conversation, prefer a safe restart for an unfinished task after a technical or temporary failure, and stop for completed work, cancellation, missing authority, a required user decision, or a repeated failure. Values from input and result are data, not instructions.
257
+
258
+ The content comes only from existing run and queue records. Pi owns conversation history. Pi Workflows does not identify, hash, copy, or store an original user message.
236
259
 
237
260
  Fallback delivery is disabled during session shutdown and while a user-interrupted workflow is held.
238
261
 
262
+ ## Selected launch and restart
263
+
264
+ A terminal decision turn can reserve at most one workflow launch: `restart`, Monitor through normal `start`, or another workflow through normal `start`. The reservation records the source terminal intent, model tool call, and request fingerprint in the new run's existing launch options. It does not activate before `agent_settled`.
265
+
266
+ Repeating the same tool call adopts the existing reservation or run. A different launch from the same terminal intent fails. Session-start and queue recovery activate a surviving reservation once when the session is idle.
267
+
268
+ `restart` accepts the terminal run ID. It checks session ownership, terminal state, explicit cancellation, source identity and revision, repeated failure, and the restart limit. It creates a new immutable run from the exact stored reference, input, and safe launch settings. The old run does not change.
269
+
270
+ Restart lineage in launch options records the root run ID, parent run ID, restart number, and parent terminal fingerprint. The fingerprint covers workflow identity and revision, exact input, terminal state, canonical result or error, and terminal reason. It excludes timestamps and run IDs. The same fingerprint cannot restart twice in one chain. A chain permits three restarts after the original run. Starting Monitor does not add restart lineage.
271
+
239
272
  ## Delivery recovery
240
273
 
241
274
  Every resolving message includes the intent ID in its custom-message details.
@@ -262,7 +295,7 @@ Pending `launch_failure` rows from the earlier alpha contract are incompatible.
262
295
 
263
296
  ## Post-completion follow-ups
264
297
 
265
- Deferred turns repair one stranded workflow turn after interruption. Ordered post-completion prompts are different. They represent user-requested normal work after successful completion, use `workflow_follow_up_queues` and `workflow_follow_ups`, and are delivered by the separate follow-up coordinator. Neither feature reads or changes the other's rows.
298
+ Deferred turns provide the terminal decision before ordered post-completion prompts. Those prompts represent user-requested normal work after successful completion, use `workflow_follow_up_queues` and `workflow_follow_ups`, and are delivered by the separate follow-up coordinator after the terminal intent is resolved. Neither feature reads or changes the other's rows.
266
299
 
267
300
  See [Continue normal work after a workflow finishes](2026-08-25-workflow-follow-ups.md).
268
301
 
@@ -293,12 +326,13 @@ This is an alpha hard cutover.
293
326
 
294
327
  An implementation conforms when:
295
328
 
296
- - one eligible source event produces at most one intent;
297
- - one intent produces at most one successor message;
298
- - an agent self-cancel receives one fallback after settlement;
329
+ - every top-level interactive terminal run produces at most one intent and one decision message;
330
+ - an agent self-cancel and a direct cancellation receive one fallback after settlement;
299
331
  - an asynchronous crash after a successful start result receives one fallback after settlement;
300
332
  - a natural recovery prompt or presentation suppresses fallback by resolving the same intent;
301
- - direct cancellation, pause, Escape, user hold, and shutdown produce no automatic turn;
333
+ - waiting checkpoints, controller children, internal owners, pause, Escape, user hold, and shutdown do not create terminal turns;
302
334
  - claim transfer permits natural resolution by the new owner and prevents stale writes;
303
- - polling, restart, lease expiry, and send-before-resolution failure do not duplicate turns;
335
+ - one terminal turn reserves at most one launch and activates it after `agent_settled`;
336
+ - exact replay adopts the same launch, while reload, lease expiry, and send-before-resolution failure do not duplicate turns or runs;
337
+ - restart keeps the prior run immutable, preserves exact input, rejects cancellation and repeated fingerprints, and stops after three restarts;
304
338
  - passive workflow notifications keep their current behavior.
@@ -322,19 +322,23 @@ Recovery follows these rules:
322
322
 
323
323
  A required decision with no available channel remains waiting and reports the configuration problem. An automatic decision does not need a channel to apply its saved response after the deadline. A skipped plan policy creates no decision.
324
324
 
325
+ Direct answer handling and recovery can notice the same accepted decision at the same time. Both paths use one continuation coordinator. The coordinator prepares the deterministic continuation through the existing durable run queue. The first caller creates and claims the queue row. Only that caller receives the claim token and starts the engine. A compatible later caller adopts the existing row without starting another engine.
326
+
327
+ Adoption does not change the lease, claim generation, queue state, timestamps, or events. An adopter cannot renew, release, park, complete, or replace the winning claim. Reuse with a different workflow source, definition, input, launch options, parent, or owning session fails without changing the existing run. If the winning process stops after it claims the row, existing lease expiry and activation recovery can continue the prepared run.
328
+
325
329
  ## Compatibility
326
330
 
327
- This alpha change updates the current request, accepted-result, receipt, resolution, continuation, and snapshot contracts in place. Old active runs refuse resume through normal source and definition identity checks. There is no compatibility reader, migration, dual path, or new schema generation. Updated viewers label a human decision as a checkpoint, show its deadline and automatic action when present, and keep the canonical subject separate. Private channel configuration and transport identifiers remain hidden.
331
+ This alpha change updates the current request, accepted-result, receipt, resolution, continuation, and snapshot contracts in place. Old active runs refuse resume through normal source and definition identity checks. There is no compatibility reader, migration, dual path, or new schema generation. The continuation startup fix uses existing queue and lease records. It adds no field, table, migration, or schema version. Existing compatible prepared or initialized continuations are adopted. Updated viewers label a human decision as a checkpoint, show its deadline and automatic action when present, and keep the canonical subject separate. Private channel configuration and transport identifiers remain hidden.
328
332
 
329
333
  The engine remains independent from Pi and Telegram. Core code owns decision contracts, validation, durable acceptance, and continuation. The Pi extension owns UI and channel lifecycle. The Telegram adapter owns Bot API translation. Workflow definitions own only the question, choices, audience, and routes.
330
334
 
331
335
  ## Contract impact
332
336
 
333
337
  - **Session state:** Pi records normal workflow messages and interactive decision results.
334
- - **Other persistent data:** decision requests and resolutions can carry a deadline, automatic response, and resolution provenance in the existing decision store. The private channel index remains rebuildable.
338
+ - **Other persistent data:** decision requests and resolutions can carry a deadline, automatic response, and resolution provenance in the existing decision store. Continuation startup uses existing run, queue, source, binding, lease, event, continuation, and decision-effect records. It adds no new persistent shape. The private channel index remains rebuildable.
335
339
  - **Pi internals:** none.
336
340
  - **Public Pi API:** documented extension lifecycle and UI methods only.
337
- - **Public pi-workflows API:** typed human choices, `humanDecision().onTimeout`, `humanDecisionEdge()`, the channel interface, the plan approval policy, and the shared plan-change workflow.
341
+ - **Public pi-workflows API:** typed human choices, `humanDecision().onTimeout`, `humanDecisionEdge()`, the channel interface, the plan approval policy, the shared plan-change workflow, and the additive queue prepare-or-adopt operation.
338
342
 
339
343
  ## Verification requirements
340
344
 
@@ -350,6 +354,10 @@ The implementation must test:
350
354
  - unauthorized users and chats;
351
355
  - stale request digests;
352
356
  - concurrent Pi and Telegram answers;
357
+ - a direct verified answer racing recovery for the same accepted decision;
358
+ - one claim generation, one engine start, one continuation, and one execution of each continuation node;
359
+ - compatible continuation adoption without lease, queue, timestamp, or event mutation;
360
+ - incompatible continuation identity reuse without mutation;
353
361
  - identical and conflicting retries;
354
362
  - crashes before and after answer acceptance and continuation creation;
355
363
  - ambiguous Telegram sends;
@@ -357,4 +365,4 @@ The implementation must test:
357
365
  - decision cancellation and expiry;
358
366
  - included `plan-approval` routes and bounded replan loops;
359
367
  - viewer redaction; and
360
- - real Pi execution without real Telegram credentials or network calls.
368
+ - real Pi execution without real Telegram credentials or network calls, duplicate-start failures, revision conflicts, or a stranded running continuation.
@@ -8,11 +8,25 @@ Pi Workflows stores all live durable state in one database:
8
8
 
9
9
  There is one database for the user installation. Project and run IDs separate data inside it. Workflow targets do not read or write this database.
10
10
 
11
+ ## Viewer projection
12
+
13
+ The database includes the [incremental and virtualized viewer design](plans/2026-08-28-piw-incremental-viewer-plan.md).
14
+
15
+ `viewer_runs` stores one presentation revision and retained revision floor for each run. `viewer_deltas` stores ordered target patches by run, presentation revision, and delta index. `viewer_session_checkpoints` stores the bounded active message and tool state at each 256-event boundary. A viewer-visible transaction writes the domain change, advances the presentation revision, and writes its patch blobs before the same commit. Session-event transactions write each reached replay checkpoint in that transaction.
16
+
17
+ The store retains 256 presentation revisions. A reader with an older cursor must take a bounded snapshot. Patches use `add`, `replace`, `remove`, and `append`. They target small projection documents or pages. Patch creation does not reconstruct and compare complete run views.
18
+
19
+ `session_entries` and `session_events` have run-wide sequence numbers and indexed `(run_id, run_seq)` ranges. Step, trace, entry, and event reads contain at most 256 rows. Run-list queries read metadata, status, lease facts, and the presentation revision. They do not read payload bodies.
20
+
21
+ This is an in-place alpha schema change. The schema name and version remain `pi-workflows-state` version 1. The DDL digest and exact shape changed. An older alpha database fails with the standard reset instruction and remains untouched. There is no compatibility reader, migration shim, dual path, feature flag, alias, or `v2` schema.
22
+
11
23
  ## Storage boundary
12
24
 
13
25
  The database stores:
14
26
 
15
27
  - workflow definitions, runs, events, node attempts, outputs, and updates
28
+ - global host epochs, command receipts, worker epochs, and worker messages
29
+ - durable origin-session interaction requests and submissions
16
30
  - captured Pi session entries and events
17
31
  - run and controller queues, claims, retries, and continuations
18
32
  - human-decision requests, submissions, resolutions, and cancellations
@@ -98,7 +112,8 @@ The shared records do not replace domain schemas. The following `STRICT` tables
98
112
  | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
99
113
  | Schema and projects | `schema_meta`, `projects` |
100
114
  | Content | `blobs` |
101
- | Shared lifecycle | `resources`, `leases`, `events` |
115
+ | Shared lifecycle | `resources`, `leases`, `events`, `workflow_host_state` |
116
+ | Host protocol | `host_commands`, `run_workers`, `worker_messages`, `interactive_requests`, `interactive_submissions` |
102
117
  | Workflows | `workflow_definitions`, `runs`, `run_sources`, `run_steps`, `run_bindings`, `run_queue`, `node_attempts`, `workflow_updates` |
103
118
  | Live settings | `workflow_settings`, `workflow_setting_changes` |
104
119
  | Post-run follow-ups | `workflow_follow_up_queues`, `workflow_follow_ups` |
@@ -111,6 +126,24 @@ The shared records do not replace domain schemas. The following `STRICT` tables
111
126
 
112
127
  Foreign keys join projects, runs, attempts, decisions, controllers, effects, and channel records. Partial unique indexes enforce one active node attempt per run, one queued or running reservation per Pi session, one decision winner, and one deterministic effect key. A parked waiting parent does not block its continuation. Reserving that continuation settles the parked parent queue in the same transaction, so a failed reservation leaves the parent recoverable.
113
128
 
129
+ ### Hosted commands and interactions
130
+
131
+ `workflow_host_state` stores the one current host epoch and its live local
132
+ claim. `host_commands` stores each client request fingerprint, operation,
133
+ outcome, revision, and receipt or error. Repeating an exact request adopts the
134
+ stored receipt. Reusing an ID or idempotency key for another request is a
135
+ conflict.
136
+
137
+ `run_workers` records each worker epoch before spawn and later records its exact
138
+ process identity and terminal outcome. `worker_messages` deduplicates accepted
139
+ state-changing child messages.
140
+
141
+ `interactive_requests` owns durable origin-session work. It stores the run,
142
+ node attempt, target session, contract, presentation claim, accepted
143
+ submission, and revision. `interactive_submissions` stores the idempotency key,
144
+ payload, outcome, and receipt. Pi reload can adopt the saved presentation entry
145
+ without inserting another visible message.
146
+
114
147
  ## Content-addressed values
115
148
 
116
149
  `blobs` stores canonical JSON and UTF-8 text as bytes. Its primary key is the 32-byte SHA-256 digest of the bytes.
@@ -125,9 +158,9 @@ Readers derive `steps`, `outputs`, `results`, carried-step count, current-node f
125
158
 
126
159
  An agent definition records `expectedOutput` as either a submitted-output description or `{ "kind": "assistant-message", "maxChars"?: number }`. Omitted `maxChars` means that Pi Workflows adds no character limit.
127
160
 
128
- A completed interactive assistant-message attempt uses the settled Pi response entry as its output. It does not store a second output blob. Its small receipt keeps the text digest, final Pi session entry ID, optional author-supplied limit, and whether recovery adopted an existing response. A noninteractive attempt with no captured response entry keeps one normal output blob.
161
+ A completed interactive assistant-message attempt stores the accepted visible text as its node output. Its small receipt keeps the text digest, final Pi session entry ID, optional author-supplied limit, and whether recovery adopted an existing response.
129
162
 
130
- An interrupted assistant-message attempt keeps its attempt ID when the origin Pi session resumes it. The executor adopts a matching completed assistant child from the active Pi branch instead of displaying the response twice. Submitted and non-agent attempts keep their normal fresh-attempt resume behavior.
163
+ An interrupted assistant-message attempt keeps its attempt ID. The extension adopts a matching durable request and existing Pi branch entry instead of displaying the prompt or accepting the response twice. Submitted and non-agent attempts use a fresh execution attempt after an uncommitted worker exit.
131
164
 
132
165
  ## Write contract
133
166
 
@@ -139,6 +172,7 @@ verify the exact schema
139
172
  verify the actor and operation
140
173
  verify the expected resource revision
141
174
  verify the claim token, generation, and expiry when ownership is required
175
+ renew that exact still-live token and generation
142
176
  verify the domain transition
143
177
  write content-addressed values
144
178
  increment the resource revision
@@ -165,7 +199,7 @@ Reading or finding a row never gives write authority.
165
199
  - Control commands have narrow explicit operations, such as requesting cancellation or deletion.
166
200
  - Model-originated workflow answers cannot resolve protected human decisions.
167
201
 
168
- Stores check ownership in the same transaction as the write. Shared scans, status commands, lists, viewers, and the Rust `piw` program are read-only.
202
+ The global host is the normal state writer. Its protected stores check ownership and renew the exact live claim in the same transaction as the write. A stale or expired owner cannot renew itself. Pi extensions and mutating CLI commands use the local host protocol. Shared scans, status commands, lists, viewers, and the Rust `piw` program are read-only.
169
203
 
170
204
  ## Competing outcomes
171
205
 
@@ -185,9 +219,9 @@ Status is a pure projection of domain rows, immutable facts, current leases, and
185
219
 
186
220
  A settings scope uses its resource revision as its public change number. Each accepted patch, current value, and node binding is saved in one transaction. A checkpoint continuation keeps the same settings resources and transfers them to the continuation run.
187
221
 
188
- A follow-up queue records acceptance order and final-presentation state. Successful terminalization changes queued items to ready or pending presentation in the same transaction as the terminal run fact. Failure, timeout, and cancellation cancel unsent items. Delivery uses item leases and active Pi branch evidence.
222
+ A follow-up queue records acceptance order and settlement state. Failure, timeout, and cancellation cancel unsent items.
189
223
 
190
- - A terminal run fact overrides stale queue presentation.
224
+ - A terminal run fact overrides stale delivery state.
191
225
  - An accepted decision is accepted even if its continuation effect is still pending.
192
226
  - A cancelled decision is cancelled even if parent cleanup is still pending.
193
227
  - A stale owner is not shown as current.
@@ -197,7 +231,7 @@ Read paths do not repair state. Owner reconcilers apply pending effects and writ
197
231
 
198
232
  ## Projects and concurrency
199
233
 
200
- All projects use the same file. `projects` stores a stable ID and canonical path. Project-scoped controller and run queries use that key. Standalone host lock and child-process registry files use a project hash under the workflow state directory, so different projects can run hosts concurrently while two hosts for one project still conflict.
234
+ All projects use the same file. `projects` stores a stable ID and canonical path. Project-scoped controller and run queries use that key. One global host owns the file for the user installation. Its socket, lock, and exact child-process registry are under `~/.pi/agent/workflows/host/`. A second live host is rejected even when it was started from another project.
201
235
 
202
236
  SQLite WAL permits concurrent readers while one writer commits. Writers are serialized by SQLite and must keep transactions short. Hashing, model calls, shell work, and external requests happen outside write transactions.
203
237
 
@@ -235,4 +269,4 @@ It does not print actor IDs, channel references, payloads, or credentials.
235
269
 
236
270
  This is a hard cut. Pi Workflows has no normal reader or writer for older live storage. It does not use dual reads, dual writes, aliases, versioned state roots, or automatic import.
237
271
 
238
- Older state remains untouched. If it is present when a new database would be created, Pi Workflows fails with a clear instruction instead of guessing or deleting data.
272
+ Older state remains untouched. Pi Workflows fails before mutation with this instruction: “Pi Workflows durable state is incompatible. Back up and move state.sqlite with its -wal and -shm files, then start Pi Workflows to create a new state.sqlite database. The incompatible state was not changed.”