@osolmaz/pi-workflows 0.15.2 → 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 (124) 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 +64 -0
  24. package/dist/controllers/sqlite.js +219 -3
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +384 -156
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-delivery.d.ts +6 -0
  30. package/dist/extension/session-delivery.js +80 -25
  31. package/dist/extension/session-delivery.js.map +1 -1
  32. package/dist/extension/session-view.d.ts +21 -0
  33. package/dist/extension/session-view.js +127 -0
  34. package/dist/extension/session-view.js.map +1 -0
  35. package/dist/extension/widget.d.ts +2 -1
  36. package/dist/extension/widget.js +15 -7
  37. package/dist/extension/widget.js.map +1 -1
  38. package/dist/host/child-worker-supervisor.js +1 -1
  39. package/dist/host/child-worker-supervisor.js.map +1 -1
  40. package/dist/host/resolver-entry.d.ts +2 -23
  41. package/dist/host/resolver-entry.js +1 -1
  42. package/dist/host/resolver-entry.js.map +1 -1
  43. package/dist/host/runner.d.ts +12 -0
  44. package/dist/host/runner.js +565 -43
  45. package/dist/host/runner.js.map +1 -1
  46. package/dist/host/state.d.ts +8 -3
  47. package/dist/host/state.js +59 -27
  48. package/dist/host/state.js.map +1 -1
  49. package/dist/host/view.d.ts +73 -0
  50. package/dist/host/view.js +871 -0
  51. package/dist/host/view.js.map +1 -0
  52. package/dist/host/worker-protocol.js +1 -1
  53. package/dist/host/worker-protocol.js.map +1 -1
  54. package/dist/state/database.d.ts +1 -0
  55. package/dist/state/database.js +15 -0
  56. package/dist/state/database.js.map +1 -1
  57. package/dist/state/prune.d.ts +3 -1
  58. package/dist/state/prune.js +6 -8
  59. package/dist/state/prune.js.map +1 -1
  60. package/dist/state/schema.js +12 -1
  61. package/dist/state/schema.js.map +1 -1
  62. package/dist/viewer/backup.d.ts +2 -0
  63. package/dist/viewer/backup.js +28 -0
  64. package/dist/viewer/backup.js.map +1 -0
  65. package/dist/viewer/cli.d.ts +4 -0
  66. package/dist/viewer/cli.js +150 -170
  67. package/dist/viewer/cli.js.map +1 -1
  68. package/dist/viewer/tui.d.ts +5 -7
  69. package/dist/viewer/tui.js +188 -108
  70. package/dist/viewer/tui.js.map +1 -1
  71. package/dist/workflows/store.d.ts +62 -1
  72. package/dist/workflows/store.js +350 -44
  73. package/dist/workflows/store.js.map +1 -1
  74. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
  75. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  76. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  77. package/docs/SQLITE_STATE.md +13 -11
  78. package/docs/WORKFLOW_HOST.md +81 -64
  79. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
  80. package/docs/development.md +2 -1
  81. package/docs/live-replay-protocol.md +70 -132
  82. package/docs/tui-viewer.md +10 -14
  83. package/docs/workflows.md +56 -3
  84. package/herdr-plugin.toml +1 -1
  85. package/package.json +9 -3
  86. package/protocol/client.v1.schema.json +137 -0
  87. package/protocol/fixtures/client-v1.json +23 -0
  88. package/src/client/activity.ts +6 -0
  89. package/src/client/client.ts +935 -0
  90. package/src/client/index.ts +24 -0
  91. package/src/client/materialize.ts +228 -0
  92. package/src/client/protocol.ts +327 -0
  93. package/src/client/resolver.ts +26 -0
  94. package/src/client/view.ts +138 -0
  95. package/src/controllers/sqlite.ts +342 -3
  96. package/src/extension/index.ts +482 -171
  97. package/src/extension/session-delivery.ts +88 -25
  98. package/src/extension/session-view.ts +154 -0
  99. package/src/extension/widget.ts +18 -9
  100. package/src/host/child-worker-supervisor.ts +1 -1
  101. package/src/host/resolver-entry.ts +11 -26
  102. package/src/host/runner.ts +749 -75
  103. package/src/host/state.ts +82 -44
  104. package/src/host/view.ts +1084 -0
  105. package/src/host/worker-protocol.ts +1 -1
  106. package/src/state/database.ts +13 -0
  107. package/src/state/prune.ts +11 -11
  108. package/src/state/schema.ts +12 -1
  109. package/src/viewer/backup.ts +29 -0
  110. package/src/viewer/cli.ts +171 -185
  111. package/src/viewer/tui.ts +196 -124
  112. package/src/workflows/store.ts +500 -45
  113. package/dist/host/client.d.ts +0 -48
  114. package/dist/host/client.js +0 -216
  115. package/dist/host/client.js.map +0 -1
  116. package/dist/host/protocol.d.ts +0 -38
  117. package/dist/host/protocol.js +0 -156
  118. package/dist/host/protocol.js.map +0 -1
  119. package/dist/viewer/watch.d.ts +0 -6
  120. package/dist/viewer/watch.js +0 -46
  121. package/dist/viewer/watch.js.map +0 -1
  122. package/src/host/client.ts +0 -293
  123. package/src/host/protocol.ts +0 -196
  124. package/src/viewer/watch.ts +0 -51
