@osolmaz/pi-workflows 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +18 -2
  2. package/dist/channels/adapter-entry.d.ts +3 -0
  3. package/dist/channels/adapter-entry.js +203 -0
  4. package/dist/channels/adapter-entry.js.map +1 -0
  5. package/dist/channels/config.d.ts +43 -0
  6. package/dist/channels/config.js +234 -0
  7. package/dist/channels/config.js.map +1 -0
  8. package/dist/channels/protocol.d.ts +94 -0
  9. package/dist/channels/protocol.js +219 -0
  10. package/dist/channels/protocol.js.map +1 -0
  11. package/dist/channels/telegram.d.ts +46 -0
  12. package/dist/channels/telegram.js +285 -0
  13. package/dist/channels/telegram.js.map +1 -0
  14. package/dist/client/client.d.ts +1 -0
  15. package/dist/client/client.js +5 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/client/index.d.ts +1 -1
  18. package/dist/client/protocol.d.ts +1 -1
  19. package/dist/client/protocol.js +11 -5
  20. package/dist/client/protocol.js.map +1 -1
  21. package/dist/client/view.d.ts +42 -16
  22. package/dist/client/view.js.map +1 -1
  23. package/dist/controllers/index.d.ts +1 -1
  24. package/dist/controllers/index.js.map +1 -1
  25. package/dist/controllers/sqlite.d.ts +12 -165
  26. package/dist/controllers/sqlite.js +124 -435
  27. package/dist/controllers/sqlite.js.map +1 -1
  28. package/dist/extension/index.d.ts +23 -2
  29. package/dist/extension/index.js +619 -535
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/recorder.d.ts +95 -0
  32. package/dist/extension/recorder.js +530 -0
  33. package/dist/extension/recorder.js.map +1 -0
  34. package/dist/extension/remote-recorder-store.d.ts +25 -0
  35. package/dist/extension/remote-recorder-store.js +81 -0
  36. package/dist/extension/remote-recorder-store.js.map +1 -0
  37. package/dist/extension/session-events.d.ts +134 -0
  38. package/dist/extension/session-events.js +60 -0
  39. package/dist/extension/session-events.js.map +1 -0
  40. package/dist/extension/session-view.d.ts +2 -0
  41. package/dist/extension/session-view.js +60 -2
  42. package/dist/extension/session-view.js.map +1 -1
  43. package/dist/extension/step-message.d.ts +6 -4
  44. package/dist/extension/step-message.js +12 -6
  45. package/dist/extension/step-message.js.map +1 -1
  46. package/dist/extension/widget.d.ts +2 -2
  47. package/dist/extension/widget.js +28 -18
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  50. package/dist/extension/workflow-message-coordinator.js +294 -0
  51. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  52. package/dist/host/channel-effects.d.ts +72 -0
  53. package/dist/host/channel-effects.js +271 -0
  54. package/dist/host/channel-effects.js.map +1 -0
  55. package/dist/host/channel-supervisor.d.ts +21 -0
  56. package/dist/host/channel-supervisor.js +54 -0
  57. package/dist/host/channel-supervisor.js.map +1 -0
  58. package/dist/host/runner.d.ts +43 -8
  59. package/dist/host/runner.js +1567 -331
  60. package/dist/host/runner.js.map +1 -1
  61. package/dist/host/state.d.ts +19 -16
  62. package/dist/host/state.js +251 -63
  63. package/dist/host/state.js.map +1 -1
  64. package/dist/host/view.d.ts +19 -9
  65. package/dist/host/view.js +222 -118
  66. package/dist/host/view.js.map +1 -1
  67. package/dist/state/prune.js +3 -25
  68. package/dist/state/prune.js.map +1 -1
  69. package/dist/state/schema.js +76 -97
  70. package/dist/state/schema.js.map +1 -1
  71. package/dist/state/workflow-messages.d.ts +96 -0
  72. package/dist/state/workflow-messages.js +335 -0
  73. package/dist/state/workflow-messages.js.map +1 -0
  74. package/dist/viewer/render.js +1 -1
  75. package/dist/viewer/render.js.map +1 -1
  76. package/dist/viewer/tui.d.ts +1 -1
  77. package/dist/viewer/tui.js +65 -8
  78. package/dist/viewer/tui.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +2 -0
  80. package/dist/workflows/human-decision.js +8 -2
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +1 -1
  83. package/dist/workflows/index.js.map +1 -1
  84. package/dist/workflows/settings.d.ts +1 -8
  85. package/dist/workflows/settings.js.map +1 -1
  86. package/dist/workflows/store.d.ts +5 -19
  87. package/dist/workflows/store.js +121 -354
  88. package/dist/workflows/store.js.map +1 -1
  89. package/dist/workflows/tool-input.d.ts +21 -0
  90. package/dist/workflows/tool-input.js +23 -1
  91. package/dist/workflows/tool-input.js.map +1 -1
  92. package/dist/workflows/types.d.ts +5 -5
  93. package/dist/workflows/workflow-message-content.d.ts +38 -0
  94. package/dist/workflows/workflow-message-content.js +157 -0
  95. package/dist/workflows/workflow-message-content.js.map +1 -0
  96. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  97. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  98. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  99. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  100. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  101. package/docs/2026-09-01-unified-workflow-client-plan.md +5 -1
  102. package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
  103. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  104. package/docs/DEFERRED_TURNS.md +66 -294
  105. package/docs/HUMAN_DECISIONS.md +29 -30
  106. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  107. package/docs/SQLITE_STATE.md +19 -15
  108. package/docs/WORKFLOW_HOST.md +93 -28
  109. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  110. package/docs/live-replay-protocol.md +8 -4
  111. package/docs/tui-viewer.md +3 -1
  112. package/docs/workflows.md +34 -27
  113. package/herdr-plugin.toml +1 -1
  114. package/package.json +1 -1
  115. package/plugins/herdr/viewer.mjs +14 -5
  116. package/src/channels/adapter-entry.ts +220 -0
  117. package/src/channels/config.ts +296 -0
  118. package/src/channels/protocol.ts +333 -0
  119. package/src/channels/telegram.ts +335 -0
  120. package/src/client/client.ts +6 -2
  121. package/src/client/index.ts +2 -1
  122. package/src/client/protocol.ts +11 -5
  123. package/src/client/view.ts +43 -18
  124. package/src/controllers/index.ts +0 -1
  125. package/src/controllers/sqlite.ts +164 -844
  126. package/src/extension/index.ts +693 -646
  127. package/src/extension/recorder.ts +687 -0
  128. package/src/extension/remote-recorder-store.ts +126 -0
  129. package/src/extension/session-events.ts +119 -0
  130. package/src/extension/session-view.ts +86 -3
  131. package/src/extension/step-message.ts +19 -9
  132. package/src/extension/widget.ts +37 -17
  133. package/src/extension/workflow-message-coordinator.ts +334 -0
  134. package/src/host/channel-effects.ts +465 -0
  135. package/src/host/channel-supervisor.ts +73 -0
  136. package/src/host/runner.ts +2120 -531
  137. package/src/host/state.ts +333 -94
  138. package/src/host/view.ts +249 -121
  139. package/src/state/prune.ts +3 -31
  140. package/src/state/schema.ts +76 -97
  141. package/src/state/workflow-messages.ts +548 -0
  142. package/src/viewer/render.ts +1 -5
  143. package/src/viewer/tui.ts +71 -6
  144. package/src/workflows/human-decision.ts +18 -2
  145. package/src/workflows/index.ts +0 -1
  146. package/src/workflows/settings.ts +1 -20
  147. package/src/workflows/store.ts +173 -489
  148. package/src/workflows/tool-input.ts +36 -1
  149. package/src/workflows/types.ts +5 -5
  150. package/src/workflows/workflow-message-content.ts +197 -0
  151. package/dist/extension/decision-channels.d.ts +0 -134
  152. package/dist/extension/decision-channels.js +0 -1307
  153. package/dist/extension/decision-channels.js.map +0 -1
  154. package/src/extension/decision-channels.ts +0 -1826
