@osolmaz/pi-workflows 0.16.1 → 0.16.2

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 (57) hide show
  1. package/dist/client/view.d.ts +6 -0
  2. package/dist/client/view.js +1 -0
  3. package/dist/client/view.js.map +1 -1
  4. package/dist/controllers/sqlite.d.ts +8 -0
  5. package/dist/controllers/sqlite.js +54 -1
  6. package/dist/controllers/sqlite.js.map +1 -1
  7. package/dist/extension/index.js +34 -15
  8. package/dist/extension/index.js.map +1 -1
  9. package/dist/extension/workflow-message-coordinator.d.ts +3 -2
  10. package/dist/extension/workflow-message-coordinator.js +78 -26
  11. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  12. package/dist/host/runner.d.ts +8 -1
  13. package/dist/host/runner.js +231 -176
  14. package/dist/host/runner.js.map +1 -1
  15. package/dist/host/view.js +6 -2
  16. package/dist/host/view.js.map +1 -1
  17. package/dist/host/worker-entry.d.ts +4 -1
  18. package/dist/host/worker-entry.js +23 -24
  19. package/dist/host/worker-entry.js.map +1 -1
  20. package/dist/host/worker-protocol.d.ts +15 -0
  21. package/dist/host/worker-protocol.js.map +1 -1
  22. package/dist/state/workflow-messages.d.ts +2 -0
  23. package/dist/state/workflow-messages.js +31 -0
  24. package/dist/state/workflow-messages.js.map +1 -1
  25. package/dist/workflows/composition.js +0 -4
  26. package/dist/workflows/composition.js.map +1 -1
  27. package/dist/workflows/definition.js +0 -8
  28. package/dist/workflows/definition.js.map +1 -1
  29. package/dist/workflows/engine.js +4 -5
  30. package/dist/workflows/engine.js.map +1 -1
  31. package/dist/workflows/schema.js +0 -4
  32. package/dist/workflows/schema.js.map +1 -1
  33. package/dist/workflows/store.d.ts +15 -0
  34. package/dist/workflows/store.js +42 -0
  35. package/dist/workflows/store.js.map +1 -1
  36. package/dist/workflows/types.d.ts +1 -3
  37. package/docs/2026-09-04-workflow-run-state-plan.md +363 -0
  38. package/docs/SQLITE_STATE.md +8 -4
  39. package/docs/WORKFLOW_HOST.md +14 -5
  40. package/docs/workflows.md +10 -1
  41. package/herdr-plugin.toml +1 -1
  42. package/package.json +1 -1
  43. package/src/client/view.ts +8 -0
  44. package/src/controllers/sqlite.ts +83 -1
  45. package/src/extension/index.ts +37 -19
  46. package/src/extension/workflow-message-coordinator.ts +91 -27
  47. package/src/host/runner.ts +277 -228
  48. package/src/host/view.ts +6 -2
  49. package/src/host/worker-entry.ts +38 -30
  50. package/src/host/worker-protocol.ts +11 -0
  51. package/src/state/workflow-messages.ts +51 -0
  52. package/src/workflows/composition.ts +0 -5
  53. package/src/workflows/definition.ts +0 -8
  54. package/src/workflows/engine.ts +4 -6
  55. package/src/workflows/schema.ts +0 -6
  56. package/src/workflows/store.ts +64 -0
  57. package/src/workflows/types.ts +1 -3
