@osolmaz/pi-workflows 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +6 -6
  2. package/dist/client/activity.d.ts +2 -0
  3. package/dist/client/activity.js +6 -0
  4. package/dist/client/activity.js.map +1 -0
  5. package/dist/client/client.d.ts +101 -0
  6. package/dist/client/client.js +733 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +3 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/materialize.d.ts +7 -0
  12. package/dist/client/materialize.js +177 -0
  13. package/dist/client/materialize.js.map +1 -0
  14. package/dist/client/protocol.d.ts +60 -0
  15. package/dist/client/protocol.js +269 -0
  16. package/dist/client/protocol.js.map +1 -0
  17. package/dist/client/resolver.d.ts +23 -0
  18. package/dist/client/resolver.js +2 -0
  19. package/dist/client/resolver.js.map +1 -0
  20. package/dist/client/view.d.ts +118 -0
  21. package/dist/client/view.js +3 -0
  22. package/dist/client/view.js.map +1 -0
  23. package/dist/controllers/sqlite.d.ts +43 -0
  24. package/dist/controllers/sqlite.js +137 -2
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +278 -183
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-view.d.ts +7 -2
  30. package/dist/extension/session-view.js +60 -52
  31. package/dist/extension/session-view.js.map +1 -1
  32. package/dist/extension/widget.d.ts +2 -1
  33. package/dist/extension/widget.js +14 -7
  34. package/dist/extension/widget.js.map +1 -1
  35. package/dist/host/child-worker-supervisor.js +1 -1
  36. package/dist/host/child-worker-supervisor.js.map +1 -1
  37. package/dist/host/resolver-entry.d.ts +2 -23
  38. package/dist/host/resolver-entry.js +1 -1
  39. package/dist/host/resolver-entry.js.map +1 -1
  40. package/dist/host/runner.d.ts +11 -0
  41. package/dist/host/runner.js +473 -17
  42. package/dist/host/runner.js.map +1 -1
  43. package/dist/host/state.d.ts +6 -3
  44. package/dist/host/state.js +52 -26
  45. package/dist/host/state.js.map +1 -1
  46. package/dist/host/view.d.ts +73 -0
  47. package/dist/host/view.js +871 -0
  48. package/dist/host/view.js.map +1 -0
  49. package/dist/host/worker-protocol.js +1 -1
  50. package/dist/host/worker-protocol.js.map +1 -1
  51. package/dist/state/database.d.ts +1 -0
  52. package/dist/state/database.js +15 -0
  53. package/dist/state/database.js.map +1 -1
  54. package/dist/state/prune.d.ts +3 -1
  55. package/dist/state/prune.js +6 -8
  56. package/dist/state/prune.js.map +1 -1
  57. package/dist/state/schema.js +12 -1
  58. package/dist/state/schema.js.map +1 -1
  59. package/dist/viewer/backup.d.ts +2 -0
  60. package/dist/viewer/backup.js +28 -0
  61. package/dist/viewer/backup.js.map +1 -0
  62. package/dist/viewer/cli.d.ts +4 -0
  63. package/dist/viewer/cli.js +150 -170
  64. package/dist/viewer/cli.js.map +1 -1
  65. package/dist/viewer/tui.d.ts +5 -7
  66. package/dist/viewer/tui.js +188 -108
  67. package/dist/viewer/tui.js.map +1 -1
  68. package/dist/workflows/store.d.ts +62 -1
  69. package/dist/workflows/store.js +350 -44
  70. package/dist/workflows/store.js.map +1 -1
  71. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  72. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  73. package/docs/SQLITE_STATE.md +13 -11
  74. package/docs/WORKFLOW_HOST.md +74 -61
  75. package/docs/development.md +2 -1
  76. package/docs/live-replay-protocol.md +70 -132
  77. package/docs/tui-viewer.md +10 -14
  78. package/docs/workflows.md +44 -1
  79. package/herdr-plugin.toml +1 -1
  80. package/package.json +9 -3
  81. package/protocol/client.v1.schema.json +137 -0
  82. package/protocol/fixtures/client-v1.json +23 -0
  83. package/src/client/activity.ts +6 -0
  84. package/src/client/client.ts +935 -0
  85. package/src/client/index.ts +24 -0
  86. package/src/client/materialize.ts +228 -0
  87. package/src/client/protocol.ts +327 -0
  88. package/src/client/resolver.ts +26 -0
  89. package/src/client/view.ts +138 -0
  90. package/src/controllers/sqlite.ts +213 -2
  91. package/src/extension/index.ts +349 -208
  92. package/src/extension/session-view.ts +73 -50
  93. package/src/extension/widget.ts +16 -8
  94. package/src/host/child-worker-supervisor.ts +1 -1
  95. package/src/host/resolver-entry.ts +11 -26
  96. package/src/host/runner.ts +648 -48
  97. package/src/host/state.ts +71 -43
  98. package/src/host/view.ts +1084 -0
  99. package/src/host/worker-protocol.ts +1 -1
  100. package/src/state/database.ts +13 -0
  101. package/src/state/prune.ts +11 -11
  102. package/src/state/schema.ts +12 -1
  103. package/src/viewer/backup.ts +29 -0
  104. package/src/viewer/cli.ts +171 -185
  105. package/src/viewer/tui.ts +196 -124
  106. package/src/workflows/store.ts +500 -45
  107. package/dist/host/client.d.ts +0 -48
  108. package/dist/host/client.js +0 -216
  109. package/dist/host/client.js.map +0 -1
  110. package/dist/host/protocol.d.ts +0 -38
  111. package/dist/host/protocol.js +0 -156
  112. package/dist/host/protocol.js.map +0 -1
  113. package/dist/viewer/watch.d.ts +0 -6
  114. package/dist/viewer/watch.js +0 -46
  115. package/dist/viewer/watch.js.map +0 -1
  116. package/src/host/client.ts +0 -293
  117. package/src/host/protocol.ts +0 -196
  118. package/src/viewer/watch.ts +0 -51