@@ -1,338 +1,110 @@
1
- # Deferred workflow turns
1
+ # Terminal workflow messages
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 every top-level interactive terminal result, cancellation, timeout, launch failure, controller interruption, and claim loss.
3
+ This specification defines the final Pi message for an interactive workflow. It replaces the separate deferred-turn and `workflow_turn_intents` design. Terminal messages use the same host state, client operations, and extension coordinator as all other [workflow messages](WORKFLOW_STEP_MESSAGES.md).
4
4
 
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).
5
+ The earlier [deferred-turn plan](plans/2026-08-21-deferred-turn-intents-plan.md) and [terminal restart plan](plans/2026-08-27-workflow-terminal-restart-plan.md) remain historical design records. This document is the current contract.
6
6
 
7
7
  ## Terms
8
8
 
9
- - **Source event:** The workflow event that creates the need for another turn.
10
- - **Turn intent:** The durable obligation to send one successor turn.
11
- - **Natural successor:** The next workflow agent prompt or result presentation.
12
- - **Fallback:** A factual model-facing message sent when no natural successor remains.
13
- - **Resolution:** The recorded fact that one message path satisfied the intent.
14
- - **Target session:** The Pi session that owns the successor turn.
9
+ - **Continuation chain:** One interactive workflow and the runs created to continue its checkpoints.
10
+ - **Final run:** The last run in the continuation chain.
11
+ - **Terminal outcome:** The final run's completed, failed, timed-out, or cancelled result.
12
+ - **Terminal workflow message:** The one model-facing message that reports the terminal outcome and offers valid next actions.
13
+ - **Restart:** A new immutable run created from an allowed action in the terminal turn.
15
14
 
16
15
  ## Core rule
17
16
 
18
- Each eligible source event creates at most one turn intent. Exactly one of these message paths can resolve it:
17
+ The host creates one `terminal` workflow message in the same transaction that records the final terminal outcome. Only the final run in a continuation chain creates this message. A parent settled by a continuation does not create one.
19
18
 