@@ -44,9 +44,7 @@ export type WorkflowEffectRecovery = "idempotent" | "manual";
44
44
  export type WorkflowManagedEffect = {
45
45
  /** Stable external effect category, for example `github.comment`. */
46
46
  type: string;
47
- /** Stable key. Idempotent adapters must pass this key to the external system. */
48
- idempotencyKey: string | ((context: WorkflowNodeContext) => MaybePromise<string>);
49
- /** Canonical request data used to reject key reuse with another operation. */
47
+ /** Canonical request data used to reject engine-owned key reuse with another operation. */
50
48
  request: unknown | ((context: WorkflowNodeContext) => MaybePromise<unknown>);
51
49
  /** `manual` never retries after an uncertain child exit. */
52
50
  recovery: WorkflowEffectRecovery;
@@ -0,0 +1,363 @@
1
+ ---
2
+ title: Unify workflow run state
3
+ author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
4
+ date: 2026-09-04
5
+ status: implemented
6
+ ---
7
+
8
+ # Unify workflow run state
9
+
10
+ ## Goal
11
+
12
+ Several Pi Workflows failures appeared together after the out-of-process host change. A provider error left old workflow-turn ownership behind. A composed workflow reused an effect key. Restart treated a failed run as a checkpoint continuation. Terminal code read a text error as JSON. That read error blocked cancellation, while the host repeatedly started a worker that could not make progress.
13
+
14
+ These failures have the same architectural cause. Important facts are inferred in more than one place. Local node names stand in for compiled identities. A parent run ID stands in for the kind of child run. Callers guess the type of stored data. Process activity stands in for workflow progress. Presentation work can determine whether a control command commits.
15
+
16
+ The approved design gives each fact one owner and one typed path. The compiler and engine create execution identities. The host state store commits workflow state. A worker executes one explicit run command. The Pi extension delivers messages and reports public Pi events. Renderers display the host view.
17
+
18
+ This plan replaces the narrower workflow-turn ownership plan. It keeps that fix and adds the effect, restart, terminal-data, cancellation, and worker-retry work needed to remove the shared cause.
19
+
20
+ ## User requirements
21
+
22
+ - Use the most direct, long-term design instead of separate guards for each symptom.
23
+ - Keep one global out-of-process host as the normal state writer.
24
+ - Keep one client protocol and one production runtime.
25
+ - Use documented Pi extension APIs only.
26
+ - Do not change Pi core or Pi session schemas.
27
+ - Keep schema version 1 and make an alpha hard cut. Do not add migrations, compatibility readers, dual paths, fallbacks, or feature flags.
28
+ - Preserve strict ownership and uniqueness checks.
29
+ - Do not claim exactly-once behavior for model calls or external systems.
30
+ - A paused workflow must remain pausable and cancellable after any earlier run failure.
31
+
32
+ ## Confirmed causes
33
+
34
+ ### Composed effects lose their full node path
35
+
36
+ `manualEffect()` and `idempotentEffect()` derive a key from `state.currentNode`. Workflow composition projects the state into a child workflow before it evaluates that key. The projection changes a compiled path such as `documentation/workspace/inspect` back to the local name `inspect`.
37
+
38
+ The outer workspace check and the documentation workspace check therefore shared the run ID and visit number. They also shared the effect type and local node name. Their requests differed, so the effect store correctly rejected the second request with `Managed effect key was reused with another request`.
39
+
40
+ ### Restart uses checkpoint continuation
41
+
42
+ The run queue records whether a child is a `continuation` or a `restart`. The worker bootstrap receives only `parentRunId`. It calls `continueRun()` for every child with a parent.
43
+
44
+ `continueRun()` requires a parent that is waiting at a checkpoint. A failed terminal run does not meet that contract. A restart must begin at the workflow start with fresh graph and effect state.
45
+
46
+ ### A failed ancestor error is read with the wrong type
47
+
48
+ Run errors are stored as text. `terminalAncestorOutcomes()` reads an ancestor `error_hash` with `readJson()`. The blob exists, but its media type is text. The generic database error says that the JSON blob is missing or has the wrong media type.
49
+
50
+ Terminal-message creation calls this reader. As a result, the error can block child failure handling and cancellation.
51
+
52
+ ### A worker can restart forever without progress
53
+
54
+ When an uninitialized worker exits, the host parks and claims the same run again. The failed restart never changes its saved workflow revision, but the host keeps launching it. The UI reports `running` because a process is active even though the workflow makes no progress.
55
+
56
+ ### Presentation can roll back control state
57
+
58
+ Cancellation and terminal-message creation currently share a transaction path. If terminal-message creation cannot read its source data, the cancellation fails too. A display failure must not keep execution authority alive.
59
+
60
+ ## Design
61
+
62
+ ### Clear responsibilities
63
+
64
+ The compiler assigns complete node paths. The engine uses those paths to identify logical work. Child workflows can receive a local view of inputs and outputs, but that local view cannot replace the compiled identity.
65
+
66
+ The host state store owns every durable transition. Host orchestration code asks the store to start, pause, finish, cancel, or recover work. It does not update lifecycle tables directly.
67
+
68
+ The worker receives one explicit command and executes it. It does not infer the command from nullable fields.
69
+
70
+ The Pi extension keeps only temporary state for the current Pi turn. A host response decides whether that turn belongs to a workflow.
71
+
72
+ Renderers use the host view. They do not infer workflow progress from process presence, messages, or local timers.
73
+
74
+ ### Complete execution identity
75
+
76
+ Each logical node visit has a stable identity made from:
77
+
78
+ - the run ID;
79
+ - the full compiled node path;
80
+ - the visit number.
81
+
82
+ The visit number is the count for that exact compiled node path. A crash retry of the same unfinished visit keeps the same number. Re-entering the node later gets the next number.
83
+
84
+ The engine derives managed-effect identity from this logical node visit and the effect type. The projected child context is still used to calculate the effect request, but it is not used to calculate internal identity.
85
+
86
+ Workflow authors no longer provide the internal effect key. If an external API needs its own idempotency key, the action sends that external key as part of the request to that API. Internal execution identity and an external API key are separate facts.
87
+
88
+ The effect store keeps its request fingerprint check. Reusing one logical effect identity with a different request remains a hard conflict.
89
+
90
+ ### One run command
91
+
92
+ The host sends the worker one tagged command:
93
+
94
+ - `start` begins a root run.
95
+ - `resume` continues the same interrupted run from saved state.
96
+ - `continue` creates a child from an accepted waiting checkpoint.
97
+ - `restart` creates a fresh child after a terminal result.
98
+
99
+ The worker handles this command with one exhaustive switch. Invalid combinations fail before workflow code runs.
100
+
101
+ A restart uses the same approved workflow definition and requested input, but it begins at the first node. It does not copy parent steps, outputs, results, open interactions, settings mutations, or effect reservations. The parent run ID and root run ID remain saved as history. The saved history also includes the restart number and terminal fingerprint.
102
+
103
+ A continuation remains the only path that carries checkpoint state forward. It requires a waiting parent and the exact accepted decision or submission.
104
+
105
+ The engine should expose one internal entry point that accepts this tagged command. The superseded parent-ID heuristic is removed in the same alpha change.
106
+
107
+ ### One durable transition interface
108
+
109
+ The existing run store becomes the only interface used by host orchestration for durable run changes. It can use focused internal modules, but callers see typed operations rather than SQL or booleans.
110
+
111
+ The interface covers:
112
+
113
+ - starting and ending exact workflow turns;
114
+ - reserving and settling exact effects;
115
+ - starting, pausing, and resuming runs;
116
+ - finishing and timing out runs;
117
+ - cancelling runs;
118
+ - preparing continuations and restarts;
119
+ - recording worker failure and recovery state.
120
+
121
+ Each operation checks the run generation and claim token in one transaction. It also checks the expected revision and run state against the exact resource identity. It returns one of these results:
122
+
123
+ - `applied` with the new saved value;
124
+ - `adopted` with an equal saved value;
125
+ - `conflict` with the conflicting evidence;
126
+ - `recoveryRequired` with the saved reason.
127
+
128
+ A boolean result is not sufficient for a lifecycle change. Callers must know whether another path already completed the same change or whether the request conflicts with current state.
129
+
130
+ ### Workflow-turn ownership
131
+
132
+ `workflowTurn.report` remains the single version-1 operation for Pi model turns.
133
+
134
+ At Pi `agent_start`, the extension proposes the exact sent workflow message and a stable turn request ID. The host atomically returns an active turn or reports that no workflow owns the Pi turn. The extension starts workflow recording only after an active result.
135
+
136
+ At Pi `agent_end`, the extension ends the exact active turn. Matching repeated reports return the saved result. Conflicting evidence remains an error.
137
+
138
+ When a run becomes terminal, the same state transaction ends its remaining open turns as `lost` and cancels still-pending workflow messages. A late report cannot revive the run. If a connection closes at an uncertain point, the extension does not attach the next Pi turn. The existing branch and idle report lets the host settle an unproved open turn as `lost`.
139
+
140
+ ### Typed run data
141
+
142
+ Run input and final output are JSON. Run error and presentation instructions are text. Code reads them through named store methods:
143
+
144
+ - `readRunInput()`;
145
+ - `readRunFinalOutput()`;
146
+ - `readRunError()`;
147
+ - `readPresentationInstructions()`;
148
+ - `readTerminalFacts()`.
149
+
150
+ Callers do not read these blob hashes directly. `readTerminalFacts()` is the only source for terminal messages and ancestor outcomes. It checks media types and returns one typed result.
151
+
152
+ The underlying content-addressed blob store remains general. The run store owns the meaning of each run column.
153
+
154
+ ### Control state before presentation
155
+
156
+ Finishing or cancelling a run commits its execution state first. That transaction saves the final status and ends open turns. It cancels pending interactions and messages before it releases the claim. It also records the terminal facts needed for presentation.
157
+
158
+ Terminal Pi-message creation is an idempotent follow-up transition derived from those saved facts. A host restart can create a missing terminal message later. A presentation error is recorded for repair, but it cannot undo the terminal state or retain execution authority.
159
+
160
+ This separation applies to completion, failure, timeout, and cancellation.
161
+
162
+ ### Retry only after progress
163
+
164
+ Every worker launch records the run revision it received. A normal worker exit reports a typed outcome to the host. Known bootstrap and workflow errors include the phase and error text.
165
+
166
+ If a worker process disappears without a report, the host compares the current run revision with the launch revision. It may resume automatically only when durable progress or a saved recovery transition changed that revision.
167
+
168
+ If no revision changed, the host parks the run with the worker failure and stops automatic launch. The user can inspect, cancel, or explicitly resume it. This rule has no arbitrary retry count. The same unchanged state is never executed in a tight loop.
169
+
170
+ The display reports `running` only while one accepted worker or origin-session turn is doing current work. A parked no-progress failure reports its recovery reason.
171
+
172
+ ## State and protocol changes
173
+
174
+ This is an alpha hard cut in schema and protocol version 1. The implementation must not add a version-2 identifier or a compatibility path.
175
+
176
+ Expected changes include:
177
+
178
+ - a tagged worker run command in the existing worker protocol;
179
+ - full compiled node identity for managed effects;
180
+ - typed results for lifecycle transitions and turn reports;
181
+ - typed run-data readers;
182
+ - durable no-progress worker failure evidence if the existing event records cannot express it without inference.
183
+
184
+ Prefer existing tables and event records when they express the contract directly. If the durable no-progress rule requires a new column or constraint, change version-1 DDL in place. An old database then fails before mutation with the standard backup-and-reset instruction.
185
+
186
+ Existing structurally valid state can use normal terminal and idle-session reconciliation. Do not add migration code to repair old table shapes.
187
+
188
+ ## Implementation plan
189
+
190
+ ### Freeze the failures in tests
191
+
192
+ Add focused tests that reproduce the current behavior before changing it:
193
+
194
+ - Mount workspace preparation directly and again under documentation, then prove the current automatic effect key collides.
195
+ - Restart a failed workflow and prove the worker selects checkpoint continuation.
196
+ - Build terminal ancestry with a failed parent and prove ancestor reading uses the wrong media type.
197
+ - Cancel a child of a failed run and prove presentation failure rolls back cancellation.
198
+ - Crash an uninitialized worker without a revision change and prove the host repeatedly claims it.
199
+ - End a workflow turn during a terminal race and prove a later ordinary Pi turn can inherit old ownership.
200
+
201
+ The final versions of these tests must assert the corrected behavior. Do not preserve assertions for the defects.
202
+
203
+ ### Move execution identity into the engine
204
+
205
+ Change managed-effect identity in `src/workflows/definition.ts`, `src/workflows/composition.ts`, `src/workflows/engine.ts`, and the related types.
206
+
207
+ The compiled engine passes the full node path and visit number to effect reservation. Child-context projection remains limited to workflow-authored request and action functions. Remove the old automatic key calculation from projected `state.currentNode`.
208
+
209
+ Update authoring docs and all built-in workflows in the same change. Keep no alias for the old internal key contract.
210
+
211
+ ### Make worker commands explicit
212
+
213
+ Add the tagged run command to `src/host/worker-protocol.ts`, the host bootstrap response, and `src/host/worker-entry.ts`.
214
+
215
+ Replace the `initialized` and `parentRunId` dispatch heuristic with an exhaustive command switch. Add the engine path for a fresh restart and keep checkpoint continuation separate. Remove the superseded dispatch code.
216
+
217
+ ### Centralize durable transitions
218
+
219
+ Move direct lifecycle SQL from `src/host/runner.ts` into typed store operations in the state and workflow-store modules. Replace ambiguous boolean returns used by run failure and cancellation with typed results.
220
+
221
+ Keep claim fencing and revision checks in each state transaction. The runner handles process supervision and protocol routing only.
222
+
223
+ ### Fix terminal state and cancellation
224
+
225
+ Add the typed run-data readers and use `readTerminalFacts()` for the current result and every ancestor result. Remove direct JSON reads of error hashes.
226
+
227
+ Commit terminal control state independently from terminal-message creation. Add idempotent reconciliation for a terminal run that has no terminal workflow message.
228
+
229
+ After this change, the currently paused failed-child case must cancel without a database reset.
230
+
231
+ ### Finish workflow-turn ownership
232
+
233
+ Implement the approved host-owned turn contract in the state store and host runner. Apply it to the extension coordinator and session view. Then connect it to recorder integration.
234
+
235
+ A terminal run must have no open workflow turn. A later ordinary Pi turn must produce no workflow-turn report for the terminal run.
236
+
237
+ ### Stop no-progress worker loops
238
+
239
+ Add structured worker exit reports for all caught bootstrap and execution failures. Save the launch revision and compare it on an unreported exit.
240
+
241
+ Park an unchanged run with a clear recovery reason. Resume automatically only after a newer durable revision makes another launch meaningful.
242
+
243
+ ### Remove old paths
244
+
245
+ Delete:
246
+
247
+ - effect keys derived from projected local node names;
248
+ - restart dispatch based only on `parentRunId`;
249
+ - direct error-hash JSON reads;
250
+ - lifecycle SQL in host orchestration where a typed store operation replaces it;
251
+ - cancellation paths that depend on successful terminal presentation;
252
+ - automatic relaunch of the same unchanged worker state;
253
+ - extension turn ownership that has not been accepted by the host.
254
+
255
+ Do not keep feature flags or fallback behavior.
256
+
257
+ ### Update documentation
258
+
259
+ Update `WORKFLOW_HOST.md`, `SQLITE_STATE.md`, and `workflows.md` after implementation so they describe the shipped interfaces and recovery behavior. Keep this plan as the decision and implementation record.
260
+
261
+ ## Tests
262
+
263
+ ### Composition and effects
264
+
265
+ - Two mounts of the same action receive different full execution identities.
266
+ - A crash retry of one unfinished node visit reuses its identity.
267
+ - A later loop visit receives a new identity.
268
+ - The same identity and request adopts the saved effect.
269
+ - The same identity with another request returns a controlled conflict.
270
+
271
+ ### Run commands
272
+
273
+ - Root start begins at the first node.
274
+ - Resume uses the same run and saved current node.
275
+ - Continuation accepts only a waiting checkpoint parent and carries the approved state.
276
+ - Restart accepts a terminal parent and begins with no parent steps, outputs, results, or effects.
277
+ - Invalid command and state combinations fail before workflow code runs.
278
+
279
+ ### Terminal state
280
+
281
+ - Completed, failed, timed-out, and cancelled ancestors produce typed terminal facts.
282
+ - A text error is never passed to `readJson()`.
283
+ - Cancellation commits even when terminal presentation is forced to fail.
284
+ - A later reconciliation creates the missing terminal message once.
285
+ - Terminalization ends open turns and pending interactions in the same state transaction.
286
+
287
+ ### Worker recovery
288
+
289
+ - A worker crash after a newer durable revision resumes safely.
290
+ - A worker crash at the same revision parks once and does not relaunch.
291
+ - A known bootstrap error becomes a saved failure instead of a process loop.
292
+ - The widget and `piw` show the same parked reason.
293
+
294
+ ### Pi turn ownership
295
+
296
+ - A provider error ends the exact accepted turn.
297
+ - Repeated and reordered reports return saved results or controlled conflicts.
298
+ - A terminal race cannot leave an open turn.
299
+ - Reconnect settles an unproved turn as `lost`.
300
+ - A later ordinary Pi message creates no workflow turn or workflow recorder.
301
+
302
+ ### Full regression
303
+
304
+ Run Autoimplement with its direct workspace preparation and nested Autodoc workspace preparation. Force the first run to fail after a managed action. Restart it and then pause and cancel it. Prove that effect identities do not collide and restart begins cleanly. Also prove that terminal facts remain readable and cancellation commits without a worker loop.
305
+
306
+ Automated tests use temporary directories and deterministic providers. They do not call a real model.
307
+
308
+ ## Acceptance criteria
309
+
310
+ - One component owns each identity and state decision.
311
+ - Included workflows cannot collide because they share local node names.
312
+ - Restart and checkpoint continuation cannot enter each other's engine path.
313
+ - Every run column is read with its declared data type.
314
+ - Control commands remain effective when presentation fails.
315
+ - A worker cannot relaunch unchanged state indefinitely.
316
+ - A terminal run has no open workflow turn.
317
+ - Later ordinary Pi turns cannot attach to terminal workflow work.
318
+ - No raw SQLite uniqueness or media-type error reaches normal recovery paths.
319
+ - Host, extension, widget, CLI, Herdr, and `piw` agree on run activity.
320
+ - Pi core and private Pi APIs remain unchanged. Pi session schemas also remain unchanged.
321
+ - One host and one database remain. The system keeps one client protocol and one production runtime.
322
+
323
+ ## Verification
324
+
325
+ Run:
326
+
327
+ ```bash
328
+ npm run check
329
+ npm run test:e2e
330
+ npm run test:e2e:live -- --runtime-only
331
+ git diff --check
332
+ npx slophammer-ts@latest dry .
333
+ npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
334
+ npx -y @simpledoc/simpledoc check
335
+ cargo test --manifest-path tui/Cargo.toml
336
+ cargo clippy --manifest-path tui/Cargo.toml --all-targets --all-features -- -D warnings
337
+ cargo fmt --manifest-path tui/Cargo.toml --check
338
+ ```
339
+
340
+ After deterministic checks pass, install the built package locally. Run a clean no-op workflow. Then run the composed Autoimplement regression and a forced provider-error recovery check. Confirm the widget and `piw` status at each stage. Use a real model only for the final manual installed-package check, with an explicit provider and model selected by the operator.
341
+
342
+ Run Pi Reviewer against `main` until no P0 or P1 finding remains. Check pull-request comments and CI before merge.
343
+
344
+ ## Scope
345
+
346
+ The implementation may change Pi Workflows engine, compiler, worker protocol, host state methods, extension coordination, viewer projection, built-in workflows, tests, and documentation in this repository.
347
+
348
+ ## Non-goals
349
+
350
+ - No Pi core or private Pi API changes.
351
+ - No Pi session schema changes.
352
+ - No new service, database, state root, or production runtime.
353
+ - No operating-system service installation.
354
+ - No migration or compatibility path for older alpha state.
355
+ - No release or package publication as part of implementation.
356
+ - No exactly-once claim for provider calls or external effects.
357
+
358
+ ## Assumptions
359
+
360
+ - Pi continues to provide the documented extension lifecycle events and session APIs.
361
+ - The host remains the only production process that opens live SQLite state.
362
+ - A package update can require the documented backup and reset when version-1 DDL changes.
363
+ - External systems remain responsible for their own stable idempotency keys or read-back checks.
@@ -104,7 +104,7 @@ A domain row and its event are written in one transaction. Normal APIs never upd
104
104
 
105
105
  `effect_attempts` records each application attempt and ownership generation. A matching repeated request adopts the existing effect. A different request under the same key is a conflict.
106
106
 
107
- Local effects use deterministic transactions. Run queue settlement effects are created only for runs that have a `run_queue` row; direct engine and controller-child runs do not create phantom queue work. External effects use provider idempotency or observation when available. An uncertain result becomes `ambiguous` and is not repeated without evidence.
107
+ Local effects use deterministic transactions. For workflow action nodes, the engine creates the idempotency key from the run ID, effect type, full compiled node path, and node visit number. A projected child-workflow view cannot replace that full identity. Run queue settlement effects are created only for runs that have a `run_queue` row; direct engine and controller-child runs do not create phantom queue work. External effects use provider idempotency or observation when available. An uncertain result becomes `ambiguous` and is not repeated without evidence.
108
108
 
109
109
  Telegram delivery and settlement use these shared effect records. The host records one numbered attempt before it tells the supervised adapter child to act. A confirmed result stores Telegram message references in the effect result. `channel_messages` stores the decision feature's delivery or settlement receipt; it does not copy effect state or external message references.
110
110
 
@@ -125,11 +125,13 @@ The shared records do not replace domain schemas. The following `STRICT` tables
125
125
  | Human decisions | `human_decisions`, `human_decision_resolutions`, `human_decision_submissions`, `continuations` |
126
126
  | Controllers | `controller_resources`, `controller_finalizers`, `controller_queue`, `controller_workflows` |
127
127
  | Effects | `effects`, `effect_attempts` |
128
- | Pi messages | `workflow_messages` |
128
+ | Pi messages | `workflow_messages`, `workflow_turns` |
129
129
  | Channels | `channels`, `channel_cursors`, `channel_messages` |
130
130
 
131
131
  `workflow_messages` is the only table that owns adding workflow content to Pi. It stores the target session, message kind, source record, content digest, session order, `pending`, `sent`, or `cancelled` state, confirmed Pi entry ID, and creation and update times. The table stores no sender, send lease, `sending` state, or separate sent time. Active-branch evidence changes `pending` or `cancelled` to `sent`. Initial, reminder, and resumed prompts are all `step` messages; their custom details contain the reason. Interactive requests, decisions, terminal runs, notifications, follow-ups, and settings keep their own domain state.
132
132
 
133
+ `workflow_turns` stores the host-approved ownership of one Pi model turn. Each row names the exact workflow message, run, session, and turn ID. A partial unique index permits only one open turn for a message. The host checks for an exact saved turn or another open turn before insertion, so a normal conflict returns a controlled protocol error instead of a raw SQLite error. Terminalization ends every open turn for that run as `lost` in the same transaction. It cancels pending step and decision messages, plus follow-ups when the run did not complete successfully. Committed notifications remain eligible. Matching late reports adopt the saved result, while conflicting identities remain errors.
134
+
133
135
  `channels` stores configured channel resource identities. `channel_cursors` stores the last accepted external polling position. `channel_messages` stores immutable decision delivery and settlement records for audit and duplicate evidence. External application state and Telegram message references belong to `effects` and `effect_attempts`.
134
136
 
135
137
  Foreign keys join projects, runs, attempts, decisions, controllers, effects, and channel records. Partial unique indexes enforce one active node attempt per run, one pending step message per interaction request, one nonterminal interactive continuation-chain reservation per Pi session, one decision winner, and one deterministic effect key. A run waiting for a checkpoint or protected decision keeps that chain reservation. A parked waiting parent does not block its own continuation. Reserving that continuation transfers the reservation and settles the parked parent queue in the same transaction, so a failed reservation leaves the parent recoverable.
@@ -158,6 +160,8 @@ Runs do not store a nested `WorkflowRunState` blob. `runs` stores run-level fact
158
160
 
159
161
  Readers derive `steps`, `outputs`, `results`, carried-step count, current-node fields, waiting state, source objects, and continuation decision receipts from these rows. Compact trace events do not copy prompts, node outputs, run inputs, final outputs, action receipts, or assistant receipts.
160
162
 
163
+ The run store reads each independent value through its declared media type. Input and final output use JSON readers. Run errors and presentation instructions use text readers. Terminal-message construction uses one typed terminal-data result instead of guessing the blob type. A missing or wrong media type fails presentation after the terminal state commits; it cannot roll back that state.
164
+
161
165
  ### Assistant-message attempts
162
166
 
163
167
  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.
@@ -215,7 +219,7 @@ A deadline with a validated default response is timeout-policy acceptance. It ca
215
219
 
216
220
  Late or repeated commands return or adopt the durable winner. They do not overwrite it.
217
221
 
218
- The same rule applies to run terminal outcomes, continuation admission, queue settlement, controller effects, retry scheduling, and channel settlement through their domain constraints and expected revisions.
222
+ The same rule applies to run terminal outcomes, continuation admission, queue settlement, controller effects, retry scheduling, channel settlement, and workflow-turn reports through their domain constraints and expected revisions. A matching turn report adopts the saved ownership result. A different report for the same turn ID remains a conflict.
219
223
 
220
224
  ## Read contract
221
225
 
@@ -225,7 +229,7 @@ A settings scope uses its resource revision as its public change number. Each ac
225
229
 
226
230
  `workflow_follow_ups` records source acceptance order, removal, and cancellation. The source and message stay attached to the continuation-chain member that accepted them; rows are not rewritten when the chain continues. The host walks the chain to find its final outcome. `workflow_messages` owns message state and Pi entry evidence. Failure, timeout, and cancellation cancel unsent follow-up messages.
227
231
 
228
- - A terminal run fact overrides stale message state.
232
+ - A terminal run fact overrides stale message state and has no open workflow turn.
229
233
  - An accepted decision is accepted even if its continuation effect is still pending.
230
234
  - A cancelled decision is cancelled even if parent cleanup is still pending.
231
235
  - A stale owner is not shown as current.
@@ -1,6 +1,6 @@
1
1
  # Workflow host
2
2
 
3
- Status: the out-of-process host, unified live client, workflow-message contract, and restored session behavior are implemented. [Unify workflow messages and restore hosted behavior](2026-09-02-unify-workflow-messages-plan.md), [run workflows outside Pi](2026-08-30-out-of-process-workflow-host-plan.md), [restore workflow session delivery and controls](2026-09-01-restore-session-delivery-controls-plan.md), and [unify live workflow clients](2026-09-01-unified-workflow-client-plan.md) record the design and implementation plans.
3
+ Status: the out-of-process host, unified live client, workflow-message contract, and restored session behavior are implemented. [Unify workflow run state](2026-09-04-workflow-run-state-plan.md) records the approved refactor for turn ownership, managed effects, restarts, terminal data, cancellation, and worker recovery. [Unify workflow messages and restore hosted behavior](2026-09-02-unify-workflow-messages-plan.md), [run workflows outside Pi](2026-08-30-out-of-process-workflow-host-plan.md), [restore workflow session delivery and controls](2026-09-01-restore-session-delivery-controls-plan.md), and [unify live workflow clients](2026-09-01-unified-workflow-client-plan.md) record the earlier design and implementation plans.
4
4
 
5
5
  ## Purpose
6
6
 
@@ -133,6 +133,8 @@ The run and queue projections follow these states:
133
133
 
134
134
  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.
135
135
 
136
+ A terminal state commits before the host builds its terminal workflow message. Missing or invalid presentation data can prevent that message, but it cannot roll back completion, failure, or cancellation. The host schedules another attempt and also finds missing terminal messages when it starts, so repaired presentation data can produce the same terminal message later.
137
+
136
138
  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.
137
139
 
138
140
  ## Worker lifecycle
@@ -162,6 +164,8 @@ A worker launch envelope contains:
162
164
 
163
165
  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.
164
166
 
167
+ After the ready message, the host sends one explicit command: `start`, `resume`, `continue`, or `restart`. A continuation names its waiting checkpoint parent. A restart begins a new run from the workflow start. The worker never infers the command from a nullable parent ID.
168
+
165
169
  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.
166
170
 
167
171
  The host records one terminal worker outcome:
@@ -173,7 +177,7 @@ The host records one terminal worker outcome:
173
177
  - `claimLost`
174
178
  - `orphaned`
175
179
 
176
- 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.
180
+ 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. If the saved run revision did not advance after the worker became ready, the host parks the run with `workerNoProgress` and does not claim it again automatically. An explicit resume can make one new attempt after the operator corrects the cause.
177
181
 
178
182
  ## Process supervision
179
183
 
@@ -365,7 +369,11 @@ The coordinator waits until Pi is idle and has no queued user input, keeps the m
365
369
 
366
370
  After a send, the coordinator waits for the matching Pi entry and reports the active branch so the host records its entry ID and marks the message `sent`. Branch evidence marks a matching message `sent` even if its source cancelled it after the send. If Pi emits `agent_start` before that report or before the session view loads, the coordinator buffers the start and matching end, records the message first, and then reports the turn events in order.
367
371
 
368
- Active-branch absence is usable only when the branch has no matching ID, Pi is idle, and Pi has no pending messages. If Pi or the extension disappears after the send call but before inspection, the message stays `pending`. A replacement extension reports the branch before another send. Documented Pi APIs do not prove cross-branch absence or exactly-once model execution.
372
+ The host alone decides whether that Pi model turn belongs to the workflow message. `workflowTurn.report` returns a version-1 receipt with `active`, `settled`, or `absent` ownership and the exact saved turn when one exists. The extension exposes workflow activity and starts session capture only after an `active` receipt. It clears its temporary copy after settlement, rejection, or disconnect. Every new Pi `agent_start` replaces any older local copy and requires fresh host acceptance before the turn can become workflow work.
373
+
374
+ Turn start and end use the exact message, run, session, and turn IDs. A matching repeat adopts the saved result. A conflicting repeat remains an error. When a run becomes terminal, the same transaction ends its open turns as `lost` and cancels pending step and decision messages. Failure and cancellation also cancel pending follow-ups. A committed notification stays eligible for delivery. A late matching end report adopts that terminal cleanup. A terminal run cannot start another step turn, and a later ordinary Pi turn cannot inherit its old workflow ownership.
375
+
376
+ Active-branch absence is usable only when the branch has no matching ID, Pi is idle, and Pi has no pending messages. If Pi or the extension disappears after the send call but before inspection, the message stays `pending`. A replacement extension reports the branch before another send. The idle branch report settles an unproved open host turn as `lost`. Documented Pi APIs do not prove cross-branch absence or exactly-once model execution.
369
377
 
370
378
  The extension subscribes to the active origin-session live run view and projects it into Pi's documented widget and status APIs. It never opens SQLite, runs workflow code, or derives a display status. `Shift+Up` and `Shift+Down` scroll the widget. When Herdr is available, the widget also shows `Ctrl+Shift+R piw`, and `/piw` remains the command fallback. Both actions open or focus the exact run from the same view.
371
379
 
@@ -427,7 +435,7 @@ Side-effecting action and shell behavior must have one of these contracts:
427
435
  - a managed effect with a read-back check that proves whether it applied;
428
436
  - an explicit non-resumable result that becomes `ambiguous` after an uncertain crash.
429
437
 
430
- 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.
438
+ The host reserves an effect before execution. The engine creates the idempotency key from the run ID, effect type, full compiled node path, and node visit number. Workflow code provides the effect type and request, but it does not provide this internal key. Included workflows can use the same local node name without sharing a key. The request fingerprint prevents key reuse with another payload.
431
439
 
432
440
  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.
433
441
 
@@ -444,7 +452,8 @@ Controller reconcile code runs in a supervised controller worker, not in the hos
444
452
  Use separate states and messages for these failures:
445
453
 
446
454
  - `claimLost`: another generation owns the run, or the claim expired.
447
- - `workerCrashed`: the child exited without a terminal protocol message.
455
+ - `workerCrashed`: the child exited without a terminal protocol message after it saved progress.
456
+ - `workerNoProgress`: the child exited before the saved run revision advanced and needs explicit resume or cancellation.
448
457
  - `workerTimedOut`: the child exceeded a declared deadline.
449
458
  - `hostUnavailable`: the client cannot reach or start the host.
450
459
  - `sourceChanged`: the workflow source does not match the saved identity.
package/docs/workflows.md CHANGED
@@ -323,7 +323,10 @@ keeps normal output capture. Lines and update data are each limited to 64 KiB.
323
323
  See [WORKFLOW_UPDATES.md](WORKFLOW_UPDATES.md) for the envelope, progress
324
324
  schema, limits, estimation, and error rules.
325
325
 
326
- The host reserves the effect before it lets the action run. A repeated key with
326
+ The host reserves the effect before it lets the action run. The engine creates
327
+ the internal key from the run ID, effect type, full compiled node path, and node
328
+ visit number. Workflow code does not supply that key. Two included workflows
329
+ can use the same local node name without sharing an effect. A repeated key with
327
330
  the same request adopts the durable record; the same key with another request
328
331
  is a conflict. A normal caught error settles the attempt as rejected. After an
329
332
  uncertain process exit, an idempotent effect returns to pending for retry, while
@@ -739,6 +742,9 @@ possible. Defaults worth knowing:
739
742
  receipt, and fences the worker before process-group shutdown. `/workflow
740
743
  resume` takes a new generation and reruns only work after the last durable
741
744
  boundary.
745
+ - The host tells each worker to `start`, `resume`, `continue`, or `restart`.
746
+ A checkpoint continuation names its waiting parent. A restart begins at the
747
+ workflow start and does not reuse checkpoint continuation rules.
742
748
  - Resuming an active run adopts the existing work. Duplicate start, control,
743
749
  update, and submission messages return their stored receipts.
744
750
  - A start is committed as `queued` with its final run ID before the command
@@ -753,6 +759,9 @@ resume` takes a new generation and reruns only work after the last durable
753
759
  same transaction. Claim loss does not write a failed run event.
754
760
  - An uncommitted pure or idempotent node can run again after a worker crash. An
755
761
  uncertain manual effect parks as ambiguous and never retries automatically.
762
+ If a ready worker exits before the saved run revision advances, the host parks
763
+ the run with `workerNoProgress`. The scheduler does not claim it again until
764
+ an operator explicitly resumes or cancels it.
756
765
  - Host status reports safe counts and timestamps. It does not report session
757
766
  IDs, project paths, prompts, payloads, tokens, process IDs, or credentials.
758
767
 
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "osolmaz.pi-workflows"
2
2
  name = "pi-workflows"
3
- version = "0.16.1"
3
+ version = "0.16.2"
4
4
  min_herdr_version = "0.7.0"
5
5
  description = "Open the active pi-workflows run in piw from a managed Herdr pane."
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -3,6 +3,8 @@ import type { WorkflowMessage, WorkflowTurn } from "../state/workflow-messages.j
3
3
 
4
4
  export const RUN_VIEW_SCHEMA = "pi-workflows.run-view.v1" as const;
5
5
  export const SESSION_VIEW_SCHEMA = "pi-workflows.session-view.v1" as const;
6
+ export const WORKFLOW_TURN_REPORT_RECEIPT_SCHEMA =
7
+ "pi-workflows.workflow-turn-report-receipt.v1" as const;
6
8
 
7
9
  export type WorkflowDisplayStatus =
8
10
  | "queued"
@@ -142,6 +144,12 @@ export type WorkflowBranchReport = {
142
144
  hasPendingMessages: boolean;
143
145
  };
144
146
 
147
+ export type WorkflowTurnReportReceipt = {
148
+ schema: typeof WORKFLOW_TURN_REPORT_RECEIPT_SCHEMA;
149
+ ownership: "active" | "settled" | "absent";
150
+ turn: WorkflowTurn | null;
151
+ };
152
+
145
153
  export type WorkflowTurnReport =
146
154
  | {
147
155
  state: "started";