@@ -6,13 +6,13 @@ Pi Workflows stores all live durable state in one database:
6
6
  ~/.pi/agent/workflows/state.sqlite
7
7
  ```
8
8
 
9
- There is one database for the user installation. Project and run IDs separate data inside it. Workflow targets do not read or write this database.
9
+ 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
10
 
11
11
  ## Viewer projection
12
12
 
13
- The database includes the [incremental and virtualized viewer design](plans/2026-08-28-piw-incremental-viewer-plan.md).
13
+ 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
14
 
15
- `viewer_runs` stores one presentation revision and retained revision floor for each run. `viewer_deltas` stores ordered target patches by run, presentation revision, and delta index. `viewer_session_checkpoints` stores the bounded active message and tool state at each 256-event boundary. A viewer-visible transaction writes the domain change, advances the presentation revision, and writes its patch blobs before the same commit. Session-event transactions write each reached replay checkpoint in that transaction.
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. `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
16
 
17
17
  The store retains 256 presentation revisions. A reader with an older cursor must take a bounded snapshot. Patches use `add`, `replace`, `remove`, and `append`. They target small projection documents or pages. Patch creation does not reconstruct and compare complete run views.
18
18
 
@@ -60,9 +60,9 @@ The database also uses:
60
60
  - directory mode `0700`
61
61
  - database and backup mode `0600`
62
62
 
63
- Read-only tools open the same file with SQLite read-only mode and `PRAGMA query_only = ON`.
63
+ 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
64
 
65
- Opening code verifies the application ID, user version, schema metadata, compiled DDL digest, and exact SQLite schema shape. An incompatible database fails with an instruction to clear the incompatible alpha state. Pi Workflows does not import, reinterpret, or delete that state.
65
+ 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
66
 
67
67
  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
68
 
@@ -111,7 +111,7 @@ The shared records do not replace domain schemas. The following `STRICT` tables
111
111
  | Area | Tables |
112
112
  | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
113
113
  | Schema and projects | `schema_meta`, `projects` |
114
- | Content | `blobs` |
114
+ | Content | `blobs`, `run_view_content` |
115
115
  | Shared lifecycle | `resources`, `leases`, `events`, `workflow_host_state` |
116
116
  | Host protocol | `host_commands`, `run_workers`, `worker_messages`, `interactive_requests`, `interactive_submissions` |
117
117
  | Workflows | `workflow_definitions`, `runs`, `run_sources`, `run_steps`, `run_bindings`, `run_queue`, `node_attempts`, `workflow_updates` |
@@ -146,7 +146,7 @@ without inserting another visible message.
146
146
 
147
147
  ## Content-addressed values
148
148
 
149
- `blobs` stores canonical JSON and UTF-8 text as bytes. Its primary key is the 32-byte SHA-256 digest of the bytes.
149
+ `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
150
 
151
151
  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
152
 
@@ -199,7 +199,7 @@ Reading or finding a row never gives write authority.
199
199
  - Control commands have narrow explicit operations, such as requesting cancellation or deletion.
200
200
  - Model-originated workflow answers cannot resolve protected human decisions.
201
201
 
202
- The global host is the normal state writer. Its protected stores check ownership and renew the exact live claim in the same transaction as the write. A stale or expired owner cannot renew itself. Pi extensions and mutating CLI commands use the local host protocol. Shared scans, status commands, lists, viewers, and the Rust `piw` program are read-only.
202
+ 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
203
 
204
204
  ## Competing outcomes
205
205
 
@@ -215,7 +215,7 @@ The same rule applies to run terminal outcomes, continuation admission, queue se
215
215
 
216
216
  ## Read contract
217
217
 
218
- Status is a pure projection of domain rows, immutable facts, current leases, and effect results.
218
+ Durable status is a pure projection of domain rows, immutable facts, current leases, and effect results. The host combines that projection with validated ephemeral origin-session activity to produce one live run view. Ephemeral activity can change display status only. It cannot change durable workflow state or authority. Every renderer consumes the host-produced display status and allowed controls without running another status reducer.
219
219
 
220
220
  A settings scope uses its resource revision as its public change number. Each accepted patch, current value, and node binding is saved in one transaction. A checkpoint continuation keeps the same settings resources and transfers them to the continuation run.
221
221
 
@@ -233,7 +233,7 @@ Read paths do not repair state. Owner reconcilers apply pending effects and writ
233
233
 
234
234
  All projects use the same file. `projects` stores a stable ID and canonical path. Project-scoped controller and run queries use that key. One global host owns the file for the user installation. Its socket, lock, and exact child-process registry are under `~/.pi/agent/workflows/host/`. A second live host is rejected even when it was started from another project.
235
235
 
236
- SQLite WAL permits concurrent readers while one writer commits. Writers are serialized by SQLite and must keep transactions short. Hashing, model calls, shell work, and external requests happen outside write transactions.
236
+ 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
237
 
238
238
  This contract is for local storage on one machine. It does not claim distributed consensus or network-filesystem safety.
239
239
 
@@ -260,6 +260,8 @@ pi-workflows state prune --before 2026-08-01T00:00:00Z --dry-run
260
260
  pi-workflows state prune --before 2026-08-01T00:00:00Z --backup /absolute/path/to/before-prune.sqlite --apply
261
261
  ```