20
- 1. a workflow agent prompt;
21
- 2. a result presentation;
22
- 3. a factual fallback.
19
+ The terminal message uses the same `workflow_messages` table and `WorkflowMessageCoordinator` as steps, decisions, notifications, and follow-ups. Initial, reminder, and resumed prompts are one step-message kind. There is no `workflow_turn_intents` table, deferred-turn sender, terminal sender, or second send path.
23
20
 
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.
21
+ The terminal message records the factual outcome before it starts a model turn. It does not keep the old assistant turn alive and does not delay cancellation or process cleanup.
25
22
 
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.
23
+ ## Outcomes
27
24
 
28
- ## Intent lifecycle
25
+ A terminal message can report:
29
26
 
30
- An intent has two stored states:
27
+ - successful completion;
28
+ - workflow failure;
29
+ - timeout with no recovery edge;
30
+ - cancellation;
31
+ - launch or worker failure that became the final run outcome.
31
32
 
32
- | State | Condition | Meaning |
33
- | -------- | ------------------------------------------------------------- | --------------------------------------------------------- |
34
- | Pending | `resolvedAt` is null | No message path has resolved the intent. |
35
- | Resolved | `resolvedAt`, `resolution`, and `resolutionMessageId` are set | One message path was sent or found in the session branch. |
33
+ Pause, Escape, a waiting checkpoint, a nonfinal continuation parent, user hold, and normal host shutdown do not create a terminal message.
36
34
 
37
- Pending intents have separate fallback eligibility:
35
+ A claim loss is a handoff. It creates no terminal outcome or terminal message unless later recovery proves that the run itself failed.
38
36
 
39
- | Eligibility | Condition | Meaning |
40
- | ----------- | -------------------- | ---------------------------------------------------------------- |
41
- | Ineligible | `eligibleAt` is null | A natural workflow prompt or presentation can still arrive. |
42
- | Eligible | `eligibleAt` is set | Durable state shows that no immediate natural successor remains. |
37
+ ## Message content and controls
43
38
 
44
- A claim is temporary coordination state. A claim does not resolve an intent. An expired claim can be acquired again.
39
+ The message names the outcome and the run. It contains only controls valid for that saved outcome. A successful final run can offer follow-up work. A failed or timed-out final run can offer an allowed restart. A cancelled run offers no restart. An ambiguous external effect remains parked and creates no terminal message until explicit recovery produces a real terminal outcome.
45
40
 
46
- Valid resolutions are:
41
+ A restart creates a new run. It does not reopen or mutate the terminal run. It keeps the approved input and source identity, uses a stable action fingerprint, rejects duplicate activation, and stops after three restarts in one chain.
47
42
 
48
- ```text
49
- workflowPrompt | presentation | fallback
50
- ```
43
+ The terminal result remains visible in the origin-session view while its terminal workflow message is pending or its first model turn is open, and then for 60 seconds after that turn ends. This display retention gives no claim, queue reservation, or execution authority. A new run in that session replaces it immediately. A verified operator can also clear it through `sessionView.clearTerminal`, exposed by `/workflow clear` and `piw`.
51
44
 
52
- Resolution records message delivery into the Pi session or the presence of the same message in the session branch. It does not prove that a model turn started or completed.
45
+ ## Turn tracking
53
46
 
54
- ## Source events
47
+ A terminal workflow message is open only until its first model turn ends. The extension reports the matching `agent_start` and `agent_end` through `workflowTurn.report`.
55
48
 
56
- Valid causes are:
49
+ The end report includes `stopReason: "completed"`, `"aborted"`, or `"error"`. Response-entry evidence comes from `ctx.sessionManager.getBranch()` after `agent_end` and can be null. A repeated report adopts the stored result. A stale turn ID cannot end a newer turn.
57
50
 
58
- ```text
59
- agentCancelled | timedOut | failed | launchFailed | controllerInterrupted | claimLost | terminal | cancelled
60
- ```
51
+ If Pi restarts after a terminal message was sent but before its end was reported, the extension's idle-session active-branch report records a synthetic end with `stopReason: "lost"`. Host restart alone does not close the turn. The terminal result remains durable and visible, but the host does not pretend that the model turn completed.
61
52
 
62
- The event policy is:
53
+ ## Sending and recovery
63
54
 
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. |
55
+ After every host connection, the coordinator waits for the complete origin-session view and reports the active branch before it sends a workflow message or reports a model turn. The host gives the next eligible pending message only to the active coordinator epoch for that session. A replacement connection fences the old one.
78
56
 
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.
57
+ The coordinator waits until Pi is idle and has no pending messages. It keeps the terminal workflow message ID in its in-memory queued map, checks the active branch, and reports a matching entry before any send. Otherwise, it performs one final synchronous check that Pi is idle, has no pending input, the message is absent, and its connection still owns the active epoch. It calls documented `pi.sendMessage()` without an `await` between that check and the call.
80
58
 
81
- ## Stable identity
59
+ A matching hidden workflow message ID in the active branch proves that Pi accepted the message. The host records the matching Pi entry ID and changes the message to `sent`, even if its source cancelled it after the send. If the extension reloads, it reports the active branch and adopts that entry before another send.
82
60
 
83
- `intentId` is a deterministic digest of:
61
+ Absence is usable only when all three facts are true:
84
62
 