@@ -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.
@@ -9,11 +9,11 @@ a recorded Pi conversation, themes, and remote viewing.
9
9
 
10
10
  The viewer uses the [incremental and virtualized viewer design](plans/2026-08-28-piw-incremental-viewer-plan.md).
11
11
 
12
- The run browser reads small metadata rows. It does not load trace, step, session, settings, or follow-up payloads. The local viewer checks SQLite `data_version` on its timer. When the value is unchanged, the timer does not scan runs or read payloads.
12
+ The run browser subscribes to small host-owned metadata views. It does not load trace, step, session, settings, or follow-up payloads. The host publishes a new bounded view only when its content changes.
13
13
 
14
- The selected run contains bounded pages. Step, trace, session-entry, and session-event pages contain at most 256 rows. Replay can jump to any position. The viewer loads the page that contains that position and keeps only the current windows. A compact graph projection keeps the latest attempt for each node and the taken transitions up to the replay point.
14
+ The selected run contains bounded pages. Step, trace, session-entry, session-event, settings, follow-up, and update pages have both a row limit and a byte budget. Replay can jump to any position. The viewer loads the page that contains that position and keeps only the current windows. A session-event page includes the replay checkpoint immediately before its first event. A compact graph projection keeps the latest attempt for each node and the taken transitions up to the replay point.
15
15
 
16
- Local page reads run outside input and drawing through one overwrite-only request slot. A newer selection replaces pending work. A failed first read leaves the run browser usable. A failed refresh keeps the last good view and marks it stale.
16
+ Large values use host content references. `piw` fetches them in bounded chunks when the user opens the related detail, verifies the byte count and SHA-256 digest, and then shows the complete text or JSON value. Page and content requests run outside input and drawing through the shared client protocol. A newer page selection replaces the previous request, including when the user returns to an earlier page. A failed first read leaves the run browser usable. A failed refresh keeps the last good view and marks it stale.
17
17
 
18
18
  ## Install
19
19
 
@@ -26,17 +26,15 @@ cargo install pi-workflows
26
26
 
27
27
  ## Modes
28
28
 
29
- - `piw` browses `~/.pi/agent/workflows/state.sqlite` through a read-only SQLite connection.
30
- - `piw <runId>` opens one run from that database.
31
- - `piw serve [--bind 127.0.0.1:9377]` exposes database-backed run views over the
32
- [live replay protocol](live-replay-protocol.md). Only
33
- loopback addresses are accepted; use an SSH tunnel for remote viewing.
29
+ - `piw` connects to the local package-owned workflow host. If the socket is absent, it runs the installed `pi-workflows host start` command.
30
+ - `piw <runId>` opens one host-owned run view.
31
+ - `piw <runId> --once` waits for that run, renders one complete 120 × 40 plain-text frame, and exits. It returns a nonzero status for host, protocol, missing-run, or snapshot-timeout failures.
32
+ - `piw serve [--bind 127.0.0.1:9377]` relays each WebSocket connection to one host socket over the [live client protocol](live-replay-protocol.md). Only loopback addresses are accepted; use an SSH tunnel for remote viewing.
34
33
  - `piw --connect ws://…` reads from another `piw serve` process.