262
262
 
263
+ 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.
264
+
263
265
  `status` reports only safe counts, file size, active leases, and unsettled effects.
264
266
  It does not print actor IDs, channel references, payloads, or credentials.
265
267
 
@@ -267,6 +269,6 @@ It does not print actor IDs, channel references, payloads, or credentials.
267
269
 
268
270
  ## Alpha cutover
269
271
 
270
- This is a hard cut. Pi Workflows has no normal reader or writer for older live storage. It does not use dual reads, dual writes, aliases, versioned state roots, or automatic import.
272
+ 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
273
 
272
274
  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.”
@@ -1,6 +1,6 @@
1
1
  # Workflow host
2
2
 
3
- Status: implemented. [Run workflows outside Pi](2026-08-30-out-of-process-workflow-host-plan.md) records the approved redesign. [Restore workflow session delivery and controls](2026-09-01-restore-session-delivery-controls-plan.md) records the delivery, widget, and Escape-control repair.
3
+ Status: implemented. The out-of-process host, unified live workflow client, session delivery, widget, Herdr controls, and client-only `piw` viewer are one production path. [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 approved redesigns.
4
4
 
5
5
  ## Purpose
6
6
 
@@ -22,6 +22,8 @@ The host solves two different failures:
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
24
  - **Managed effect:** A side effect reserved and settled through an idempotent durable record.
25
+ - **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.
26
+ - **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
27
 
26
28
  ## Boundaries
27
29
 
@@ -37,17 +39,20 @@ One host owns the global workflow database for one user installation.
37
39
 
38
40
  ```text
39
41
  Pi extension ─┐
40
- Pi extension ─┼── local socket ── workflow host ── SQLite
41
- CLI client ───┘
42
- ├── run worker A
43
- ├── run worker B ── headless pi --mode rpc
44
- ├── controller worker
45
- └── source resolver
42
+ CLI client ───┼── WorkflowClient v1 ── local socket ── workflow host ── SQLite
43
+ piw ──────────┘
44
+ ├── run worker A
45
+ Remote piw ── SSH tunnel ── loopback WebSocket relay ────────┤
46
+ ├── run worker B ── headless pi --mode rpc
47
+ ├── controller worker
48
+ └── source resolver
46
49
  ```
47
50
 
51
+ 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. The worker and source-resolver channels are private supervision protocols, not alternate client interfaces.
52
+
48
53
  The host may manage runs from more than one project. Each run keeps its canonical project path and source identity.
49
54
 
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.
55
+ 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
56
 
52
57
  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
58
 
@@ -185,17 +190,20 @@ The child protocol must apply backpressure. A child that exceeds message or outp
185
190
 
186
191
  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
192
 
188
- ## Local client protocol
193
+ ## Live client protocol
194
+
195
+ 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
196
 
190
- Clients connect through a user-only local socket. Unix socket mode is `0600`. Other platforms use their equivalent local transport and access control.
197
+ 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
198
 
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.
199
+ 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
200
 
194
- Every request uses this envelope:
201
+ Each message uses one envelope:
195
202
 
196
203
  ```json
197
204
  {
198
- "schema": "pi-workflows.host-request.v1",
205
+ "schema": "pi-workflows.client.v1",
206
+ "type": "request",
199
207
  "requestId": "opaque-id",
200
208
  "clientId": "opaque-id",
201
209
  "operation": "run.cancel",
@@ -206,57 +214,56 @@ Every request uses this envelope:
206
214
  }
207
215
  ```
208
216
 
209
- A response uses:
217
+ 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 delivery change, or availability change. Valid command outcomes remain `accepted`, `adopted`, `rejected`, `conflict`, `notFound`, `claimLost`, and `unavailable`.
210
218
 
211
- ```json
212
- {
213
- "schema": "pi-workflows.host-response.v1",
214
- "requestId": "opaque-id",
215
- "outcome": "accepted",
216
- "revision": 13,
217
- "receipt": {}
218
- }
219
- ```
219
+ 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
220
 
221
- Valid outcomes are:
221
+ 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
222
 
223
- - `accepted`
224
- - `adopted`
225
- - `rejected`
226
- - `conflict`
227
- - `notFound`
228
- - `claimLost`
229
- - `unavailable`
230
-
231
- The host commits a command receipt before it acknowledges success. Repeating the same request ID and payload returns the stored receipt. Reusing an ID with another payload returns a conflict.
232
-
233
- The first command set is:
234
-
235
- - `run.start`
236
- - `run.pause`
237
- - `run.resume`
238
- - `run.cancel`
239
- - `run.status`
240
- - `run.list`
241
- - `checkpoint.answer`
242
- - `decision.answer`
243
- - `interaction.submit`
244
- - `interaction.update`
245
- - `notification.claim`
246
- - `notification.deliver`
247
- - `turn.claim`
248
- - `turn.resolve`
249
- - `controller.list`
250
- - `controller.get`
251
- - `controller.apply`
252
- - `controller.reconcile`
253
- - `controller.delete`
254
- - `host.status`
255
- - `host.stop`
223
+ The protocol owns four operation groups:
224
+
225
+ - run and controller commands, including start, pause, resume, cancel, answers, updates, submissions, reconciliation, and host control;
226
+ - live views, including lightweight run lists, one run snapshot, revision subscriptions, byte-bounded pages, chunked referenced content, and one consistent origin-session response with its active run view, ordered pending delivery records, and read-only delivery availability;
227
+ - origin-session activity reports for the exact workflow delivery being processed by Pi;
228
+ - state maintenance, including status, verification, backup, and prune 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.
256
229
 
257
230
  `notification.claim` and `turn.claim` can create a claim or revalidate the exact retained claim before delivery. Revalidation checks the in-memory client claim and its durable lease without creating another claim.
258
231
 
259
- Read operations may use the existing read-only store directly in viewers. Mutating Pi and CLI paths use the host.
232
+ ### Live run view
233
+
234
+ 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.
235
+
236
+ 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.
237
+
238
+ The origin-session response contains this active run view or no active run plus an ordered byte-bounded window of pending delivery records, their complete count, and read-only notification and turn availability for that session. It does not retain an older terminal run after the session reservation ends. The records include the request, delivery, contract, revision, presentation entry, and claim facts required by the shared delivery coordinator. The host returns them from one consistent read. The extension materializes the complete active run revision and hydrates the definition and delivery contracts before it updates the widget or delivery coordinator. It issues a claim command only when the matching availability fact is true. Polling an idle session creates no durable command.
239
+
240
+ 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.
241
+
242
+ The closed `display.status` set is `queued`, `running`, `waiting`, `paused`, `completed`, `failed`, `timed_out`, `cancelled`, and `ambiguous`.
243
+
244
+ The host computes effective status in this order:
245
+
246
+ 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.
247
+ 2. Another durable terminal result keeps its terminal label.
248
+ 3. A durable pause is `paused`.
249
+ 4. A live supervised worker or an exact active origin-session workflow turn is `running`.
250
+ 5. A pending interaction, decision, or presentation with no exact active turn is `waiting`.
251
+ 6. Parked resumable work with no pending interaction is `queued`.
252
+ 7. Admitted work that has not started is `queued`.
253
+
254
+ 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.
255
+
256
+ ### Origin-session activity
257
+
258
+ The Pi extension reports `started`, `settled`, and lease refreshes only for the exact workflow delivery that it can identify through documented Pi lifecycle events and its in-memory delivery map. The host requires the deterministic `interaction:<request-id>` delivery identity and keys activity by connection and request, not by an unchecked caller label. The first activity report on each client connection is `started`; only later reports on that connection are refreshes. Each report includes the origin session, run, request, delivery, client connection, and increasing activity sequence. The host validates these facts against the durable pending interactive request and its recorded delivery or presentation entry. Settlement of the presentation claim does not end activity while the model turn continues.
259
+
260
+ Activity is ephemeral display state. It cannot grant workflow authority, settle a request, change a durable run state, or survive as a claim. A repeated report is idempotent. A stale sequence, replaced delivery, or wrong session is rejected. One shared client constants module defines the refresh period and lease duration. The refresh period is shorter than the lease duration, and the lease duration bounds stale `running` display after client loss. The host clears activity on the matching settled event, client disconnect, or lease expiry. Missing activity falls back to the durable `waiting` view and never creates a false `paused` or `running` state.
261
+
262
+ ### Renderers and controls
263
+
264
+ 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.
265
+
266
+ 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
267
 
261
268
  ## Worker protocol
262
269
 
@@ -298,7 +305,7 @@ Add only these records if implementation proves the current rows cannot hold the
298
305
 
299
306
  ### Host commands
300
307
 
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 unique request fingerprint prevents one request ID from naming two commands.
308
+ `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
309
 
303
310
  ### Interactive requests
304
311
 
@@ -322,7 +329,7 @@ The extension finds pending requests during `session_start`, after `agent_settle
322
329
 
323
330
  The host grants one live presentation claim. The current presenter cannot claim the same request again before that claim expires. A poll that sees any live presentation claim treats it as unavailable, not as a tool failure. When the matching custom message appears in the active Pi branch, the coordinator records its public session entry ID through the host and clears the local queued state. If Pi becomes idle without exposing a matching entry after the confirmation interval, the coordinator reports the delivery as ambiguous and keeps it blocked. A failed durable receipt also keeps the visible message blocked. Neither case can send the message again. The normal `workflow` tool contract then submits updates and results.
324
331
 
325
- The extension projects the active origin-session run into Pi's widget and status APIs by reading host-owned durable state. This projection never runs workflow code and never writes run state. `Shift+Up` and `Shift+Down` scroll the widget.
332
+ 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
333
 
327
334
  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
335
 
@@ -437,7 +444,8 @@ When the installed state has the old digest, fail before mutation with the stand
437
444
  - **Session state:** Pi appends normal messages and tool results. Pi Workflows does not edit session files.
438
445
  - **Other persistent data:** The workflow SQLite shape changes in place and older alpha state requires reset.
439
446
  - **Pi internals:** None.
440
- - **Public API:** The extension uses documented command registration, tool registration, session lifecycle events, message sending, widgets, status, and session IDs.
447
+ - **Public API:** The extension uses documented command and shortcut registration, tool registration, session lifecycle events, message sending, widgets, status, and session IDs.
448
+ - **Client protocol:** All live clients use `pi-workflows.client.v1`. There is no compatibility transport or direct live-state fallback.
441
449
 
442
450
  ## Conformance
443
451
 
@@ -456,4 +464,9 @@ The implementation conforms when:
456
464
  - effects are deduplicated or marked ambiguous;
457
465
  - the extension and host run no workflow or controller code in their own event loops;
458
466
  - the production package contains no embedded execution fallback;
467
+ - the host is the only production process that opens live SQLite state;
468
+ - the widget, status line, Herdr actions, CLI, and `piw` render the same host-produced status and controls;
469
+ - a busy origin session displays `running` only while its exact workflow turn is active, and `paused` appears only after a durable pause;
470
+ - a TypeScript-created live database is viewable by the matching Rust `piw` through the client protocol without a duplicated SQLite digest;
471
+ - no removed host, replay, or direct SQLite client path remains selectable;
459
472
  - real Pi end-to-end tests, repository checks, reviewer checks, and CI pass.
@@ -159,7 +159,8 @@ workflow stores a long-lived registry token:
159
159
  For later versions:
160
160
 
161
161
  1. Update `version` in `package.json`, `package-lock.json`, `tui/Cargo.toml`,
162
- and `tui/Cargo.lock`, then merge that change into the default branch.
162
+ `tui/Cargo.lock`, and `herdr-plugin.toml`, then merge that change into the
163
+ default branch.
163
164
  2. Publish a GitHub Release whose tag is `v<version>`, such as `v0.2.0`.
164
165
  3. Wait for the **Publish npm package** and **Publish crates.io package**
165
166
  workflows to finish.
@@ -1,175 +1,113 @@
1
- # Live replay protocol
1
+ # Live client protocol
2
2
 
3
- The Rust viewer (`tui/`) can read SQLite directly or connect to `piw serve`. Both modes use the same bounded viewer projection. The protocol ID is `pi-workflows.replay.v1`.
3
+ Pi Workflows uses one live client protocol for the Pi extension, the TypeScript CLI, local `piw`, and remote `piw` through the loopback relay.
4
4
 
5
- The server reads SQLite and never writes it. It accepts loopback addresses only. Remote use goes through an SSH tunnel. The server rejects WebSocket handshakes with an `Origin` header so a web page cannot read workflow state from localhost.
5
+ The protocol ID is `pi-workflows.client.v1`. Its schema is [`protocol/client.v1.schema.json`](../protocol/client.v1.schema.json). TypeScript and Rust use the same valid and invalid fixture corpus.
6
6
 
7
- ## Framing
7
+ The host is the only process that reads or writes the active SQLite database. A client protocol or package-version mismatch does not mean that SQLite state is incompatible. The client stops and asks for matching `pi-workflows` and `piw` packages.
8
8
 
9
- The endpoint is `/ws`. Each message is one JSON object with a `type` field. Unknown message types and fields are ignored. The server sends `hello` first. A client disconnects when it does not support the protocol ID.
9
+ ## Transports
10
10
 
11
- ```json
12
- { "type": "hello", "protocol": "pi-workflows.replay.v1" }
11
+ The local transport is a user-only Unix socket on Unix systems:
12
+
13
+ ```text
14
+ ~/.pi/agent/workflows/host/host.sock
13
15
  ```
14
16
 
15
- ## Bounded run view
17
+ On Windows, the same client uses the package-derived `\\.\pipe\pi-workflows-<state-directory-hash>` named pipe. Local `piw`, `piw serve`, and the TypeScript client derive the same endpoint from the workflow state directory. A foreground TypeScript cold start keeps its retry wait referenced until the detached host becomes ready or the start deadline expires. Only background reconnect timers are unreferenced.
18
+
19
+ Each message is one canonical JSON object followed by a newline. TypeScript and Rust use the same ECMAScript number formatting and UTF-16 object-key order, including for arbitrary workflow JSON. A message can be at most 1 MiB. Unknown envelope fields, non-canonical JSON, and invalid framing close only the offending connection. If socket backpressure delays a client write, connection close, socket error, or request cancellation ends the wait instead of leaving the request pending.
20
+
21
+ `piw serve` provides the remote transport at `/ws`. It binds to loopback only. Each WebSocket connection has one matching host-socket connection, and their lifecycles are coupled. The relay forwards one canonical JSON object per text frame. It does not read SQLite, translate views, multiplex clients, or retain state.
22
+
23
+ Remote clients use an SSH tunnel to reach the loopback relay.
16
24
 
17
- A snapshot contains one bounded run view:
25
+ ## Envelope
26
+
27
+ The protocol has four message types:
28
+
29
+ - `hello` identifies the protocol connection and package version.
30
+ - `request` carries one operation, request ID, client ID, idempotency key, optional run ID and revision, and payload.
31
+ - `response` settles one request with an outcome, optional revision, receipt, or safe error.
32
+ - `event` carries a revisioned run list, run snapshot, run patch, run page, session snapshot, or unavailable condition.
33
+
34
+ The host sends `hello` first:
18
35
 
19
36
  ```json
20
37
  {
21
- "presentationRevision": 42,
22
- "graphRevision": 17,
23
- "manifest": { … },
24
- "workflow": { … },
25
- "graphScene": {
26
- "ranks": [ … ],
27
- "edges": [ … ],
28
- "segments": [ … ],
29
- "rankOfNode": { … }
30
- },
31
- "graphSteps": [ … ],
32
- "takenTransitions": [ "prepare->run" ],
33
- "stepStart": 768,
34
- "stepTotal": 1000,
35
- "state": {
36
- "steps": [ … ]
37
- },
38
- "tracePage": {
39
- "presentationRevision": 42,
40
- "start": 768,
41
- "total": 1000,
42
- "items": [ … ]
43
- },
44
- "session": {
45
- "presentationRevision": 42,
46
- "binding": { … },
47
- "entryPage": {
48
- "presentationRevision": 42,
49
- "start": 768,
50
- "total": 1000,
51
- "items": [ … ]
52
- },
53
- "eventPage": {
54
- "presentationRevision": 42,
55
- "start": 768,
56
- "total": 1000,
57
- "items": [ … ]
58
- },
59
- "capture": { … }
60
- },
61
- "settingsScopes": [ … ],
62
- "followUpQueue": { … },
63
- "live": true,
64
- "possiblyInterrupted": false
38
+ "connectionId": "connection-1",
39
+ "packageVersion": "0.15.3",
40
+ "schema": "pi-workflows.client.v1",
41
+ "type": "hello"
65
42
  }
66
43
  ```
67
44
 
68
- Each step, trace, session-entry, and session-event page contains at most 256 rows. `graphSteps` contains at most one latest attempt per node at the replay cursor. `takenTransitions` contains distinct transitions up to that cursor. `graphScene` is the retained language-neutral rank and route plan shared by Rust and TypeScript.
69
-
70
- A snapshot does not contain complete trace or session history. A replay jump fetches the page that contains the requested zero-based cursor.
71
-
72
- ## Revisions and target patches
73
-
74
- Each viewer-visible SQLite transaction advances the run presentation revision and commits ordered target patches with the same transaction. The server reads those patches. It does not build complete old and new run views to compare them.
45
+ A request uses a stable request ID and idempotency key:
75
46
 
76
47
  ```json
77
48
  {
78
- "type": "run_patch",
79
- "runId": "run-1",
80
- "revision": 43,
81
- "targets": [
82
- {
83
- "targetType": "conversation",
84
- "targetKey": "entries:tail",
85
- "patch": [
86
- { "op": "replace", "path": "/presentationRevision", "value": 43 },
87
- { "op": "remove", "path": "/items/0" },
88
- { "op": "append", "path": "/items", "value": [ { "seq": 1001, … } ] },
89
- { "op": "replace", "path": "/start", "value": 745 },
90
- { "op": "replace", "path": "/total", "value": 1001 }
91
- ]
92
- }
93
- ]
49
+ "clientId": "client-1",
50
+ "idempotencyKey": "status-1",
51
+ "operation": "host.status",
52
+ "payload": {},
53
+ "requestId": "request-1",
54
+ "schema": "pi-workflows.client.v1",
55
+ "type": "request"
94
56
  }
95
57
  ```
96
58
 
97
- The patch set supports `add`, `replace`, `remove`, and `append`. `append` adds each value to the target array in order. Sliding tail pages keep 256 rows by removing old leading rows when necessary. Session-event pages stay aligned to 256-event checkpoint boundaries. They append inside one block and request the next page when a write crosses a boundary.
59
+ The closed outcomes are `accepted`, `adopted`, `rejected`, `conflict`, `notFound`, `claimLost`, and `unavailable`.
98
60
 
99
- A patch targets one bounded document or page. A client applies a tail patch only when it holds that tail page. Older loaded pages stay valid because committed history is immutable. A target that needs a fresh bounded projection causes a snapshot. This still avoids complete-run reads and complete-run JSON comparison.
61
+ ## Run and session views
100
62
 
101
- Revisions must arrive in order. Duplicate state is harmless because a client ignores an old revision. A wrong run, malformed patch, missing path, stale page, future revision, or gap cannot replace the last good view. A gap or a cursor older than retained patches causes a bounded snapshot.
63
+ The host produces `pi-workflows.run-view.v1` from one consistent database read. The view contains the bounded workflow state, graph, trace, session projection, page cursors, presentation revision, and one `display` object. A terminal display includes the stored failure reason, not only its machine error code. A reason above the shared 16 KiB inline-content threshold uses a small `reason` notice and a digest-bound `reasonContent` reference. This keeps the run list below the 1 MiB frame limit and keeps the complete diagnostic available.
102
64
 
103
- The database retains 256 presentation revisions per run. The server does not replay an unbounded patch backlog.
65
+ The closed display statuses are:
104
66
 
105
- ## Pages
67
+ - `queued`
68
+ - `running`
69
+ - `waiting`
70
+ - `paused`
71
+ - `completed`
72
+ - `failed`
73
+ - `timed_out`
74
+ - `cancelled`
75
+ - `ambiguous`
106
76
 
107
- A client asks for a page with `fetch_page`:
77
+ Only the host computes this status. A parked queue is not a pause. `paused` requires the durable pause flag. An exact live worker or origin-session turn is `running`. An effect being applied by that live worker is still `running`; only a durable `ambiguous` effect that needs operator action is `ambiguous`. `unavailable` is a client connection condition, not a run status.
108
78
 
109
- ```json
110
- {
111
- "type": "fetch_page",
112
- "runId": "run-1",
113
- "kind": "session_events",
114
- "cursor": 20000
115
- }
116
- ```
79
+ The run list uses the same display object. The host sends it as revision-bound `pi-workflows.run-list-page.v1` pages. Each page reads only lightweight run status and source facts. It does not load input, launch options, steps, trace, or session history. TypeScript and Rust clients collect all pages for one revision before they replace the visible list. If the revision changes, they discard the partial list and start from the next subscription event.
117
80
 
118
- `kind` is one of `steps`, `trace`, `trace_at_step`, `session_entries`, `session_events`, `settings`, `follow_ups`, or `updates`. `trace_at_step` uses a step index as its cursor and returns the trace page around that step's timestamp. The server answers with `run_page`:
81
+ An origin-session subscription returns `pi-workflows.session-view.v1`, which contains the current active run view, an ordered byte-bounded window of pending interaction records, their complete count, and read-only notification and turn availability in one read. A session with no active reservation returns no run, even when that session has older terminal runs. The Pi extension assembles the active run's complete step history and hydrates its definition and referenced values before it updates the widget or delivery coordinator. It sends a durable claim command only when the matching availability fact is true. After a turn claim, it reads the claimed run by its exact run ID. It does not use the latest run in the session. An idle session does not create empty claim or status commands.
119
82
 
120
- ```json
121
- {
122
- "type": "run_page",
123
- "runId": "run-1",
124
- "revision": 43,
125
- "kind": "session_events",
126
- "cursor": 20000,
127
- "start": 19872,
128
- "total": 48620,
129
- "items": [ … ]
130
- }
131
- ```
83
+ A snapshot page contains at most 256 items and also has a byte budget. `view.page` returns another byte-bounded window that contains the requested cursor. Page responses use `pi-workflows.run-page.v1` and echo the requested cursor and run-view revision. A client applies a page only when both still match its current request and snapshot. A step-centered trace request selects the exact stored attempt first and uses its node only when that attempt has no trace event. Large workflow topology has bounded node, edge, graph-step, and transition projections plus durable content references for the complete original definition and complete graph history. TypeScript clients assemble every run-history page for that revision and hydrate the complete definition and all referenced content before they emit a complete non-interactive view or update the Pi widget. TypeScript and Rust use the same verified content loader for complete graph steps and transitions. Rust also requests, verifies, and decodes the complete referenced definition before it builds the graph layout. A session-event page also carries the replay checkpoint for the exact sequence before its first item, so reducing the page does not lose earlier active messages or tool calls. The checkpoint can itself be a durable content reference. TypeScript hydrates it with the run view, and Rust requests and resolves it before replay.
132
84
 
133
- Page reads use bounded ranges. The response echoes the requested `cursor` and carries the presentation revision read in the same SQLite snapshot as its rows. A client ignores an older response when a newer cursor is pending. A step page also returns `graphCursor`, `graphSteps`, and `takenTransitions` for that exact replay point, even when the selected step was already in the prior page. A historical session-event page can return `replayCheckpoint`. The checkpoint contains only active message and tool state at the page boundary. The writer stores it at each 256-event boundary, so a page jump reads one checkpoint blob instead of predecessor event rows. It lets the client continue the temporal reducer without loading predecessor event pages. The client also requests the related entry page. Settings, follow-up, and current-update pages keep the Info inspector complete without loading every record. A page request does not change the shared watched-run projection or another client's cursor.
85
+ The host counts histories first and reads only the selected SQLite ranges. An unchanged subscription uses a lightweight revision check and reuses its prior view. It does not rebuild complete histories every 250 milliseconds.
134
86
 
135
- ## Messages
87
+ Large prompt, output, event, settings, follow-up, and update values use a content reference instead of making a protocol frame exceed 1 MiB. `view.content` returns the referenced UTF-8 content in verified chunks. The reference includes its media type, byte count, SHA-256 digest, and an opaque marker for host-created references. The host saves generated view content directly under its exact run ID, content digest, and media type before it advertises the reference. It does not share media metadata with general state blobs. A request for another run or media representation is unavailable. Memory-cache eviction therefore cannot make an advertised aggregate unavailable. Run pruning removes the durable content. Clients reassemble all chunks and verify the bytes against both the content response and the advertised reference before display. Opaque content is restored as user data without interpreting nested objects as host references. Other cursors and content references keep the complete logical history and result available.
136
88
 
137
- Client to server:
89
+ ## Subscriptions and reconnection
138
90
 
139
- | Type | Fields | Meaning |
140
- | ---------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
141
- | `watch_runs` | none | Subscribe to run-list rows. |
142
- | `watch_run` | `runId`, optional `revision`, `stepCursor`, `traceCursor`, `sessionEntryCursor`, `sessionEventCursor` | Subscribe or resume one run. |
143
- | `unwatch_run` | `runId` | End one run subscription. |
144
- | `fetch_page` | `runId`, `kind`, `cursor` | Read one bounded page. |
145
- | `fetch_artifact` | `runId`, `path` | Unsupported for SQLite state; returns `error`. |
91
+ A client keeps one persistent connection and records its desired run-list, run, and origin-session subscriptions. A request to watch a run that does not exist returns `notFound` and does not install a subscription. TypeScript and Rust clients show that response instead of waiting for a snapshot. Explicit `piw <runId>` mode keeps that run selected even when the run list contains a newer run. After reconnection, the client sends accepted subscriptions again with its run revision. The host sends a bounded snapshot when the client needs one. The protocol also supports retained revision patches. Unsubscribing sends the subscription ID to the host for every subscription kind, so no unused snapshot work remains on a live connection.
146
92
 
147
- Server to client:
93
+ A slow or disconnected client cannot stop the host, another client, claim renewal, or workflow execution. The host waits for socket drain before it publishes another snapshot to that connection. Polling coalesces while the connection is blocked, so the socket buffer cannot grow by one snapshot on every poll. When a connection closes, the host removes its subscriptions and exact origin-session activity immediately.
148
94
 
149
- | Type | Fields | Meaning |
150
- | -------------- | ------------------------------------------------------ | ---------------------------------------- |
151
- | `hello` | `protocol` | Identify the protocol. |
152
- | `runs` | `runs` | Send all lightweight run-list rows. |
153
- | `run_snapshot` | `runId`, `revision`, `view` | Send one bounded run view. |
154
- | `run_patch` | `runId`, `revision`, `targets` | Apply direct bounded target patches. |
155
- | `run_page` | `runId`, `revision`, `kind`, `start`, `total`, `items` | Return one bounded page. |
156
- | `artifact` | `runId`, `path`, `content` | Reserved and not sent by SQLite servers. |
157
- | `error` | `message`, optional `runId` | Report a sanitized request failure. |
95
+ ## Origin-session activity
158
96
 
159
- The run list contains `presentationRevision`, `manifest`, `live`, and `possiblyInterrupted`. It contains no payload bodies.
97
+ The Pi extension reports `started`, `refresh`, and `settled` activity for the exact session, run, interaction request, delivery, and Pi session entry. Reports use a monotonic sequence. The first report on each protocol connection is `started`; only later reports on that same connection use `refresh`. Refresh happens before the connection-scoped lease expires.
160
98
 
161
- ## Several clients
99
+ The host accepts activity only when its session, run, request, deterministic `interaction:<request-id>` delivery ID, and presented Pi session entry match the durable presented interaction. Activity entries are keyed by connection and request, so another caller-supplied delivery label cannot create a duplicate overlay. Activity changes display only. It does not grant authority, renew a workflow claim, settle a step, or change durable pause state. A disconnect or expired activity lease removes the overlay.
162
100
 
163
- The server keeps one projection and graph scene for each watched run. The first watcher loads it. Later watchers reuse it. The last unwatch or disconnect releases it. Different watched runs load independently.
101
+ ## Commands and uncertain results
164
102
 
165
- Each client keeps its own revision and page cursors. Network sends happen outside the shared state lock. A slow client cannot stop another client. If a broadcast receiver falls behind, that client receives a bounded snapshot.
103
+ Durable commands use stable idempotency keys. The Pi extension routes each state-changing command through the durable client request path. If the connection closes after the host commits but before the response arrives, the client reconnects with a new request ID and adopts the stored result. A retry with the same durable identity and payload adopts the stored receipt. Reusing that identity with another payload is a conflict. The request ID identifies one transport attempt and is not part of the durable request fingerprint. A retry after a local abort uses a new request ID and keeps the durable idempotency and submission IDs, so a late response from the aborted attempt cannot settle the retry.
166
104
 
167
- ## Reconnection
105
+ An `interaction.submit` response stays open while the supervised workflow child validates the value. The response settles only after the durable result is accepted, adopted, or rejected. A tool abort stops waiting immediately but does not undo an accepted host command. A later retry adopts the durable outcome. The host waits on the stored submission ID returned by adoption, not a different ID from the retry attempt. Clients do not poll SQLite.
168
106
 
169
- The client keeps the run list and selected run as desired state. After a disconnect, it keeps cached content visible with a stale or reconnecting label. It retries with bounded backoff, sends `watch_runs` after the next valid `hello`, and resumes `watch_run` from its revision and page cursors. A retained cursor receives patches. A stale cursor receives a bounded snapshot and requested pages.
107
+ The protocol does not claim exactly-once behavior for an external system that cannot prove it. An uncertain non-idempotent effect becomes ambiguous and requires explicit recovery.
170
108
 
171
- ## SQLite consistency
109
+ ## Maintenance
172
110
 
173
- The server uses a query-only SQLite connection. A writer commits the domain change, presentation revision, and patch records atomically. A reader sees all of that transaction or none of it.
111
+ Active `state.status`, `state.verify`, `state.backup`, and `state.prune` requests run in the host against its existing database connection. The CLI uses one stable client identity and creates a fresh idempotency key for each backup or applied prune invocation. If that invocation loses its connection, the client reconnects once with a new request ID and the same invocation key, so the host adopts the exact in-flight or stored result. A later user invocation gets a new key and does not reuse a stale success or rejection. The host keeps an in-flight maintenance command alive after a client disconnect, stores its accepted or rejected receipt before it responds, and adopts the exact retry instead of running the operation again. Host shutdown waits for in-flight maintenance receipts. `state.status` returns the database file size and safe counts for resources, runs, controllers, decisions, settings scopes, pending interactions, pending follow-ups, active leases, and unsettled effects.
174
112
 
175
- The refresh timer first checks `PRAGMA data_version`. An unchanged value causes no run-index query and no payload read. A changed value refreshes lightweight rows and only the watched projections whose presentation revisions changed.
113
+ Only `pi-workflows state verify <inactive-backup>` opens SQLite outside the host. It uses a query-only TypeScript connection and rejects the active database, including another path to the same file.