85
- - target session ID;
86
- - run ID;
87
- - source event ID;
88
- - node ID or `$launch`;
89
- - attempt ID when known;
90
- - cause.
63
+ 1. the active branch has no matching hidden workflow message ID;
64
+ 2. `ctx.isIdle()` is true; and
65
+ 3. `ctx.hasPendingMessages()` is false.
91
66
 
92
- The same source event must always produce the same ID. Reusing an ID with different immutable facts is an error.
67
+ If Pi or the extension disappears after `pi.sendMessage()` but before the branch report, the message stays `pending`. A replacement extension reports the active branch before it sends. Documented Pi APIs do not prove cross-branch absence or exactly-once model execution. Pi Workflows guarantees its saved message identity, active-branch evidence, and recovery decisions. It does not claim more.
93
68
 
94
- `sourceEventId` identifies the source transition independently of delivery retries. Terminal handling must reuse an earlier abort event instead of creating a second terminal event for the same interruption.
69
+ ## Follow-ups
95
70
 
96
- IDs use the existing controller-store key limit of 512 characters. The digest representation must be stable across processes and extension restarts.
71
+ Successful completion can have ordered [follow-up prompts](2026-08-25-workflow-follow-ups.md). They use the same workflow-message coordinator.
97
72
 
98
- ## Stored record
73
+ A follow-up is eligible only after:
99
74
 
100
- `workflow_turn_intents` lives in the existing controller SQLite database.
75
+ - the final run is successfully completed;
76
+ - its terminal workflow message has been sent;
77
+ - the terminal model turn has ended;
78
+ - every earlier follow-up is settled or cancelled; and
79
+ - no nonterminal run reserves the origin session.
101
80
 
102
- | Column | Null | Meaning |
103
- | --------------------------- | ---- | -------------------------------------------- |
104
- | `intent_id` | No | Deterministic primary key. |
105
- | `source_event_id` | No | Stable source transition identity. |
106
- | `run_id` | No | Related workflow run. |
107
- | `workflow_ref` | No | Workflow identity used in messages. |
108
- | `target_session_id` | No | Session that can receive the successor. |
109
- | `cause` | No | Closed cause value. |
110
- | `node_id` | Yes | Source node when known. |
111
- | `attempt_id` | Yes | Source attempt when known. |
112
- | `fallback_facts_json` | No | Bounded factual payload. |
113
- | `requested_at` | No | Absolute ISO 8601 creation time. |
114
- | `eligible_at` | Yes | Absolute ISO 8601 fallback eligibility time. |
115
- | `resolved_at` | Yes | Absolute ISO 8601 resolution time. |
116
- | `resolution` | Yes | Closed resolution value. |
117
- | `resolution_message_id` | Yes | Stable message identity. |
118
- | `delivery_claim_token` | Yes | Current claimant. |
119
- | `delivery_claim_expires_at` | Yes | Claim expiry as epoch milliseconds. |
81
+ A follow-up turn is reported for ordering and recovery, but it does not make the completed workflow display as `running`.
120
82
 
121
- The table requires these indexes:
83
+ ## State and compatibility
122
84
 
123
- - unresolved intents by run and target session;
124
- - unresolved eligible intents by target session, eligibility time, and intent ID.
85
+ This is an alpha hard cut in `pi-workflows-state` schema version 1.
125
86
 
126
- A resolved row has all three resolution fields. A pending row has none of them. A row cannot change immutable identity or source fields after creation.
87
+ - `workflow_messages` owns terminal message content, `pending`, `sent`, or `cancelled` state, Pi entry evidence, and turn reports.
88
+ - Run and continuation rows own terminal outcomes and chain identity.
89
+ - `workflow_follow_ups` owns accepted follow-up source prompts and cancellation only.
90
+ - `workflow_turn_intents` and `workflow_follow_up_queues` do not exist.
127
91
 