35
34
  - `piw --theme <name>` selects a theme for this invocation.
36
35
  - `piw --list-themes` prints the built-in theme names.
37
36
 
38
- Direct database mode and connected mode use the same semantic run view.
39
- The protocol is the network form of that view.
37
+ Local and remote modes use the same semantic run view and the same protocol. `piw` has no SQLite mode or database schema copy.
40
38
 
41
39
  ## Herdr
42
40
 
@@ -211,11 +209,9 @@ listing and selected-run subscription after the server returns. Cached content
211
209
  stays visible but is labeled reconnecting or disconnected, never current.
212
210
  Revision gaps force a bounded snapshot.
213
211
 
214
- `piw serve` keeps one loaded projection and one graph scene for each watched run. The first watcher loads it. Later watchers reuse it. The last unwatch or disconnect releases it. Clients keep separate revision and page cursors, so one client's replay jump does not move another client. A lagged client gets a bounded snapshot instead of an unbounded patch backlog.
212
+ `piw serve` is a frame relay. It keeps no run projection or client activity after either side closes. Clients keep separate revision and page cursors, so one client's replay jump does not move another client. A lagged client gets a bounded snapshot instead of an unbounded patch backlog.
215
213
 
216
- Expanded prompt and output fields come from content-addressed SQLite blobs.
217
- The local reader uses query-only mode. Remote snapshots carry the same bounded
218
- semantic view and do not expose a filesystem path.
214
+ The host resolves expanded prompt and output fields from durable content. Local and remote snapshots carry the same bounded semantic view and do not expose a database path.
219
215
 
220
216
  ## Interaction
221
217
 
package/docs/workflows.md CHANGED
@@ -126,6 +126,8 @@ pi-workflows host run
126
126
  `host run` stays attached. The other commands start, inspect, or stop the
127
127
  on-demand process. No command installs an operating-system service.
128
128
 
129
+ The extension, CLI, and `piw` use the same version-1 client protocol over a Unix socket or Windows named pipe. The host sends byte-bounded run-list and run-view pages. Clients collect a complete run list for one revision and reject stale run pages whose cursor or revision no longer matches. The host reads only the selected history ranges and reuses an unchanged subscribed view after a lightweight revision check. It waits for a slow socket to drain and removes every subscription when its client unsubscribes. Large values stay available through verified content chunks. Host-generated aggregate values are saved and linked to the run before the host advertises them.
130
+
129
131
  A worker verifies the root and all mounted source identities before it loads
130
132
  workflow modules. It then checks the resolved mounted-source map and executes
131
133
  from committed state through a host-backed store. A source mismatch parks the
@@ -193,7 +195,10 @@ For submitted output, the engine appends the existing workflow-tool contract.
193
195
  The host checks the durable transport identifiers, stores a `validating`
194
196
  submission, and starts a supervised worker. In that worker, the output passes
195
197
  through tolerant JSON normalization and then `validate`. The tool reports
196
- success only after this check accepts the output. Rejected submissions return
198
+ success only after this check accepts the output. If the tool turn is aborted,
199
+ the client stops waiting but does not cancel the durable host command. A retry
200
+ uses a new transport request ID with the same durable submission identity and
201
+ adopts the stored result. Rejected submissions return
197
202
  the validation error and can retry in the same step. If the model settles
198
203
  without submitting, the durable request stays pending until it receives valid
199
204
  output, times out, or is cancelled. For assistant-message output, the engine appends a normal-response contract,
@@ -475,8 +480,18 @@ The model sees one `workflow` tool. Its `action` field supports:
475
480
  A direct user request to continue or resume the active workflow maps to
476
481
  `resume` immediately. The model does not call `status` instead of `resume` or
477
482
  use it as a prerequisite. An already active run adopts the resume request. A
478
- paused or parked run gets a new claim generation and worker. With no resumable
479
- run, the host rejects the request.
483
+ paused interaction remains the same durable request and resumes without a
484
+ worker. Other paused or parked work gets a new claim generation and worker.
485
+ With no resumable run, the host rejects the request.
486
+
487
+ The origin Pi session shows its active run in the workflow widget. `Shift+Up`
488
+ and `Shift+Down` scroll it. If Escape aborts the model turn started by a
489
+ presented workflow interaction, the extension detects the public context abort
490
+ signal and pauses that run through the host. It also accepts Pi's public
491
+ `aborted` stop reason. The matching `agent_end` event must contain the workflow
492
+ prompt, so an unrelated interrupted turn cannot pause old pending work. The
493
+ paused run does not accept updates, submissions, or decision answers until
494
+ `resume`.
480
495
 
