@osolmaz/pi-workflows 0.15.3 → 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.
- package/README.md +24 -8
- package/dist/channels/adapter-entry.d.ts +3 -0
- package/dist/channels/adapter-entry.js +203 -0
- package/dist/channels/adapter-entry.js.map +1 -0
- package/dist/channels/config.d.ts +43 -0
- package/dist/channels/config.js +234 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/protocol.d.ts +94 -0
- package/dist/channels/protocol.js +219 -0
- package/dist/channels/protocol.js.map +1 -0
- package/dist/channels/telegram.d.ts +46 -0
- package/dist/channels/telegram.js +285 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +102 -0
- package/dist/client/client.js +737 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +275 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +144 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +47 -157
- package/dist/controllers/sqlite.js +230 -406
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -1
- package/dist/extension/index.js +734 -555
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +95 -0
- package/dist/extension/recorder.js +530 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/remote-recorder-store.d.ts +25 -0
- package/dist/extension/remote-recorder-store.js +81 -0
- package/dist/extension/remote-recorder-store.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/session-view.d.ts +9 -2
- package/dist/extension/session-view.js +117 -51
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/step-message.d.ts +6 -4
- package/dist/extension/step-message.js +12 -6
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.d.ts +3 -2
- package/dist/extension/widget.js +41 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +27 -0
- package/dist/extension/workflow-message-coordinator.js +294 -0
- package/dist/extension/workflow-message-coordinator.js.map +1 -0
- package/dist/host/channel-effects.d.ts +72 -0
- package/dist/host/channel-effects.js +271 -0
- package/dist/host/channel-effects.js.map +1 -0
- package/dist/host/channel-supervisor.d.ts +21 -0
- package/dist/host/channel-supervisor.js +54 -0
- package/dist/host/channel-supervisor.js.map +1 -0
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +54 -8
- package/dist/host/runner.js +1997 -305
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +25 -19
- package/dist/host/state.js +303 -89
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +83 -0
- package/dist/host/view.js +975 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +9 -33
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +88 -98
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +96 -0
- package/dist/state/workflow-messages.js +335 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +245 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +2 -0
- package/dist/workflows/human-decision.js +8 -2
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +67 -20
- package/dist/workflows/store.js +451 -378
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +21 -0
- package/dist/workflows/tool-input.js +23 -1
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +5 -5
- package/dist/workflows/workflow-message-content.d.ts +38 -0
- package/dist/workflows/workflow-message-content.js +157 -0
- package/dist/workflows/workflow-message-content.js.map +1 -0
- package/docs/2026-08-18-herdr-piw-plan.md +2 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +26 -50
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/DEFERRED_TURNS.md +66 -294
- package/docs/HUMAN_DECISIONS.md +29 -30
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
- package/docs/SQLITE_STATE.md +31 -25
- package/docs/WORKFLOW_HOST.md +155 -77
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +74 -132
- package/docs/tui-viewer.md +12 -14
- package/docs/workflows.md +78 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/plugins/herdr/viewer.mjs +14 -5
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/channels/adapter-entry.ts +220 -0
- package/src/channels/config.ts +296 -0
- package/src/channels/protocol.ts +333 -0
- package/src/channels/telegram.ts +335 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +939 -0
- package/src/client/index.ts +25 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +333 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +163 -0
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +339 -808
- package/src/extension/index.ts +853 -665
- package/src/extension/recorder.ts +687 -0
- package/src/extension/remote-recorder-store.ts +126 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/session-view.ts +157 -51
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +53 -25
- package/src/extension/workflow-message-coordinator.ts +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +2639 -450
- package/src/host/state.ts +404 -137
- package/src/host/view.ts +1212 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +14 -42
- package/src/state/schema.ts +88 -98
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +261 -124
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +650 -511
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +5 -5
- package/src/workflows/workflow-message-content.ts +197 -0
- package/dist/extension/decision-channels.d.ts +0 -134
- package/dist/extension/decision-channels.js +0 -1307
- package/dist/extension/decision-channels.js.map +0 -1
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/extension/decision-channels.ts +0 -1826
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
package/docs/SQLITE_STATE.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# SQLite state
|
|
2
2
|
|
|
3
|
+
Status: this is the implemented single-host database contract. The [workflow-message plan](2026-09-02-unify-workflow-messages-plan.md) records the schema version 1 hard cut that unified Pi message state and restored hosted behavior.
|
|
4
|
+
|
|
3
5
|
Pi Workflows stores all live durable state in one database:
|
|
4
6
|
|
|
5
7
|
```text
|
|
6
8
|
~/.pi/agent/workflows/state.sqlite
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
There is one database for the user installation. Project and run IDs separate data inside it. Workflow targets do not
|
|
11
|
+
There is one database for the user installation. Project and run IDs separate data inside it. The host is the only production process that opens this live database. Workflow targets, extensions, CLI clients, Herdr adapters, and `piw` do not open it. Live clients use `pi-workflows.client.v1`.
|
|
10
12
|
|
|
11
13
|
## Viewer projection
|
|
12
14
|
|
|
13
|
-
The database includes the [incremental and virtualized viewer design](plans/2026-08-28-piw-incremental-viewer-plan.md).
|
|
15
|
+
The database includes the [incremental and virtualized viewer design](plans/2026-08-28-piw-incremental-viewer-plan.md). The host owns this projection and exposes it as the canonical live run view. Local and remote renderers do not recreate it or validate its SQLite tables.
|
|
14
16
|
|
|
15
|
-
`viewer_runs` stores one presentation revision and retained revision floor for each run. `viewer_deltas` stores ordered target patches by run, presentation revision, and delta index. `viewer_session_checkpoints` stores the bounded active message and tool state at each 256-event boundary. A viewer-visible transaction writes the domain change, advances the presentation revision, and writes its patch blobs before the same commit. Session-event transactions write each reached replay checkpoint in that transaction.
|
|
17
|
+
`viewer_runs` stores one presentation revision and retained revision floor for each run. `viewer_deltas` stores ordered target patches by run, presentation revision, and delta index. `viewer_session_checkpoints` stores the bounded active message and tool state at each 256-event boundary. `run_view_content` stores generated reference bytes under the exact run ID, content digest, and media type. It is separate from general state blobs, and content reads require all three identities. A viewer-visible transaction writes the domain change, advances the presentation revision, and writes its patch blobs before the same commit. Session-event transactions write each reached replay checkpoint in that transaction.
|
|
16
18
|
|
|
17
19
|
The store retains 256 presentation revisions. A reader with an older cursor must take a bounded snapshot. Patches use `add`, `replace`, `remove`, and `append`. They target small projection documents or pages. Patch creation does not reconstruct and compare complete run views.
|
|
18
20
|
|
|
@@ -31,7 +33,7 @@ The database stores:
|
|
|
31
33
|
- run and controller queues, claims, retries, and continuations
|
|
32
34
|
- human-decision requests, submissions, resolutions, and cancellations
|
|
33
35
|
- controller resources, finalizers, effects, and child workflows
|
|
34
|
-
-
|
|
36
|
+
- workflow messages that Pi must add to origin conversations
|
|
35
37
|
- workflow settings, accepted JSON Patch changes, and post-completion follow-up prompts
|
|
36
38
|
- nonsecret channel cursors, inbox records, messages, and settlement receipts
|
|
37
39
|
- canonical JSON, text, and large text values
|
|
@@ -60,9 +62,9 @@ The database also uses:
|
|
|
60
62
|
- directory mode `0700`
|
|
61
63
|
- database and backup mode `0600`
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
The host opens the active database. It verifies the application ID, user version, schema metadata, compiled DDL digest, and exact SQLite schema shape. An incompatible database fails with the standard backup-and-reset instruction. Pi Workflows does not import, reinterpret, or delete that state.
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
The host completes this verification before it serves any client, and no other production process opens the active database. A maintenance verifier may open an explicit inactive backup with SQLite read-only mode and `PRAGMA query_only = ON`. That offline verification path is not a live client and cannot select the active state database. TypeScript and Rust clients validate the client protocol and package versions, not the SQLite DDL digest.
|
|
66
68
|
|
|
67
69
|
The normalized run layout is an in-place alpha cutover. It keeps SQLite user version `1` and the current `v1` public record identifiers. A database with the former nested run-snapshot layout is incompatible and must be moved or removed. There is no migration, compatibility reader, dual write, alias, or second schema generation.
|
|
68
70
|
|
|
@@ -72,7 +74,7 @@ Four record groups provide the common lifecycle rules.
|
|
|
72
74
|
|
|
73
75
|
### Resources
|
|
74
76
|
|
|
75
|
-
`resources` identifies each mutable aggregate and holds its current revision. Runs, settings scopes, follow-up
|
|
77
|
+
`resources` identifies each mutable aggregate and holds its current revision. Runs, settings scopes, follow-up items, decisions, controller resources, effects, channels, workflow messages, and session segments have stable resource identities.
|
|
76
78
|
|
|
77
79
|
Every accepted domain command compares its expected revision and increments it once.
|
|
78
80
|
|
|
@@ -104,6 +106,8 @@ A domain row and its event are written in one transaction. Normal APIs never upd
|
|
|
104
106
|
|
|
105
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.
|
|
106
108
|
|
|
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
|
+
|
|
107
111
|
## Domain tables
|
|
108
112
|
|
|
109
113
|
The shared records do not replace domain schemas. The following `STRICT` tables keep the state explicit:
|
|
@@ -111,20 +115,24 @@ The shared records do not replace domain schemas. The following `STRICT` tables
|
|
|
111
115
|
| Area | Tables |
|
|
112
116
|
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
113
117
|
| Schema and projects | `schema_meta`, `projects` |
|
|
114
|
-
| Content | `blobs`
|
|
118
|
+
| Content | `blobs`, `run_view_content` |
|
|
115
119
|
| Shared lifecycle | `resources`, `leases`, `events`, `workflow_host_state` |
|
|
116
120
|
| Host protocol | `host_commands`, `run_workers`, `worker_messages`, `interactive_requests`, `interactive_submissions` |
|
|
117
121
|
| Workflows | `workflow_definitions`, `runs`, `run_sources`, `run_steps`, `run_bindings`, `run_queue`, `node_attempts`, `workflow_updates` |
|
|
118
122
|
| Live settings | `workflow_settings`, `workflow_setting_changes` |
|
|
119
|
-
| Post-run follow-ups | `
|
|
123
|
+
| Post-run follow-ups | `workflow_follow_ups` |
|
|
120
124
|
| Session capture | `session_segments`, `session_entries`, `attempt_entries`, `session_events` |
|
|
121
125
|
| Human decisions | `human_decisions`, `human_decision_resolutions`, `human_decision_submissions`, `continuations` |
|
|
122
126
|
| Controllers | `controller_resources`, `controller_finalizers`, `controller_queue`, `controller_workflows` |
|
|
123
127
|
| Effects | `effects`, `effect_attempts` |
|
|
124
|
-
| Pi
|
|
125
|
-
| Channels | `channels`, `channel_cursors`, `
|
|
128
|
+
| Pi messages | `workflow_messages` |
|
|
129
|
+
| Channels | `channels`, `channel_cursors`, `channel_messages` |
|
|
130
|
+
|
|
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.
|
|
126
132
|
|
|
127
|
-
|
|
133
|
+
`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
|
+
|
|
135
|
+
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.
|
|
128
136
|
|
|
129
137
|
### Hosted commands and interactions
|
|
130
138
|
|
|
@@ -138,15 +146,11 @@ conflict.
|
|
|
138
146
|
process identity and terminal outcome. `worker_messages` deduplicates accepted
|
|
139
147
|
state-changing child messages.
|
|
140
148
|
|
|
141
|
-
`interactive_requests` owns durable origin-session work. It stores the run,
|
|
142
|
-
node attempt, target session, contract, presentation claim, accepted
|
|
143
|
-
submission, and revision. `interactive_submissions` stores the idempotency key,
|
|
144
|
-
payload, outcome, and receipt. Pi reload can adopt the saved presentation entry
|
|
145
|
-
without inserting another visible message.
|
|
149
|
+
`interactive_requests` owns the durable request contract and workflow state for origin-session work. It stores the run, node attempt, target session, contract, request status, accepted submission, `unproductiveTurnEnds`, and revision. Pause is stored once on the run and derived for its one pending interaction. `interactive_submissions` stores the idempotency key, payload, outcome, and receipt. It stores no Pi presentation claim or Pi session entry. `workflow_messages` owns those facts, and the extension reports the active Pi branch after reload.
|
|
146
150
|
|
|
147
151
|
## Content-addressed values
|
|
148
152
|
|
|
149
|
-
`blobs` stores canonical JSON and UTF-8 text as bytes. Its primary key is the 32-byte SHA-256 digest of the bytes.
|
|
153
|
+
`blobs` stores canonical JSON and UTF-8 text as bytes. Its primary key is the 32-byte SHA-256 digest of the bytes. `run_view_content` keeps host-generated large view values reachable for the life of the run, including aggregate outputs that do not exist as one source record. The host creates this link before it sends a content reference. Deleting the run removes the link, and normal blob pruning can then remove unreferenced content.
|
|
150
154
|
|
|
151
155
|
Insertion verifies the digest, media type, byte length, and exact bytes. Repeated content adopts the existing row. This replaces separate artifact files while keeping outputs, errors, settled Pi entries, and rendered channel text deduplicated. Opening the database never deletes blobs. The explicit prune command removes unreferenced blobs after it deletes safe old run trees.
|
|
152
156
|
|
|
@@ -195,11 +199,11 @@ Reading or finding a row never gives write authority.
|
|
|
195
199
|
- A run owner may advance the run, apply automatic decision policy, create its continuation, settle its parent, and complete its queue work.
|
|
196
200
|
- A controller claim owner may update controller status, reserve effects, and start child workflows for that resource.
|
|
197
201
|
- A verified human channel actor may submit one answer candidate for the named decision. It does not gain run ownership.
|
|
198
|
-
-
|
|
202
|
+
- The host-owned channel adapter path may update only its channel cursor, decision delivery and settlement records, and exact managed effects.
|
|
199
203
|
- Control commands have narrow explicit operations, such as requesting cancellation or deletion.
|
|
200
204
|
- Model-originated workflow answers cannot resolve protected human decisions.
|
|
201
205
|
|
|
202
|
-
The global host is the normal state writer. Its protected stores check ownership and renew the exact live claim in the same transaction as the write. A stale or expired owner cannot renew itself. Pi extensions and
|
|
206
|
+
The global host is the sole live database owner and the normal state writer. Its protected stores check ownership and renew the exact live claim in the same transaction as the write. A stale or expired owner cannot renew itself. Pi extensions, CLI commands, Herdr adapters, and the Rust `piw` program use the versioned client protocol for live reads and controls. They do not open the active database. Only explicit inactive backup verification remains a direct read-only SQLite operation.
|
|
203
207
|
|
|
204
208
|
## Competing outcomes
|
|
205
209
|
|
|
@@ -215,13 +219,13 @@ The same rule applies to run terminal outcomes, continuation admission, queue se
|
|
|
215
219
|
|
|
216
220
|
## Read contract
|
|
217
221
|
|
|
218
|
-
|
|
222
|
+
Durable status is a pure projection of domain rows, immutable facts, current leases, effect results, and exact workflow-turn start and end reports. The host uses these facts to produce one live run view. An open workflow turn can change display status only. It cannot change workflow authority. Every renderer consumes the host-produced display status and allowed controls without running another status reducer.
|
|
219
223
|
|
|
220
224
|
A settings scope uses its resource revision as its public change number. Each accepted patch, current value, and node binding is saved in one transaction. A checkpoint continuation keeps the same settings resources and transfers them to the continuation run.
|
|
221
225
|
|
|
222
|
-
|
|
226
|
+
`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.
|
|
223
227
|
|
|
224
|
-
- A terminal run fact overrides stale
|
|
228
|
+
- A terminal run fact overrides stale message state.
|
|
225
229
|
- An accepted decision is accepted even if its continuation effect is still pending.
|
|
226
230
|
- A cancelled decision is cancelled even if parent cleanup is still pending.
|
|
227
231
|
- A stale owner is not shown as current.
|
|
@@ -233,7 +237,7 @@ Read paths do not repair state. Owner reconcilers apply pending effects and writ
|
|
|
233
237
|
|
|
234
238
|
All projects use the same file. `projects` stores a stable ID and canonical path. Project-scoped controller and run queries use that key. One global host owns the file for the user installation. Its socket, lock, and exact child-process registry are under `~/.pi/agent/workflows/host/`. A second live host is rejected even when it was started from another project.
|
|
235
239
|
|
|
236
|
-
SQLite WAL
|
|
240
|
+
SQLite WAL keeps bounded projection reads consistent with commits. Writers are serialized by SQLite and must keep transactions short. Hashing, model calls, shell work, and external requests happen outside write transactions. Production clients receive revisioned snapshots, patches, and pages from the host instead of opening concurrent SQLite readers.
|
|
237
241
|
|
|
238
242
|
This contract is for local storage on one machine. It does not claim distributed consensus or network-filesystem safety.
|
|
239
243
|
|
|
@@ -260,6 +264,8 @@ pi-workflows state prune --before 2026-08-01T00:00:00Z --dry-run
|
|
|
260
264
|
pi-workflows state prune --before 2026-08-01T00:00:00Z --backup /absolute/path/to/before-prune.sqlite --apply
|
|
261
265
|
```
|
|
262
266
|
|
|
267
|
+
These commands send maintenance operations to the host when they target the active database. Only `pi-workflows state verify` with an explicit inactive backup opens SQLite in the command process. It rejects the active database, including another path to the same file.
|
|
268
|
+
|
|
263
269
|
`status` reports only safe counts, file size, active leases, and unsettled effects.
|
|
264
270
|
It does not print actor IDs, channel references, payloads, or credentials.
|
|
265
271
|
|
|
@@ -267,6 +273,6 @@ It does not print actor IDs, channel references, payloads, or credentials.
|
|
|
267
273
|
|
|
268
274
|
## Alpha cutover
|
|
269
275
|
|
|
270
|
-
|
|
276
|
+
The persisted-state alpha boundary is a hard cut. Pi Workflows has no normal reader or writer for older live storage. It does not use dual reads, dual writes, aliases, versioned state roots, or automatic import. No direct live-state client, replay server reader, or Rust SQLite fallback remains outside the host.
|
|
271
277
|
|
|
272
278
|
Older state remains untouched. Pi Workflows fails before mutation with this instruction: “Pi Workflows durable state is incompatible. Back up and move state.sqlite with its -wal and -shm files, then start Pi Workflows to create a new state.sqlite database. The incompatible state was not changed.”
|
package/docs/WORKFLOW_HOST.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Workflow host
|
|
2
2
|
|
|
3
|
-
Status: implemented. [
|
|
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.
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -21,7 +21,10 @@ The host solves two different failures:
|
|
|
21
21
|
- **Generation:** A number increased each time a new owner claims a run. It fences older owners.
|
|
22
22
|
- **Durable boundary:** A committed node or lifecycle transition from which execution can resume.
|
|
23
23
|
- **Interactive request:** A durable agent or assistant-message step that must run in the origin Pi session.
|
|
24
|
+
- **Workflow message:** Host-owned content that Pi must add to an origin conversation, such as a step, reminder, decision, notification, terminal result, or follow-up.
|
|
24
25
|
- **Managed effect:** A side effect reserved and settled through an idempotent durable record.
|
|
26
|
+
- **Live run view:** The host's versioned, bounded projection of one run, including its durable state, current origin-session activity, allowed controls, and page cursors.
|
|
27
|
+
- **Renderer:** A Pi widget, status line, command-line view, Herdr adapter, or `piw` screen that displays or acts on a live run view without deriving workflow state.
|
|
25
28
|
|
|
26
29
|
## Boundaries
|
|
27
30
|
|
|
@@ -37,20 +40,26 @@ One host owns the global workflow database for one user installation.
|
|
|
37
40
|
|
|
38
41
|
```text
|
|
39
42
|
Pi extension ─┐
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
CLI client ───┼── WorkflowClient v1 ── local socket ── workflow host ── SQLite
|
|
44
|
+
piw ──────────┘ │
|
|
45
|
+
├── run worker A
|
|
46
|
+
Remote piw ── SSH tunnel ── loopback WebSocket relay ────────┤
|
|
47
|
+
├── run worker B ── headless pi --mode rpc
|
|
48
|
+
├── controller worker
|
|
49
|
+
├── channel adapter child
|
|
50
|
+
└── source resolver
|
|
46
51
|
```
|
|
47
52
|
|
|
53
|
+
The local socket and loopback WebSocket relay carry the same logical client protocol and live run view. The relay reads no state and translates no domain contract. The host is the only production process that opens the live SQLite database. Worker, channel-adapter, and source-resolver channels are private supervision protocols, not alternate client interfaces.
|
|
54
|
+
|
|
48
55
|
The host may manage runs from more than one project. Each run keeps its canonical project path and source identity.
|
|
49
56
|
|
|
50
|
-
The host process performs only bounded protocol handling, short SQLite transactions, timers, queue scheduling, and process supervision. It does not import or execute workflow definitions.
|
|
57
|
+
The host process performs only bounded protocol handling, live-view projection, short SQLite transactions, timers, queue scheduling, and process supervision. It does not import or execute workflow definitions.
|
|
51
58
|
|
|
52
59
|
A worker loads one workflow source and executes one run generation. It cannot receive a writable `WorkflowRunStore`. It proposes changes to the host over a private child channel. This is an architectural guard against accidental writes. It is not a security sandbox against code running as the same operating-system user.
|
|
53
60
|
|
|
61
|
+
A channel adapter child handles one approved external presentation channel. It receives only the rendered presentation and the private channel configuration needed for its work. It does not open SQLite or receive the decision subject. The host owns claims, answer verification, settlement, and process supervision.
|
|
62
|
+
|
|
54
63
|
## Host lifecycle
|
|
55
64
|
|
|
56
65
|
The package CLI owns host lifecycle commands:
|
|
@@ -66,7 +75,7 @@ pi-workflows host run
|
|
|
66
75
|
|
|
67
76
|
The host uses one global lock and one host epoch. Socket creation and the SQLite host claim must agree before the host accepts commands. A second live host refuses to start. After the old host lease expires, a new host increases the epoch before it handles work. Messages from an older epoch are rejected.
|
|
68
77
|
|
|
69
|
-
The host stays alive while it has a connected client, an active worker, a scheduled wake, a pending controller, or unsettled work. An idle host may exit after a documented idle period. A later client can start it again.
|
|
78
|
+
The host stays alive while it has a connected client, an active worker, a scheduled wake, a pending controller, a pending external-channel decision, or other unsettled work. An idle host may exit after a documented idle period. A later client can start it again.
|
|
70
79
|
|
|
71
80
|
## Claim rules
|
|
72
81
|
|
|
@@ -185,17 +194,20 @@ The child protocol must apply backpressure. A child that exceeds message or outp
|
|
|
185
194
|
|
|
186
195
|
The process registry includes a process start identity, not only a PID. The host accepts a worker registration only when the PID is a direct child of that active worker. A reused PID cannot let a new host kill an unrelated process.
|
|
187
196
|
|
|
188
|
-
##
|
|
197
|
+
## Live client protocol
|
|
198
|
+
|
|
199
|
+
Every production client uses one versioned `WorkflowClient` protocol. No extension, CLI command, Herdr adapter, or `piw` mode opens the live SQLite database. Clients connect through a user-only local socket. Unix socket mode is `0600`. Other platforms use their equivalent local transport and access control. Remote viewing uses a loopback-only WebSocket relay through an SSH tunnel. The relay carries the same messages and does not read SQLite.
|
|
189
200
|
|
|
190
|
-
|
|
201
|
+
The alpha hard cut replaces the existing host request and replay protocols in place with `pi-workflows.client.v1`. It adds no `v2`, compatibility path, fallback reader, or second live protocol. One neutral JSON schema is the wire-contract source for TypeScript and Rust. Shared conformance fixtures must pass in both languages.
|
|
191
202
|
|
|
192
|
-
Messages use newline-delimited canonical JSON. One message is at most 1 MiB, matching the existing durable event limit. The receiver closes only the offending connection when framing or validation fails.
|
|
203
|
+
Messages use newline-delimited canonical JSON on the local socket and one canonical JSON object per WebSocket message. TypeScript and Rust use the same ECMAScript number formatting and UTF-16 object-key order for canonical JSON. Both parsers reject unknown envelope fields and non-canonical framing. One message is at most 1 MiB, matching the existing durable event limit. The receiver closes only the offending connection when framing or validation fails.
|
|
193
204
|
|
|
194
|
-
|
|
205
|
+
Each message uses one envelope:
|
|
195
206
|
|
|
196
207
|
```json
|
|
197
208
|
{
|
|
198
|
-
"schema": "pi-workflows.
|
|
209
|
+
"schema": "pi-workflows.client.v1",
|
|
210
|
+
"type": "request",
|
|
199
211
|
"requestId": "opaque-id",
|
|
200
212
|
"clientId": "opaque-id",
|
|
201
213
|
"operation": "run.cancel",
|
|
@@ -206,57 +218,66 @@ Every request uses this envelope:
|
|
|
206
218
|
}
|
|
207
219
|
```
|
|
208
220
|
|
|
209
|
-
A response
|
|
221
|
+
The `type` is `hello`, `request`, `response`, or `event`. A response repeats the request ID and includes its outcome, revision, receipt, or bounded safe error. An event names its subscription and carries one revisioned run-list snapshot, run-view snapshot, patch, page, origin-session workflow-message change, or availability change. Valid command outcomes remain `accepted`, `adopted`, `rejected`, `conflict`, `notFound`, `claimLost`, and `unavailable`.
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
"schema": "pi-workflows.host-response.v1",
|
|
214
|
-
"requestId": "opaque-id",
|
|
215
|
-
"outcome": "accepted",
|
|
216
|
-
"revision": 13,
|
|
217
|
-
"receipt": {}
|
|
218
|
-
}
|
|
219
|
-
```
|
|
223
|
+
The host commits a command receipt before it acknowledges success. The request ID identifies one transport attempt and is excluded from the durable fingerprint. The Pi extension sends state-changing commands through the durable client path. If a connection closes after commit but before response, a retry uses a new request ID with the same client ID, idempotency key, operation, and payload, then adopts the stored receipt. Reusing a request ID or idempotency key with another durable payload returns a conflict. An `interaction.submit` response stays pending while the supervised child validates the value and settles only after the durable outcome is `accepted`, `adopted`, or `rejected`. A reconnect with the same durable identity and payload waits for and returns that same outcome. Clients do not poll SQLite for submission results.
|
|
220
224
|
|
|
221
|
-
|
|
225
|
+
View and subscription reads do not create receipts. Reconnection restores desired subscriptions from the last accepted presentation revision. A retained revision receives patches. A stale revision receives a bounded snapshot. A slow subscriber gets at most one socket-buffered snapshot at a time because the host waits for drain and coalesces later polls. A backpressured client write stops waiting when its connection closes, its socket fails, or its request is cancelled. Every explicit client unsubscribe removes the matching host subscription, including run-list and origin-session subscriptions.
|
|
222
226
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
- `run.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
-
|
|
241
|
-
|
|
242
|
-
-
|
|
243
|
-
|
|
244
|
-
- `
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
- `
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
227
|
+
The protocol owns four operation groups:
|
|
228
|
+
|
|
229
|
+
- run and controller commands, including start, pause, resume, cancel, restart, decisions, updates, submissions, settings, follow-ups, reconciliation, and host control;
|
|
230
|
+
- live views and recording, including run lists, snapshots, subscriptions, pages, referenced content, origin-session workflow messages, terminal-view clear, and batched session events;
|
|
231
|
+
- active-branch and model-turn reports;
|
|
232
|
+
- state and channel maintenance, including status, verification, backup, prune, channel status, channel reload, and explicit channel recovery against the active database. Backup and applied prune use one fresh CLI idempotency key per user invocation. An automatic reconnect retry keeps that key and uses a new request ID. A later invocation gets a new key. The host finishes an in-flight operation after a disconnect, stores its accepted or rejected receipt before response, waits for it during shutdown, and adopts an exact retry.
|
|
233
|
+
|
|
234
|
+
`workflowMessage.reportBranch` reports workflow message IDs and Pi entry IDs from the complete origin-session view window together with `isIdle` and `hasPendingMessages`. The host adopts matching entries, changes a matching pending or cancelled message to `sent`, closes proved-lost turns, and creates one missing message of the source's own kind after a branch change. `workflowTurn.report` records exact model-turn starts and ends. The host keeps one active coordinator connection and process-local epoch for each origin session. A replacement connection fences the old one. Only the active epoch receives the next eligible pending message or can report branch and turn state. Polling alone creates no durable command.
|
|
235
|
+
|
|
236
|
+
### Live run view
|
|
237
|
+
|
|
238
|
+
The host returns one canonical `pi-workflows.run-view.v1` document. It contains the existing bounded workflow projection and page cursors plus a `display` object. The queue field contains display metadata only. It does not repeat the input, launch options, worker affinity, or claim capability; the complete input remains reachable through the state projection. The `display` object contains the effective status, current activity kind, allowed controls, and the stored reason when action is required. A reason above the shared 16 KiB inline-content threshold uses a small `reason` notice and a digest-bound `reasonContent` reference, so one diagnostic cannot exceed the 1 MiB protocol frame while the complete reason remains available. Renderers use this object directly. They must not combine separate queries or infer status from durable rows.
|
|
239
|
+
|
|
240
|
+
The unfinished node-attempt row is the one durable source for the current node. A running run exposes it as `currentNode`. A parked interactive run exposes the same node as `waitingOn`. A checkpoint has no unfinished attempt, so its completed checkpoint node supplies `waitingOn`. During an exact origin-session model turn, the host `display` changes that waiting node to running for presentation only. It does not infer another node or change the durable run state.
|
|
241
|
+
|
|
242
|
+
Generated referenced content is stored directly in `run_view_content` under its exact run ID, content digest, and media type. It does not share general state-blob media metadata. A content read must match all three values, so a reference from another run or another media representation is unavailable.
|
|
243
|
+
|
|
244
|
+
The origin-session response contains the active run view. When no run is active, it keeps the most recent terminal run visible while its terminal workflow message is pending or its first model turn is open, and then for 60 seconds after that turn ends. A newer run or `sessionView.clearTerminal` removes the retained terminal view. `/workflow clear` and the matching `piw` action call that control without changing workflow state.
|
|
245
|
+
|
|
246
|
+
The response also contains an ordered byte-bounded window of all nonterminal workflow messages and open sent messages needed for recovery, their complete count, and the next eligible pending message ID only for the active coordinator epoch. Message records include the source, content reference, order, state, and Pi entry needed by the shared coordinator. A branch report can name only IDs from this complete window. The host returns all these facts from one consistent read. The extension materializes the complete run revision and message content before it updates the widget or coordinator. After every host connection, it reports the active branch before it sends a workflow message or reports a model turn. Polling an idle session creates no durable command.
|
|
247
|
+
|
|
248
|
+
Each history page has both an item limit and an encoded byte budget. Oversized values become digest-bound content references. Large workflow topology uses bounded node, edge, graph-step, and transition projections plus references for the complete original definition and complete graph history. Before the host advertises a generated reference, it stores the bytes under the exact run ID, content digest, and media type in `run_view_content`. It does not share media metadata with general state blobs. Memory-cache eviction cannot make a reference unavailable. `view.content` returns bounded chunks until the client has the complete value. The client verifies the assembled bytes against both the response digest and the digest in the advertised reference. TypeScript clients assemble every run-history page for one revision and hydrate the complete definition, complete graph history, and all referenced content before they emit a complete non-interactive view or update the Pi widget. Rust automatically requests and verifies the complete referenced definition and graph history, decodes the complete values, and then builds its graph layout. Session-event pages include the replay checkpoint immediately before the first event in the page. A large checkpoint is also a referenced value. TypeScript hydrates it with the run view, and Rust requests and resolves it before replay. A step-centered trace page selects the exact stored attempt first and uses the node ID only if that attempt has no trace event. The run list reads only status facts and never loads complete run histories.
|
|
249
|
+
|
|
250
|
+
The closed `display.status` set is `queued`, `running`, `waiting`, `paused`, `completed`, `failed`, `timed_out`, `cancelled`, and `ambiguous`.
|
|
251
|
+
|
|
252
|
+
The host computes effective status in this order:
|
|
253
|
+
|
|
254
|
+
1. A durable ambiguous external effect that requires explicit review is `ambiguous`. An effect that is still applying under a live worker is not ambiguous.
|
|
255
|
+
2. A live supervised worker or an exact active origin-session workflow turn is `running`.
|
|
256
|
+
3. A durable terminal result keeps its terminal label after its presentation turn ends.
|
|
257
|
+
4. A durable pause is `paused` after its active Pi turn ends.
|
|
258
|
+
5. A pending interaction, decision, or presentation with no exact active turn is `waiting`.
|
|
259
|
+
6. Parked resumable work with no pending interaction is `queued`.
|
|
260
|
+
7. Admitted work that has not started is `queued`.
|
|
261
|
+
|
|
262
|
+
Host connection failure is the client condition `unavailable`, not a `display.status` value. `paused` is never inferred from a parked queue, pending interaction, stale cursor, or missing activity report.
|
|
263
|
+
|
|
264
|
+
### Origin-session activity
|
|
265
|
+
|
|
266
|
+
`agent_start` has no message payload. The extension binds it through the current origin-session view. The latest sent step is open while its interaction remains pending and its run is not paused. Any turn that starts in that state is workflow work. A terminal or follow-up message is open only until its first turn ends. Decisions and notifications never open a turn. The host rejects a start against a closed message.
|
|
267
|
+
|
|
268
|
+
Each report names the sent workflow message, workflow turn ID, run, and origin session. The extension creates the turn ID at start and keeps it through the matching end and host reconnect. If the session view or message receipt is still loading, it buffers start and end and reports them in order when the message becomes available.
|
|
269
|
+
|
|
270
|
+
At `agent_end`, the extension derives `completed`, `aborted`, or `error` from the documented assistant messages. It reads response-entry evidence from `ctx.sessionManager.getBranch()`; the entry ID can be null. The host applies the end, activity update, pause, unproductive-turn counter, and pending step-message cancellation in one transaction. A repeated report adopts that result. A stale turn ID cannot clear newer activity.
|
|
271
|
+
|
|
272
|
+
An aborted turn sets the run pause, cancels pending step messages, and does not increment `unproductiveTurnEnds`. The interaction derives its paused state from the run. Resuming that submitted-output step atomically clears the pause, increments the interaction revision, and creates one step message with reason `resumed`. Pi starts a fresh model turn from that message. A protected decision does not start a model turn, so pause and resume do not change its interaction revision or create another decision message. A completed, recoverably failed, or proved-lost turn increments the counter only when the submitted-output step remains pending, not paused, and has no accepted or validating submission. Values one and two create one step message with reason `reminder`; a value above two fails the attempt. At most one pending reminder-reason step exists. Acceptance, pause, cancellation, timeout, and branch re-presentation cancel pending step messages.
|
|
273
|
+
|
|
274
|
+
The process-local coordinator epoch ends on client disconnect, but an open reported workflow turn does not end. Host startup does not close Pi turns. On `session_start`, `workflowMessage.reportBranch` closes an unended open message as `lost` only when Pi is idle. A busy Pi session re-reports the same started turn. A lost step follows the unproductive-turn rule. A lost terminal or follow-up closes after its first turn. Follow-up activity controls ordering but does not show the completed workflow as `running`. Activity cannot grant workflow authority or settle a workflow request.
|
|
275
|
+
|
|
276
|
+
### Renderers and controls
|
|
277
|
+
|
|
278
|
+
The Pi widget, Pi status line, `/piw`, `Ctrl+Shift+R`, Herdr placement adapter, CLI status output, and every local or remote `piw` screen consume the same live run view. The Pi extension subscribes by origin session and materializes the complete step history before it renders the widget. The Herdr adapter receives the exact run target from that view and owns only pane placement and focus. The TypeScript CLI and Rust TUI subscribe by run ID and use protocol pages and referenced content. Explicit `piw <runId>` mode keeps the requested run selected and does not replace it with the newest run-list item. They do not open live SQLite or compile or validate its DDL digest.
|
|
279
|
+
|
|
280
|
+
Local `piw` may start the host only by executing the installed `pi-workflows host start` command. It does not reimplement host lifecycle. A foreground TypeScript client keeps its cold-start retry timer referenced until the host is ready or the start deadline expires. It uses the package socket on Unix and the same package-derived named pipe as TypeScript on Windows. `piw serve` becomes a loopback WebSocket relay for the same client protocol. It opens one host socket connection for each WebSocket connection and couples their lifecycles one to one. It never multiplexes clients, translates state, or opens the database. A client that cannot start or reach the matching host fails with one clear unavailable or package-version error. It must not fall back to direct SQLite access.
|
|
260
281
|
|
|
261
282
|
## Worker protocol
|
|
262
283
|
|
|
@@ -283,6 +304,24 @@ Every worker message includes the worker launch schema, run ID, generation, work
|
|
|
283
304
|
|
|
284
305
|
The host checks the generation and epoch before it reads the payload. A stale worker gets one claim-loss response and must exit. The host stores receipts for accepted state-changing messages so a retry receives the same answer.
|
|
285
306
|
|
|
307
|
+
## Supervised channel adapters
|
|
308
|
+
|
|
309
|
+
The host launches one transport-only child for each configured Telegram profile. The child receives only the complete operator presentation, allowed Telegram identities, and the one profile credential that it needs. It does not receive the canonical decision subject, open SQLite, load workflow code, or change run state.
|
|
310
|
+
|
|
311
|
+
The private version-1 channel protocol has these message kinds:
|
|
312
|
+
|
|
313
|
+
- `channel.ready`;
|
|
314
|
+
- `channel.present`;
|
|
315
|
+
- `channel.answer`;
|
|
316
|
+
- `channel.settle`; and
|
|
317
|
+
- `channel.exiting`.
|
|
318
|
+
|
|
319
|
+
Each message includes the adapter epoch, profile, sequence, expected channel revision, and stable attempt ID. The host validates these values before it accepts a result or answer. A stale adapter or stale attempt cannot settle newer work.
|
|
320
|
+
|
|
321
|
+
Before the child sends or edits an external message, the host records a managed effect in `effects` and `effect_attempts`. A confirmed result stores its Telegram message references in the effect result. A known rejection can start a bounded new attempt. If the host or adapter stops while an exact attempt is applying, only that in-flight attempt becomes `ambiguous`; the host does not send it again automatically.
|
|
322
|
+
|
|
323
|
+
The operator checks Telegram before resolving an ambiguous attempt. `/workflow-channel recover <message-id> confirm` records that the external work happened. `/workflow-channel recover <message-id> retry` starts a new numbered attempt and warns that a duplicate is possible. `channel_messages` remains the decision feature record for delivery and settlement; it does not duplicate the external-effect state or Telegram references.
|
|
324
|
+
|
|
286
325
|
## Durable protocol records
|
|
287
326
|
|
|
288
327
|
Reuse current rows when they already own a fact:
|
|
@@ -291,18 +330,20 @@ Reuse current rows when they already own a fact:
|
|
|
291
330
|
- `node_attempts` owns node execution state and resolved wall-clock deadlines.
|
|
292
331
|
- `human_decisions` and resolution tables own checkpoints.
|
|
293
332
|
- `effects` and `effect_attempts` own side effects and ambiguous outcomes.
|
|
294
|
-
- `
|
|
333
|
+
- `workflow_messages` owns all content that Pi must add to an origin conversation.
|
|
295
334
|
- `run_bindings` owns origin session and execution mode.
|
|
296
335
|
|
|
336
|
+
`workflow_messages` contains the target session, message kind, source record, content digest, session order, state, confirmed Pi entry ID, and timestamps. Its states are `pending`, `sent`, and `cancelled`; it has no separate sent timestamp. Active-branch evidence changes a matching pending or cancelled message to `sent`. Message kind determines its renderer, turn behavior, and host eligibility rule. A partial unique index allows at most one pending step message for one interactive request. The table stores no sender, send lease, duplicate flag, or message-to-message pointer.
|
|
337
|
+
|
|
297
338
|
Add only these records if implementation proves the current rows cannot hold the contract:
|
|
298
339
|
|
|
299
340
|
### Host commands
|
|
300
341
|
|
|
301
|
-
`host_commands` stores request ID, client ID, operation, idempotency key, request fingerprint, run ID, accepted revision, outcome, receipt or error hash, and timestamps. The
|
|
342
|
+
`host_commands` stores request ID, client ID, operation, idempotency key, durable request fingerprint, run ID, accepted revision, outcome, receipt or error hash, and timestamps. The request ID is transport identity and is not part of the fingerprint. The request primary key prevents one request ID from naming two payloads. The client and idempotency-key uniqueness adopts the same durable payload across transport attempts.
|
|
302
343
|
|
|
303
344
|
### Interactive requests
|
|
304
345
|
|
|
305
|
-
`interactive_requests` stores request ID, run ID, attempt ID, target session ID, kind, contract hash, pending or settled status, accepted submission ID, and timestamps. One attempt has at most one request. The linked node attempt stores its resolved wall-clock deadline.
|
|
346
|
+
`interactive_requests` stores request ID, run ID, attempt ID, target session ID, kind, contract hash, pending or settled status, accepted submission ID, `unproductiveTurnEnds`, and timestamps. Pause is stored once on the run and derived for its interaction. One attempt has at most one request. The linked node attempt stores its resolved wall-clock deadline.
|
|
306
347
|
|
|
307
348
|
`interactive_submissions` stores request ID, submission ID, idempotency key, payload hash, validating, accepted, or rejected outcome, receipt hash, and submission time. Repeated keys return the same receipt.
|
|
308
349
|
|
|
@@ -318,21 +359,25 @@ Agent and assistant-message steps for an interactive run execute in the origin P
|
|
|
318
359
|
|
|
319
360
|
The worker commits the node's resolved wall-clock deadline before it proposes `interaction.requested`. The host commits the request, changes the node attempt to waiting, parks the queue row, releases the claim, and acknowledges the worker. The worker then exits. The host continues to enforce the durable deadline while no worker exists. If the deadline passes, one control claim atomically closes the stale request and schedules a supervised timeout-resume child. The child preserves the same attempt and deadline, records `timed_out`, and follows any `$result.outcome` edge. A run with no timeout recovery edge becomes terminal and releases its session reservation. Restart recovery starts this timeout path before it schedules other work.
|
|
320
361
|
|
|
321
|
-
The extension finds
|
|
362
|
+
The extension finds eligible workflow messages during `session_start`, after model turns settle, and once per second while the session is open. The same poll also establishes the one session subscription when initial host startup or connection fails. It keeps at most one connection attempt and one active subscription, so the open Pi session recovers without a restart and without duplicate coordinators. One `WorkflowMessageCoordinator` handles every message kind. After every host connection, it 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 view names the next eligible pending message only to the active coordinator epoch.
|
|
363
|
+
|
|
364
|
+
The coordinator waits until Pi is idle and has no queued user input, keeps the message ID in its in-memory queued map, and searches the active branch for that hidden ID. It reports a matching entry before any send. Otherwise, it rechecks synchronously 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()` with no `await` between the final check and call. A poll can discover work, but it cannot send an ID already in the queued map.
|
|
365
|
+
|
|
366
|
+
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.
|
|
322
367
|
|
|
323
|
-
|
|
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.
|
|
324
369
|
|
|
325
|
-
The extension
|
|
370
|
+
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.
|
|
326
371
|
|
|
327
372
|
A tool update or submission goes to the host. It includes the exact request, node, attempt, expected revision, and tool-call idempotency key. The host first checks this transport contract and records a provisional `validating` submission. It then schedules a supervised workflow child. Only that child loads workflow code and runs the node's `validate` function. The child reports `interaction.accepted` or `interaction.rejected` to the host. The host settles the request only after acceptance. A rejected payload leaves the same request pending and returns the stored actionable error to the model. If the child stops before it reports a result, the host rejects the provisional submission and leaves the request ready for a corrected retry.
|
|
328
373
|
|
|
329
374
|
An ordinary checkpoint accepts the model-facing `answer` action and starts a continuation run. A protected human decision never accepts that tool action. The extension displays the decision without starting a model turn, and a person answers it with `/workflow answer` through `decision.answer`. When a protected decision reaches its saved `onTimeout` deadline, the host takes a control claim on the waiting parent, atomically records the validated default, closes the pending interaction, releases the parent claim, and reserves the continuation. A human answer cannot win after that deadline.
|
|
330
375
|
|
|
331
|
-
The session keeps normal Pi entries for prompts, tools, and replies. Pi Workflows stores the public session entry ID used for presentation adoption. It does not edit the Pi session file or schema.
|
|
376
|
+
The session keeps normal Pi entries for prompts, tools, and replies. Pi Workflows stores the public session entry ID used for presentation adoption. It does not edit the Pi session file or schema. A normal worker continuation leaves an active session capture open until the matching Pi turn ends. Only proved interruption can fail that capture; worker handoff alone cannot report that the host stopped.
|
|
332
377
|
|
|
333
|
-
One session
|
|
378
|
+
One session sends one workflow message at a time. Messages keep acceptance order, but an earlier ineligible or cancelled message does not block unrelated eligible work. Source state and message kind decide eligibility. A reload clears only process-local queued state. `workflowMessage.reportBranch` adopts existing entries and closes lost turns. When a pending source has no entry on the active branch, it creates one message of that source's own kind: a step with reason `resumed` for an interaction, or a decision for a protected decision. Repeating a report or returning to a branch that already contains that source creates no new message.
|
|
334
379
|
|
|
335
|
-
|
|
380
|
+
A notify node creates a passive `notification` message in the same transaction as its node result. The final leaf of an interactive checkpoint-continuation chain creates one terminal message with its terminal outcome; parent runs settled by continuation do not. Initial, reminder, and resumed prompts are one `step` kind. Protected decisions and follow-ups use the same table and coordinator. Their feature records keep validation, authority, timeout, counters, and result state.
|
|
336
381
|
|
|
337
382
|
## Detached execution
|
|
338
383
|
|
|
@@ -344,7 +389,7 @@ The run binding records `interactive` or `headless` execution mode. Viewers show
|
|
|
344
389
|
|
|
345
390
|
## Pause and cancellation
|
|
346
391
|
|
|
347
|
-
|
|
392
|
+
An explicit pause command atomically commits `paused = 1` on the run, parks the queue, releases the exact claim, cancels pending step messages, and stores the command receipt. The fenced worker process group then stops. When Escape aborts an origin-session turn, the extension sends one `workflowTurn.report` end message with `stopReason: "aborted"`. The host atomically ends that exact activity, sets the run pause, derives the pending interaction as paused, and cancels its pending step messages. The extension does not send a second pause command. A parked interaction has no worker or live run claim. While its run is paused, updates, submissions, and decision answers are rejected. Resume clears the run pause; work for the same pending interaction continues in place, while other paused work takes a new generation and starts another worker from the last durable boundary. An uncommitted pure node can run again after resume.
|
|
348
393
|
|
|
349
394
|
Cancellation against a live worker atomically commits terminal cancellation, cancels pending attempt and interaction state, settles effect recovery state, releases the exact claim, and stores the command receipt. A pending effect becomes cancelled. An applying effect becomes ambiguous because the host cannot prove its external outcome. The host then stops the fenced worker process group. A host crash after the receipt cannot resume the cancelled run or retry the ambiguous effect. If the child does not stop by the deadline, the host kills its process group.
|
|
350
395
|
|
|
@@ -362,10 +407,11 @@ At startup the host:
|
|
|
362
407
|
4. Reads managed effect state before deciding whether work can repeat.
|
|
363
408
|
5. Parks uncertain effects for manual review.
|
|
364
409
|
6. Makes pure and fully settled work claimable.
|
|
365
|
-
7. Restores pending interactive requests and scheduled controller work.
|
|
366
|
-
8. Starts supervised timeout recovery for pending interactive requests
|
|
410
|
+
7. Restores pending interactive requests, workflow messages, external-channel decisions, and scheduled controller work without changing Pi message or turn state.
|
|
411
|
+
8. Starts supervised timeout recovery for pending interactive requests only when their durable deadline expired during a reported model turn from an active connected session. A disconnect suspends the timer, and a new branch report resumes it without losing the prior active time. Message delivery, waiting, paused time, host downtime, and a closed Pi session do not consume the node timeout.
|
|
367
412
|
9. Resumes any remaining provisional `validating` submission in a new supervised child.
|
|
368
|
-
10.
|
|
413
|
+
10. Waits for the extension's active-branch report before it confirms pending entries as sent, closes a turn as `lost`, or creates a branch-specific replacement. The extension sends this report after every host connection.
|
|
414
|
+
11. Starts no model turn until a matching Pi session connects or headless mode is declared.
|
|
369
415
|
|
|
370
416
|
Recovery resumes from the last committed boundary. An uncommitted compute node may run again because compute is pure. An action with a stored effect receipt adopts that receipt. An effect in `ambiguous` state requires explicit recovery.
|
|
371
417
|
|
|
@@ -437,7 +483,8 @@ When the installed state has the old digest, fail before mutation with the stand
|
|
|
437
483
|
- **Session state:** Pi appends normal messages and tool results. Pi Workflows does not edit session files.
|
|
438
484
|
- **Other persistent data:** The workflow SQLite shape changes in place and older alpha state requires reset.
|
|
439
485
|
- **Pi internals:** None.
|
|
440
|
-
- **Public API:** The extension uses documented command registration, tool registration, session lifecycle events, message sending, widgets, status, and session IDs.
|
|
486
|
+
- **Public API:** The extension uses documented command and shortcut registration, tool registration, session lifecycle events, message sending, widgets, status, and session IDs.
|
|
487
|
+
- **Client protocol:** All live clients use `pi-workflows.client.v1`. There is no compatibility transport or direct live-state fallback.
|
|
441
488
|
|
|
442
489
|
## Conformance
|
|
443
490
|
|
|
@@ -447,13 +494,44 @@ The implementation conforms when:
|
|
|
447
494
|
- an expired or replaced owner cannot write;
|
|
448
495
|
- a blocked worker cannot stop host renewal;
|
|
449
496
|
- Pi can restart while work computes or waits;
|
|
497
|
+
- an open Pi session reconnects after initial host startup or connection fails;
|
|
450
498
|
- the host can restart and recover from committed state;
|
|
451
499
|
- run, queue, attempt, decision, lease, event, and viewer projections remain consistent after injected crashes;
|
|
452
500
|
- an expired running row can be resumed or cancelled safely;
|
|
453
501
|
- duplicate commands and submissions return stored receipts;
|
|
454
502
|
- an interactive request appears once in the origin session and survives reload;
|
|
455
|
-
-
|
|
503
|
+
- every workflow-message kind uses one active coordinator epoch, active-branch report, and turn-report contract;
|
|
504
|
+
- initial, reminder, and resumed prompts use one step-message kind and one request counter;
|
|
505
|
+
- a busy Pi session with a pending message does not queue duplicate messages or model turns;
|
|
506
|
+
- two Pi processes cannot send for one origin session because only one coordinator epoch is active;
|
|
507
|
+
- active-branch absence requires no hidden message ID, an idle Pi session, and no pending Pi messages;
|
|
508
|
+
- every host connection reports the active branch before any workflow-message send or turn report;
|
|
509
|
+
- a crash after send leaves the message pending until branch evidence adopts it;
|
|
510
|
+
- active-branch evidence changes a matching pending or cancelled message to sent;
|
|
511
|
+
- host restart alone never closes an active Pi turn as `lost`;
|
|
512
|
+
- a branch change creates at most one source-kind message when that source has no entry on the active branch;
|
|
513
|
+
- a partial unique index allows at most one pending step message for each interactive request;
|
|
514
|
+
- pause is stored once on the run and derived for its interaction;
|
|
515
|
+
- resuming an aborted submitted-output step creates one new resumed message and one fresh origin-session model turn;
|
|
516
|
+
- pausing and resuming a protected decision does not invalidate its answer revision or create a duplicate decision message;
|
|
517
|
+
- follow-ups wait for the final continuation outcome, its terminal turn, earlier follow-ups, and release of the origin-session reservation;
|
|
518
|
+
- only the final leaf in a checkpoint continuation chain receives a terminal workflow message;
|
|
456
519
|
- effects are deduplicated or marked ambiguous;
|
|
520
|
+
- external channel delivery and settlement use the same managed-effect and attempt records;
|
|
521
|
+
- a channel child is transport-only and cannot open SQLite, load workflow code, or change run state;
|
|
522
|
+
- a stale adapter epoch or attempt cannot settle newer channel work;
|
|
523
|
+
- an interrupted exact in-flight channel attempt becomes ambiguous and is never retried without explicit recovery;
|
|
524
|
+
- explicit confirmation records observed success, while explicit retry creates a new attempt and warns about possible duplication;
|
|
457
525
|
- the extension and host run no workflow or controller code in their own event loops;
|
|
458
526
|
- the production package contains no embedded execution fallback;
|
|
527
|
+
- the host is the only production process that opens live SQLite state;
|
|
528
|
+
- the widget, status line, Herdr actions, CLI, and `piw` render the same host-produced status and controls;
|
|
529
|
+
- running and waiting projections identify the current workflow node from the same unfinished attempt, while checkpoint waits use the completed checkpoint node;
|
|
530
|
+
- an exact origin-session model turn presents that same waiting node as running without changing its durable identity;
|
|
531
|
+
- a busy origin session displays `running` for the full exact workflow turn, including a terminal or pausing turn, and a stale turn-end report cannot clear newer activity;
|
|
532
|
+
- normal worker continuation does not fail an active Pi session capture or report a false host interruption;
|
|
533
|
+
- `paused` appears only after a durable pause and matching turn end;
|
|
534
|
+
- a terminal run remains in the origin-session view while its terminal message is pending or its first turn is open, and then for 60 seconds after that turn ends, without retaining execution authority;
|
|
535
|
+
- a TypeScript-created live database is viewable by the matching Rust `piw` through the client protocol without a duplicated SQLite digest;
|
|
536
|
+
- no removed host, replay, or direct SQLite client path remains selectable;
|
|
459
537
|
- real Pi end-to-end tests, repository checks, reviewer checks, and CI pass.
|