128
- ## Fallback facts
129
-
130
- `fallback_facts_json` uses this versioned camelCase object:
131
-
132
- ```json
133
- {
134
- "schema": "pi-workflows.deferred-turn-facts.v1",
135
- "workflowName": "autoimplement",
136
- "runId": "20260821T081731Z-autoimplement-407480dd",
137
- "observedState": "failed",
138
- "cause": "failed",
139
- "nodeId": "$launch",
140
- "attemptId": null,
141
- "reason": "scope must be a non-empty string",
142
- "handoff": false
143
- }
144
- ```
145
-
146
- Rules:
147
-
148
- - `schema` is required and has the exact value shown above.
149
- - `workflowName`, `runId`, `observedState`, and `cause` are required strings.
150
- - `nodeId`, `attemptId`, and `reason` are strings or null.
151
- - `handoff` is a required boolean.
152
- - `reason` is safe diagnostic text with at most 8,192 characters.
153
- - The serialized object is at most 64 KiB.
154
- - Unknown fields are rejected during alpha.
155
- - The object must not contain credentials, raw environment values, or unbounded command output.
156
-
157
- `handoff: true` means another runner can continue. A handoff message must not describe the run as terminal unless separate durable state proves it.
158
-
159
- ## Store operations
160
-
161
- The controller store provides these internal operations:
162
-
163
- - `ensureWorkflowTurnIntent`
164
- - `getWorkflowTurnIntent`
165
- - `claimWorkflowTurnIntentForRun`
166
- - `claimEligibleWorkflowTurnIntentsForSession`
167
- - `makeWorkflowTurnIntentEligible`
168
- - `resolveWorkflowTurnIntent`
169
- - `releaseWorkflowTurnIntentClaim`
170
- - a bounded diagnostic list operation
171
-
172
- `ensureWorkflowTurnIntent` is idempotent when all immutable fields match. It fails on an identity collision.
173
-
174
- Claim operations use a caller-supplied token and lease duration. Resolution requires the matching live claim token. Conditional updates ensure that natural delivery and fallback cannot both resolve the same intent.
175
-
176
- List operations require a positive bounded limit and deterministic ordering.
177
-
178
- ## Abort ordering
179
-
180
- For an eligible active-turn abort, Pi Workflows performs these steps in order:
181
-
182
- 1. Record abort provenance on the active run.
183
- 2. Build the source event and intent ID.
184
- 3. Attempt to persist the intent.
185
- 4. Record the intent ID in system-abort bookkeeping.
186
- 5. Call `ctx.abort()`.
187
-
188
- The persistence attempt is synchronous because cancellation immediately crosses the turn boundary. A store failure does not prevent `ctx.abort()`. Terminal handling retries the same intent ID. If retry also fails, Pi Workflows reports that it could not preserve the successor-turn guarantee.
189
-
190
- ## Natural delivery
191
-
192
- All extension-owned workflow prompts and result presentations pass through one `DeferredTurnCoordinator`.
193
-
194
- If no intent exists, the coordinator preserves current message content and delivery options.
195
-
196
- If a pending intent exists, the coordinator:
197
-
198
- 1. waits until the old turn has settled or the session is verified idle;
199
- 2. claims the intent for the run and session;
200
- 3. adds `turnIntentId` to the message details;
201
- 4. sends the normal prompt or presentation;
202
- 5. resolves the intent with the matching resolution and message ID;
203
- 6. releases the claim if sending fails.
204
-
205
- A prompt generated during abort handling must not be sent into the aborting turn. The coordinator releases it after `agent_settled`.
206
-
207
- ## Fallback delivery
208
-
209
- Fallback synchronization runs:
210
-
211
- - after `agent_settled` and system-abort cleanup;
212
- - when the target session starts;
213
- - during the existing periodic synchronization pass while the session is idle.
214
-
215
- The fallback custom message uses this details object:
216
-
217
- ```json
218
- {
219
- "schema": "pi-workflows.deferred-turn-message.v1",
220
- "turnIntentId": "deferred-turn:...",
221
- "runId": "20260821T081731Z-autoimplement-407480dd",
222
- "cause": "failed",
223
- "presentation": {
224
- "workflowName": "autoimplement",
225
- "state": "failed",
226
- "reasonKind": "maxSteps",
227
- "restart": {
228
- "count": 0,
229
- "limit": 3
230
- }
231
- }
232
- }
233
- ```
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
-
237
- The message type is `pi-workflows-deferred-turn`. Delivery uses:
238
-
239
- ```ts
240
- {
241
- deliverAs: "followUp",
242
- triggerTurn: true,
243
- }
244
- ```
245
-
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.
259
-
260
- Fallback delivery is disabled during session shutdown and while a user-interrupted workflow is held.
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
-
272
- ## Delivery recovery
273
-
274
- Every resolving message includes the intent ID in its custom-message details.
275
-
276
- Before sending, the coordinator scans the current session branch for that ID. If the message is already present, the coordinator resolves the intent without sending again. This repairs a crash or SQLite failure that occurs after `sendMessage` succeeds but before resolution is stored.
277
-
278
- Store leases prevent concurrent processes from sending the same resolution. Branch identity handles the remaining send-before-resolution window.
279
-
280
- ## Claim transfer
281
-
282
- Claim loss stops the old runner's work and all fenced SQLite writes. The intent stays pending and fallback-ineligible while another runner can resume the run.
283
-
284
- The new runner's next workflow prompt can resolve the intent. If durable state later proves a terminal outcome with no natural successor, terminal handling makes the intent eligible for fallback to its target session.
285
-
286
- Time alone does not prove terminal failure.
287
-
288
- ## Launch notifications
289
-
290
- Workflow-authored `progress` and `final` notifications remain passive. They do not trigger model turns.
291
-
292
- The `launch_failure` notification kind is removed. Queued launch failure creates an eligible turn intent instead.
293
-
294
- Pending `launch_failure` rows from the earlier alpha contract are incompatible. Pi Workflows must stop with a clear controller-store reset instruction. It must not reinterpret, migrate, or silently delete those rows.
295
-
296
- ## Post-completion follow-ups
297
-
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.
299
-
300
- See [Continue normal work after a workflow finishes](2026-08-25-workflow-follow-ups.md).
301
-
302
- ## Availability limits
303
-
304
- Pi Workflows can guarantee only the facts under its control:
305
-
306
- - the intent was stored;
307
- - a claimant acquired it;
308
- - a custom message was sent or found in the session branch;
309
- - the local intent was resolved.
310
-
311
- Pi Workflows cannot guarantee model start or completion with the current Pi API. It also cannot deliver after permanent loss of the process, target session, controller store, machine, or model provider.
312
-
313
- Undelivered intents remain pending. The implementation does not add a service to process them outside a live Pi session.
314
-
315
- ## Compatibility
316
-
317
- This is an alpha hard cutover.
318
-
319
- - Keep `pi-workflows.controller-store.v1`.
320
- - Add `workflow_turn_intents` to the existing database.
321
- - Remove the launch-trigger runtime path.
322
- - Add no v2 schema, compatibility reader, dual write, alias, or feature flag.
323
- - Keep historical terminal SQLite runs readable because their contract does not change.
92
+ The DDL digest changes in place. There is no migration, compatibility reader, dual path, fallback sender, alias, feature flag, or second schema. Incompatible state remains untouched and fails with the standard backup-and-reset instruction.
324
93
 