481
496
  `status` reports the durable queue projection. A host command succeeds only
482
497
  after its transaction commits. The protocol stores request fingerprints and
@@ -785,3 +800,41 @@ const engine = new WorkflowEngine({
785
800
  });
786
801
  const { state } = await engine.run(workflow, { task: "..." });
787
802
  ```
803
+
804
+ ## Test the installed package
805
+
806
+ The installed-package end-to-end test packs this repository and installs the
807
+ archive with production dependencies in a temporary consumer project. It then
808
+ starts the repository-pinned base Pi with only that Pi Workflows installation.
809
+ The test checks command isolation, host startup, widget and status changes,
810
+ pause, resume, completion, and `piw <runId> --once` output.
811
+
812
+ Run the deterministic phase without a model call:
813
+
814
+ ```bash
815
+ npm run test:e2e:live -- --runtime-only
816
+ ```
817
+
818
+ A real-model phase is manual. Supply the exact Pi provider and model as separate
819
+ values. The runner does not select a default or accept model fallback:
820
+
821
+ ```bash
822
+ npm run test:e2e:live -- \
823
+ --provider openai \
824
+ --model gpt-5.6-luna
825
+ ```
826
+
827
+ For subscription authentication, use a dedicated Pi profile that has no other
828
+ extensions or resources:
829
+
830
+ ```bash
831
+ npm run test:e2e:live -- \
832
+ --profile ~/.config/pi-workflows-e2e/openai-codex \
833
+ --provider openai-codex \
834
+ --model gpt-5.6-luna
835
+ ```
836
+
837
+ The profile and normal provider environment remain operator-owned. The runner
838
+ does not read, copy, print, or save credentials. It uses one guarded temporary
839
+ root and removes that root after success or failure. Use `--keep` only when you
840
+ need the isolated files for diagnosis.
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "osolmaz.pi-workflows"
2
2
  name = "pi-workflows"
3
- version = "0.15.2"
3
+ version = "0.16.0"
4
4
  min_herdr_version = "0.7.0"
5
5
  description = "Open the active pi-workflows run in piw from a managed Herdr pane."
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -24,6 +24,7 @@
24
24
  "examples",
25
25
  "docs",
26
26
  "schemas",
27
+ "protocol",
27
28
  "plugins/herdr",
28
29
  "herdr-plugin.toml",
29
30
  "README.md",
@@ -39,6 +40,10 @@
39
40
  "types": "./dist/extension/index.d.ts",
40
41
  "default": "./dist/extension/index.js"
41
42
  },
43
+ "./client": {
44
+ "types": "./dist/client/index.d.ts",
45
+ "default": "./dist/client/index.js"
46
+ },
42
47
  "./controllers": {
43
48
  "types": "./dist/controllers/index.d.ts",
44
49
  "default": "./dist/controllers/index.js"
@@ -62,11 +67,13 @@
62
67
  "test": "vitest run",
63
68
  "test:coverage": "vitest run --coverage",
64
69
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
70
+ "test:e2e:live": "node scripts/live-e2e.mjs",
65
71
  "typecheck": "tsc -p tsconfig.json --noEmit"
66
72
  },
67
73
  "dependencies": {
68
74
  "better-sqlite3": "^13.0.2",
69
- "jiti": "^2.7.0"
75
+ "jiti": "^2.7.0",
76
+ "tsx": "^4.23.13"
70
77
  },
71
78
  "devDependencies": {
72
79
  "@earendil-works/pi-ai": "0.84.2",
@@ -77,7 +84,6 @@
77
84
  "@vitest/coverage-istanbul": "^4.1.10",
78
85
  "oxfmt": "^0.59.0",
79
86
  "oxlint": "^1.74.0",
80
- "tsx": "^4.23.1",
81
87
  "typebox": "^1.3.6",
82
88
  "typescript": "^7.0.2",
83
89
  "vitest": "^4.1.10"
@@ -0,0 +1,137 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/osolmaz/pi-workflows/blob/main/protocol/client.v1.schema.json",
4
+ "title": "Pi Workflows client protocol version 1",
5
+ "oneOf": [
6
+ {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": ["schema", "type", "connectionId", "packageVersion"],
10
+ "properties": {
11
+ "schema": { "const": "pi-workflows.client.v1" },
12
+ "type": { "const": "hello" },
13
+ "connectionId": { "$ref": "#/$defs/opaqueId" },
14
+ "packageVersion": { "type": "string", "minLength": 1 }
15
+ }
16
+ },
17
+ {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "required": [
21
+ "schema",
22
+ "type",
23
+ "requestId",
24
+ "clientId",
25
+ "operation",
26
+ "idempotencyKey",
27
+ "payload"
28
+ ],
29
+ "properties": {
30
+ "schema": { "const": "pi-workflows.client.v1" },
31
+ "type": { "const": "request" },
32
+ "requestId": { "$ref": "#/$defs/opaqueId" },
33
+ "clientId": { "$ref": "#/$defs/opaqueId" },
34
+ "operation": {
35
+ "enum": [
36
+ "run.start",
37
+ "run.pause",
38
+ "run.resume",
39
+ "run.cancel",
40
+ "run.status",
41
+ "run.list",
42
+ "checkpoint.answer",
43
+ "decision.answer",
44
+ "interaction.submit",
45
+ "interaction.update",
46
+ "notification.claim",
47
+ "notification.deliver",
48
+ "turn.claim",
49
+ "turn.resolve",
50
+ "controller.list",
51
+ "controller.get",
52
+ "controller.apply",
53
+ "controller.reconcile",
54
+ "controller.delete",
55
+ "host.status",
56
+ "host.stop",
57
+ "view.runs.watch",
58
+ "view.runs.page",
59
+ "view.run.get",
60
+ "view.run.watch",
61
+ "view.run.unwatch",
62
+ "view.page",
63
+ "view.content",
64
+ "view.session.watch",
65
+ "activity.report",
66
+ "state.status",
67
+ "state.verify",
68
+ "state.backup",
69
+ "state.prune"
70
+ ]
71
+ },
72
+ "idempotencyKey": { "$ref": "#/$defs/opaqueId" },
73
+ "runId": { "$ref": "#/$defs/opaqueId" },
74
+ "expectedRevision": { "$ref": "#/$defs/revision" },
75
+ "payload": {}
76
+ }
77
+ },
78
+ {
79
+ "type": "object",
80
+ "additionalProperties": false,
81
+ "required": ["schema", "type", "requestId", "outcome"],
82
+ "properties": {
83
+ "schema": { "const": "pi-workflows.client.v1" },
84
+ "type": { "const": "response" },
85
+ "requestId": { "$ref": "#/$defs/opaqueId" },
86
+ "outcome": {
87
+ "enum": [
88
+ "accepted",
89
+ "adopted",
90
+ "rejected",
91
+ "conflict",
92
+ "notFound",
93
+ "claimLost",
94
+ "unavailable"
95
+ ]
96
+ },
97
+ "revision": { "$ref": "#/$defs/revision" },
98
+ "receipt": {},
99
+ "error": { "type": "string", "minLength": 1 }
100
+ }
101
+ },
102
+ {
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "required": ["schema", "type", "subscriptionId", "event", "payload"],
106
+ "properties": {
107
+ "schema": { "const": "pi-workflows.client.v1" },
108
+ "type": { "const": "event" },
109
+ "subscriptionId": { "$ref": "#/$defs/opaqueId" },
110
+ "event": {
111
+ "enum": [
112
+ "runs",
113
+ "run_snapshot",
114
+ "run_patch",
115
+ "run_page",
116
+ "session_snapshot",
117
+ "unavailable"
118
+ ]
119
+ },
120
+ "revision": { "$ref": "#/$defs/revision" },
121
+ "runId": { "$ref": "#/$defs/opaqueId" },
122
+ "payload": {}
123
+ }
124
+ }
125
+ ],
126
+ "$defs": {
127
+ "opaqueId": {
128
+ "type": "string",
129
+ "minLength": 1,
130
+ "maxLength": 256
131
+ },
132
+ "revision": {
133
+ "type": "integer",
134
+ "minimum": 0
135
+ }
136
+ }
137
+ }