325
94
  ## Conformance
326
95
 
327
96
  An implementation conforms when:
328
97
 
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;
331
- - an asynchronous crash after a successful start result receives one fallback after settlement;
332
- - a natural recovery prompt or presentation suppresses fallback by resolving the same intent;
333
- - waiting checkpoints, controller children, internal owners, pause, Escape, user hold, and shutdown do not create terminal turns;
334
- - claim transfer permits natural resolution by the new owner and prevents stale writes;
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;
338
- - passive workflow notifications keep their current behavior.
98
+ - one continuation chain creates at most one terminal workflow message;
99
+ - only the final run creates that message;
100
+ - every terminal outcome uses the shared workflow-message path;
101
+ - a busy Pi session cannot cause duplicate terminal messages or model turns;
102
+ - reload reports the branch and adopts an existing active-branch entry before another send;
103
+ - unproved absence leaves the message `pending` and cannot cause a retry before branch reporting;
104
+ - a stale or disconnected coordinator epoch cannot send or report message state;
105
+ - active-branch evidence changes a matching pending or cancelled message to sent;
106
+ - an unended sent message receives a synthetic `lost` end only from an idle-session branch report after Pi restart;
107
+ - terminal status remains visible while its message is pending or its first turn is open, and then for 60 seconds after that turn ends, without retaining workflow authority;
108
+ - restart creates one separate run and never mutates the terminal run;
109
+ - follow-ups wait for the terminal turn and use the same coordinator; and
110
+ - no deferred-turn table, sender, or production fallback remains.
@@ -214,51 +214,49 @@ pi-workflows keeps credential references in a separate private file. A Telegram
214
214
  }
215
215
  ```
216
216
 
217
- Run `/workflow-channel setup` in Pi TUI to verify and install a profile, `/workflow-channel status` to inspect whether profiles are active, and `/workflow-channel reload` after a private configuration change. Setup asks for the token file path, not the token. It updates mode-`0600` private files and does not copy the token. Token values never enter source files, workflow inputs, SQLite runs, logs, child environments, or model-visible tool results.
217
+ Run `/workflow-channel setup` in Pi TUI to verify and install a profile, `/workflow-channel status` to inspect active profiles and uncertain operations, and `/workflow-channel reload` after a private configuration change. When status reports an uncertain operation, inspect Telegram before you run `/workflow-channel recover <message-id> confirm|retry`. `confirm` records that the operation happened. `retry` starts a new attempt and can duplicate the Telegram operation when the first result cannot be proved.
218
218
 
219
- The same Unix account can read a local credential file. This design prevents accidental propagation, not a hostile same-account process. A separately owned connector can implement the same channel interface later if stronger isolation becomes necessary.
219
+ Setup asks for the token file path, not the token. It updates mode-`0600` private files and does not copy the token. Token values never enter source files, workflow inputs, SQLite runs, logs, child environments, or model-visible tool results.
220
+
221
+ The same Unix account can read a local credential file. This design prevents accidental propagation, not a hostile same-account process. A separately owned connector can implement the same channel-child protocol later if stronger isolation becomes necessary.
220
222
 
221
223
  ## Channel interface
222
224
 
223
- All decision channels implement one interface:
225
+ The host owns decision state and launches each external channel adapter as a supervised child process. The private child protocol has these message kinds:
224
226
 
225
- ```typescript
226
- interface HumanDecisionChannel {
227
- readonly id: string;
228
- start(): Promise<void>;
229
- deliver(request: HumanDecisionRequest): Promise<DecisionDeliveryResult>;
230
- settle(decision: AcceptedHumanDecision | HumanDecisionCancellation): Promise<void>;
231
- stop(): Promise<void>;
232
- }
233
- ```
227
+ - `channel.ready`;
228
+ - `channel.present`;
229
+ - `channel.answer`;
230
+ - `channel.settle`; and
231
+ - `channel.exiting`.
234
232
 
235
- The context exposes a narrow answer submission function. It does not expose the workflow engine, arbitrary run mutation, or another channel's credentials.
233
+ Each message names the adapter epoch, channel profile, saved request or settlement record, expected revision, and stable attempt ID. The host validates and saves every state change. The child never opens SQLite, loads workflow code, changes a run directly, receives the canonical decision subject, or receives another channel's credentials.
236
234
 
237
- Channel delivery is independent from workflow routing. A failed Telegram delivery leaves the decision available in Pi. Audience policy decides whether one successful channel is enough or whether all configured channels are required before the request is considered delivered.
235
+ Channel handling is independent from workflow routing. A failed Telegram send leaves the decision available in Pi. Audience policy decides whether one successful channel is enough or whether all configured channels must receive the request.
238
236
 
239
237
  ### Pi channel
240
238
 
241
- The Pi channel uses documented extension UI APIs. It shows the request, lists the choices, and opens a text editor when the selected choice requires input. The host records the interactive session as the answer source.
239
+ The host creates one `decision` workflow message for the origin session. The shared extension coordinator shows it through documented `pi.sendMessage()` with no model turn. The message lists the request ID, choices, input rule, and deadline. It uses no blocking Pi dialog and no private Pi API.
242
240
 
243
- The waiting workflow remains durable before the UI opens. Closing Pi or cancelling the view cannot lose the checkpoint. A later Pi session can reopen pending decisions.
241
+ A verified operator answers with `/workflow answer` or the matching `piw` control. The host validates the choice and optional text, records the Pi channel as the source, and accepts only the first valid winner. Closing or reloading Pi cannot lose the request. On `session_start` or `session_tree`, the extension adopts an existing decision message or creates one new `decision` message when the pending request has no entry on the active branch.
244
242
 
245
243
  ### Telegram channel
246
244
 
247
- The Telegram channel uses the Bot API and private profile configuration. It sends one decision message with inline buttons.
245
+ The Telegram adapter uses the Bot API and private profile configuration. It sends one decision message with inline buttons.
248
246
 
249
247
  A choice without input submits from its button. A text choice such as `replan` works as follows:
250
248
 
251
249
  1. the operator presses **Replan**;
252
250
  2. the bot sends a `ForceReply` prompt tied to that decision and choice;
253
251
  3. the operator replies to that exact prompt;
254
- 4. the channel verifies the numeric user ID, chat ID, reply message ID, decision ID, and request digest; and
252
+ 4. the adapter verifies the numeric user ID, chat ID, reply message ID, decision ID, and request digest; and
255
253
  5. the exact received text becomes `input.instructions`.
256
254
 
257
- The adapter does not infer a choice from ordinary chat text. Callback payloads contain short opaque IDs because Telegram limits callback data. Private local SQLite rows map each opaque ID to the validated decision presentation. Credentials remain outside the database.
255
+ The adapter does not infer a choice from ordinary chat text. Callback payloads contain short opaque IDs because Telegram limits callback data. The host's channel records map each opaque ID to the validated decision request. Credentials remain outside the database and reach only the matching supervised adapter child.
258
256
 
259
- Telegram permits one long-polling consumer for a bot profile. Active Pi processes use the shared SQLite lease so one process owns polling and the others observe the same channel state. The lease owner can accept a verified reply, but only the Pi session that owns the waiting run creates its continuation. Active sessions inspect the durable accepted-answer fence and recover their own continuation. If no Pi process is running, Telegram delivery and reply collection resume when Pi starts again. Running an always-on service is outside this design.
257
+ Telegram permits one long-polling consumer for a bot profile. The global host starts at most one adapter child for that profile and keeps the package-owned on-demand host process alive while an external decision is pending. No Pi process or adapter child owns a SQLite lease. If the host stops, the next Pi, CLI, or `piw` client starts it and the host recovers the saved channel state before it starts another adapter child. This design installs no operating-system service.
260
258
 
261
- The Bot API does not provide an idempotency key for `sendMessage`. pi-workflows therefore writes a delivery intent before sending and never blindly retries an ambiguous send. A timed-out send is recorded as `unknown`; Pi remains available and an operator can request another delivery. This avoids automatic duplicate messages while keeping decision acceptance exactly once.
259
+ The Bot API does not provide an idempotency key for `sendMessage`. Before it tells the adapter to send or settle a message, the host records the exact attempt in `effects` and `effect_attempts`. A confirmed Telegram message ID settles the effect and remains in its result. A timed-out, disconnected, or interrupted exact attempt with no proof becomes `ambiguous` and is not retried automatically. Pi remains available while the operator checks Telegram and explicitly confirms or retries the operation. This prevents blind duplicate sends without claiming exactly-once Telegram behavior.
262
260
 
263
261
  ## Durable decision records
264
262
 
@@ -268,8 +266,8 @@ Human decisions use the canonical [SQLite state](SQLITE_STATE.md) database:
268
266
  - `human_decision_submissions` records human, policy, channel, and control candidates;
269
267
  - `human_decision_resolutions` stores the one accepted-or-cancelled winner;
270
268
  - `continuations` links the parent and continuation runs;
271
- - `effects` records parent settlement, continuation, and presentation settlement work; and
272
- - channel tables store delivery and settlement receipts.
269
+ - `effects` and `effect_attempts` record parent settlement, continuation, external delivery, and external settlement work; and
270
+ - `channels`, `channel_cursors`, and `channel_messages` store channel identity, polling position, and decision delivery or settlement receipts.
273
271
 
274
272
  A valid human answer, eligible timeout policy, explicit cancellation, or no-default expiry competes for the same resolution primary key. The winning transaction records the immutable resolution, audit event, and required effects together. A retry adopts the existing matching result. A conflicting or late answer receives the durable winner.
275
273
 
@@ -315,7 +313,7 @@ Recovery follows these rules:
315
313
  - duplicate channel updates are harmless;
316
314
  - stale responses are rejected;
317
315
  - one human or timeout response creates one continuation;
318
- - the winning human answer or timeout policy dismisses any pending Pi dialog;
316
+ - the winning human answer or timeout policy settles or cancels the pending Pi decision workflow message;
319
317
  - confirmed channel settlement is adopted without another remote call;
320
318
  - failed channel settlement has a bounded retry count and cannot create an unbounded record loop; and
321
319
  - cancellation resolves the owned waiting decision from durable state, even after restart, closes pending views, and prevents a later answer from continuing the run.
@@ -330,15 +328,15 @@ Adoption does not change the lease, claim generation, queue state, timestamps, o
330
328
 
331
329
  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.
332
330
 
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.
331
+ The engine remains independent from Pi and Telegram. Core code owns decision contracts and validation. The host owns durable acceptance, continuation, workflow messages, and channel-child supervision. The Pi extension owns documented session presentation and controls. The Telegram adapter child owns Bot API translation. Workflow definitions own only the question, choices, audience, and routes.
334
332
 
335
333
  ## Contract impact
336
334
 
337
335
  - **Session state:** Pi records normal workflow messages and interactive decision results.
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.
336
+ - **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. External channel attempts use the shared effect records, and channel delivery and settlement records remain separate feature evidence.
339
337
  - **Pi internals:** none.
340
- - **Public Pi API:** documented extension lifecycle and UI methods only.
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
+ - **Public Pi API:** documented extension lifecycle, message, command, session, and status APIs only.
339
+ - **Public pi-workflows API:** typed human choices, `humanDecision().onTimeout`, `humanDecisionEdge()`, channel profile configuration, the plan approval policy, the shared plan-change workflow, and the additive queue prepare-or-adopt operation.
342
340
 
343
341
  ## Verification requirements
344
342
 
@@ -349,7 +347,8 @@ The implementation must test:
349
347
  - runtime choice and input validation;
350
348
  - legacy checkpoint continuation;
351
349
  - model-tool answer rejection;
352
- - Pi interactive answers;
350
+ - Pi decision workflow messages, branch adoption, branch-specific decision messages, and verified answers;
351
+ - supervised channel-child protocol fencing and restart recovery;
353
352
  - Telegram callbacks and reply binding with a fake Bot API;
354
353
  - unauthorized users and chats;
355
354
  - stale request digests;
@@ -361,7 +360,7 @@ The implementation must test:
361
360
  - identical and conflicting retries;
362
361
  - crashes before and after answer acceptance and continuation creation;
363
362
  - ambiguous Telegram sends;
364
- - long-poll lease handoff between Pi processes;
363
+ - one supervised long-poll adapter per profile and host-restart recovery;
365
364
  - decision cancellation and expiry;
366
365
  - included `plan-approval` routes and bounded replan loops;
367
366
  - viewer redaction; and
@@ -1,6 +1,6 @@
1
1
  # Human decision presentations
2
2
 
3
- This contract is implemented. Human decision requests separate their canonical
3
+ Human decision requests separate their canonical
4
4
  subject from the complete readable message shown to an operator. Human decisions use
5
5
  one v1 contract with no legacy body form or parallel schema version.
6
6
  The implementation plan is in
@@ -216,20 +216,13 @@ The renderer:
216
216
 
217
217
  The renderer never adds an ellipsis in place of omitted decision content.
218
218
 
219
- The channel records intent before sending. It records each part after an
220
- unambiguous response. If a send result is ambiguous, it marks delivery unknown
221
- and does not retry that part or later parts automatically. Another configured
222
- channel can still answer the decision.
219
+ The host saves each channel message before the adapter sends it. It records each part after an unambiguous response. If a send result is uncertain, the host marks that channel message `ambiguous` and does not retry that part or later parts automatically. Another configured channel can still answer the decision.
223
220
 
224
221
  ### Pi
225
222
 
226
- Pi uses the documented extension TUI API. A custom decision component shows the
227
- complete presentation and fingerprint together with the choices and optional
228
- text prompt. It wraps and scrolls while responding to resize, theme, cancellation,
229
- and `AbortSignal` events.
223
+ The host creates one `decision` workflow message. The shared extension coordinator sends it through documented `pi.sendMessage()` without starting a model turn. Its custom renderer shows the complete presentation and fingerprint with the choices, input rules, request ID, and deadline. It wraps and scrolls with normal Pi custom-message behavior.
230
224
 
231
- When Telegram or another channel accepts the decision, the signal closes the Pi
232
- dialog. pi-workflows does not modify Pi core or use undocumented TUI state.
225
+ A verified operator answers through `/workflow answer` or the matching `piw` control. When Telegram or another channel accepts the decision, the host cancels an unsent Pi decision message. A sent card remains normal conversation history, while later answer controls return the saved winner. Pi Workflows does not open a blocking dialog, modify Pi core, or use undocumented TUI state.
233
226
 
234
227
  ### Other channels
235
228