@osolmaz/pi-workflows 0.13.4 → 0.15.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 (237) hide show
  1. package/README.md +146 -160
  2. package/dist/builtins/autoimplement.workflow.js +3 -1
  3. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  4. package/dist/builtins/change-verification.workflow.js +7 -2
  5. package/dist/builtins/change-verification.workflow.js.map +1 -1
  6. package/dist/builtins/metadata.d.ts +22 -0
  7. package/dist/builtins/metadata.js +10 -0
  8. package/dist/builtins/metadata.js.map +1 -0
  9. package/dist/builtins/monitor.workflow.js +4 -1
  10. package/dist/builtins/monitor.workflow.js.map +1 -1
  11. package/dist/builtins/sanity-check.workflow.js +4 -1
  12. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  13. package/dist/builtins/workspace-preparation.workflow.js +3 -1
  14. package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
  15. package/dist/controllers/index.d.ts +1 -2
  16. package/dist/controllers/index.js +0 -1
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.d.ts +85 -31
  19. package/dist/controllers/sqlite.js +541 -126
  20. package/dist/controllers/sqlite.js.map +1 -1
  21. package/dist/controllers/store.d.ts +1 -0
  22. package/dist/extension/controller-command.d.ts +22 -0
  23. package/dist/extension/controller-command.js +32 -0
  24. package/dist/extension/controller-command.js.map +1 -0
  25. package/dist/extension/index.d.ts +2 -9
  26. package/dist/extension/index.js +876 -2928
  27. package/dist/extension/index.js.map +1 -1
  28. package/dist/extension/message-card.d.ts +13 -0
  29. package/dist/extension/message-card.js +46 -0
  30. package/dist/extension/message-card.js.map +1 -0
  31. package/dist/extension/step-message.d.ts +5 -6
  32. package/dist/extension/step-message.js +41 -52
  33. package/dist/extension/step-message.js.map +1 -1
  34. package/dist/host/child-worker-supervisor.d.ts +50 -0
  35. package/dist/host/child-worker-supervisor.js +159 -0
  36. package/dist/host/child-worker-supervisor.js.map +1 -0
  37. package/dist/host/client.d.ts +48 -0
  38. package/dist/host/client.js +216 -0
  39. package/dist/host/client.js.map +1 -0
  40. package/dist/host/controller-worker-entry.d.ts +2 -0
  41. package/dist/host/controller-worker-entry.js +244 -0
  42. package/dist/host/controller-worker-entry.js.map +1 -0
  43. package/dist/host/controller-worker-protocol.d.ts +36 -0
  44. package/dist/host/controller-worker-protocol.js +49 -0
  45. package/dist/host/controller-worker-protocol.js.map +1 -0
  46. package/dist/host/controller-worker-supervisor.d.ts +21 -0
  47. package/dist/host/controller-worker-supervisor.js +54 -0
  48. package/dist/host/controller-worker-supervisor.js.map +1 -0
  49. package/dist/host/host-entry.d.ts +2 -0
  50. package/dist/host/host-entry.js +23 -0
  51. package/dist/host/host-entry.js.map +1 -0
  52. package/dist/host/processes.d.ts +17 -12
  53. package/dist/host/processes.js +154 -50
  54. package/dist/host/processes.js.map +1 -1
  55. package/dist/host/protocol.d.ts +38 -0
  56. package/dist/host/protocol.js +156 -0
  57. package/dist/host/protocol.js.map +1 -0
  58. package/dist/host/resolver-entry.d.ts +49 -0
  59. package/dist/host/resolver-entry.js +149 -0
  60. package/dist/host/resolver-entry.js.map +1 -0
  61. package/dist/host/rpc-executor.d.ts +14 -3
  62. package/dist/host/rpc-executor.js +63 -30
  63. package/dist/host/rpc-executor.js.map +1 -1
  64. package/dist/host/runner.d.ts +95 -29
  65. package/dist/host/runner.js +2311 -348
  66. package/dist/host/runner.js.map +1 -1
  67. package/dist/host/state.d.ts +174 -0
  68. package/dist/host/state.js +689 -0
  69. package/dist/host/state.js.map +1 -0
  70. package/dist/host/worker-entry.d.ts +10 -0
  71. package/dist/host/worker-entry.js +446 -0
  72. package/dist/host/worker-entry.js.map +1 -0
  73. package/dist/host/worker-protocol.d.ts +31 -0
  74. package/dist/host/worker-protocol.js +122 -0
  75. package/dist/host/worker-protocol.js.map +1 -0
  76. package/dist/host/worker-store.d.ts +80 -0
  77. package/dist/host/worker-store.js +91 -0
  78. package/dist/host/worker-store.js.map +1 -0
  79. package/dist/host/worker-supervisor.d.ts +22 -0
  80. package/dist/host/worker-supervisor.js +54 -0
  81. package/dist/host/worker-supervisor.js.map +1 -0
  82. package/dist/render/canvas.d.ts +2 -1
  83. package/dist/render/canvas.js +40 -15
  84. package/dist/render/canvas.js.map +1 -1
  85. package/dist/render/graph-render.d.ts +7 -3
  86. package/dist/render/graph-render.js +125 -74
  87. package/dist/render/graph-render.js.map +1 -1
  88. package/dist/state/database.js +3 -1
  89. package/dist/state/database.js.map +1 -1
  90. package/dist/state/index.d.ts +1 -0
  91. package/dist/state/index.js +1 -0
  92. package/dist/state/index.js.map +1 -1
  93. package/dist/state/prune.js +36 -10
  94. package/dist/state/prune.js.map +1 -1
  95. package/dist/state/schema.d.ts +1 -1
  96. package/dist/state/schema.js +171 -3
  97. package/dist/state/schema.js.map +1 -1
  98. package/dist/state/viewer.d.ts +46 -0
  99. package/dist/state/viewer.js +249 -0
  100. package/dist/state/viewer.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +1 -1
  102. package/dist/viewer/cli.js +47 -18
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/workflows/composition.js +25 -2
  105. package/dist/workflows/composition.js.map +1 -1
  106. package/dist/workflows/definition.d.ts +3 -1
  107. package/dist/workflows/definition.js +25 -0
  108. package/dist/workflows/definition.js.map +1 -1
  109. package/dist/workflows/engine.d.ts +1 -0
  110. package/dist/workflows/engine.js +157 -42
  111. package/dist/workflows/engine.js.map +1 -1
  112. package/dist/workflows/errors.d.ts +3 -1
  113. package/dist/workflows/errors.js +4 -7
  114. package/dist/workflows/errors.js.map +1 -1
  115. package/dist/workflows/human-decision.d.ts +3 -0
  116. package/dist/workflows/human-decision.js +31 -0
  117. package/dist/workflows/human-decision.js.map +1 -1
  118. package/dist/workflows/index.d.ts +1 -1
  119. package/dist/workflows/index.js +1 -1
  120. package/dist/workflows/index.js.map +1 -1
  121. package/dist/workflows/schema.js +17 -6
  122. package/dist/workflows/schema.js.map +1 -1
  123. package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
  124. package/dist/{viewer → workflows}/session-reducer.js +4 -0
  125. package/dist/workflows/session-reducer.js.map +1 -0
  126. package/dist/workflows/store.d.ts +87 -1
  127. package/dist/workflows/store.js +892 -93
  128. package/dist/workflows/store.js.map +1 -1
  129. package/dist/workflows/tool-input.d.ts +0 -22
  130. package/dist/workflows/tool-input.js +1 -44
  131. package/dist/workflows/tool-input.js.map +1 -1
  132. package/dist/workflows/types.d.ts +36 -4
  133. package/docs/2026-08-25-workflow-follow-ups.md +8 -6
  134. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
  135. package/docs/CONTROLLERS.md +11 -11
  136. package/docs/DEFERRED_TURNS.md +61 -27
  137. package/docs/HUMAN_DECISIONS.md +12 -4
  138. package/docs/SQLITE_STATE.md +42 -8
  139. package/docs/WORKFLOW_HOST.md +452 -0
  140. package/docs/development.md +46 -30
  141. package/docs/live-replay-protocol.md +129 -100
  142. package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
  143. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
  144. package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
  145. package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
  146. package/docs/plans/tui-viewer-implementation-plan.md +5 -0
  147. package/docs/tui-viewer.md +18 -3
  148. package/docs/workflows.md +164 -156
  149. package/examples/workflows/command-batch.workflow.ts +2 -0
  150. package/examples/workflows/shell.workflow.ts +2 -1
  151. package/herdr-plugin.toml +1 -1
  152. package/package.json +1 -1
  153. package/skills/autodoc/SKILL.md +1 -1
  154. package/skills/autoimplement/SKILL.md +1 -1
  155. package/skills/autoplan/SKILL.md +1 -1
  156. package/skills/pi-workflows/SKILL.md +2 -0
  157. package/src/builtins/autoimplement.workflow.ts +3 -0
  158. package/src/builtins/change-verification.workflow.ts +7 -2
  159. package/src/builtins/metadata.ts +9 -0
  160. package/src/builtins/monitor.workflow.ts +4 -0
  161. package/src/builtins/sanity-check.workflow.ts +4 -0
  162. package/src/builtins/workspace-preparation.workflow.ts +3 -1
  163. package/src/controllers/index.ts +3 -5
  164. package/src/controllers/sqlite.ts +929 -225
  165. package/src/controllers/store.ts +1 -0
  166. package/src/extension/controller-command.ts +45 -0
  167. package/src/extension/index.ts +948 -3506
  168. package/src/extension/message-card.ts +61 -0
  169. package/src/extension/step-message.ts +58 -63
  170. package/src/host/child-worker-supervisor.ts +183 -0
  171. package/src/host/client.ts +293 -0
  172. package/src/host/controller-worker-entry.ts +311 -0
  173. package/src/host/controller-worker-protocol.ts +104 -0
  174. package/src/host/controller-worker-supervisor.ts +79 -0
  175. package/src/host/host-entry.ts +23 -0
  176. package/src/host/processes.ts +171 -54
  177. package/src/host/protocol.ts +196 -0
  178. package/src/host/resolver-entry.ts +241 -0
  179. package/src/host/rpc-executor.ts +76 -34
  180. package/src/host/runner.ts +2813 -422
  181. package/src/host/state.ts +1160 -0
  182. package/src/host/worker-entry.ts +533 -0
  183. package/src/host/worker-protocol.ts +165 -0
  184. package/src/host/worker-store.ts +229 -0
  185. package/src/host/worker-supervisor.ts +74 -0
  186. package/src/render/canvas.ts +44 -10
  187. package/src/render/graph-render.ts +145 -90
  188. package/src/state/database.ts +2 -1
  189. package/src/state/index.ts +14 -0
  190. package/src/state/prune.ts +35 -9
  191. package/src/state/schema.ts +171 -3
  192. package/src/state/viewer.ts +356 -0
  193. package/src/viewer/cli.ts +49 -17
  194. package/src/workflows/composition.ts +36 -2
  195. package/src/workflows/definition.ts +32 -0
  196. package/src/workflows/engine.ts +157 -54
  197. package/src/workflows/errors.ts +11 -2
  198. package/src/workflows/human-decision.ts +49 -0
  199. package/src/workflows/index.ts +2 -0
  200. package/src/workflows/schema.ts +19 -6
  201. package/src/{viewer → workflows}/session-reducer.ts +11 -1
  202. package/src/workflows/store.ts +1316 -108
  203. package/src/workflows/tool-input.ts +2 -60
  204. package/src/workflows/types.ts +32 -4
  205. package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
  206. package/dist/controllers/workflow-engine-scheduler.js +0 -175
  207. package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
  208. package/dist/extension/controller-host.d.ts +0 -48
  209. package/dist/extension/controller-host.js +0 -110
  210. package/dist/extension/controller-host.js.map +0 -1
  211. package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
  212. package/dist/extension/deferred-turn-coordinator.js +0 -143
  213. package/dist/extension/deferred-turn-coordinator.js.map +0 -1
  214. package/dist/extension/deferred-turn.d.ts +0 -44
  215. package/dist/extension/deferred-turn.js +0 -110
  216. package/dist/extension/deferred-turn.js.map +0 -1
  217. package/dist/extension/executor.d.ts +0 -86
  218. package/dist/extension/executor.js +0 -311
  219. package/dist/extension/executor.js.map +0 -1
  220. package/dist/extension/follow-up-coordinator.d.ts +0 -27
  221. package/dist/extension/follow-up-coordinator.js +0 -131
  222. package/dist/extension/follow-up-coordinator.js.map +0 -1
  223. package/dist/extension/recorder.d.ts +0 -84
  224. package/dist/extension/recorder.js +0 -528
  225. package/dist/extension/recorder.js.map +0 -1
  226. package/dist/extension/session-events.d.ts +0 -133
  227. package/dist/extension/session-events.js +0 -61
  228. package/dist/extension/session-events.js.map +0 -1
  229. package/dist/viewer/session-reducer.js.map +0 -1
  230. package/src/controllers/workflow-engine-scheduler.ts +0 -246
  231. package/src/extension/controller-host.ts +0 -167
  232. package/src/extension/deferred-turn-coordinator.ts +0 -171
  233. package/src/extension/deferred-turn.ts +0 -166
  234. package/src/extension/executor.ts +0 -411
  235. package/src/extension/follow-up-coordinator.ts +0 -151
  236. package/src/extension/recorder.ts +0 -655
  237. package/src/extension/session-events.ts +0 -121
@@ -1,146 +1,175 @@
1
1
  # Live replay protocol
2
2
 
3
- The Rust viewer (`tui/`) can watch runs in two ways: by reading SQLite runs
4
- directly from the filesystem (the default, in-process) or by connecting to a
5
- `piw serve` WebSocket server. Both paths produce the same semantic state; the
6
- protocol below is the network form of that state. Protocol id:
7
- `pi-workflows.replay.v1`.
8
-
9
- The server is a reader like any other: it only consumes SQLite runs (see
10
- [SQLITE_STATE.md](SQLITE_STATE.md)) and never writes them. The protocol has no
11
- authentication, so the server only accepts loopback bind addresses and refuses
12
- to start on anything else; workflow state contains private data, and remote viewing
13
- goes through an SSH tunnel. Handshakes that
14
- carry an `Origin` header are rejected: browsers always send one, and a web
15
- page must not be able to read workflow state by opening a WebSocket to localhost.
16
-
17
- ## Transport and framing
18
-
19
- A single WebSocket endpoint (`/ws`). Every message is one JSON object with a
20
- `type` field. Unknown message types and unknown fields must be ignored by both
21
- sides. The server sends `hello` on connect; a client that does not recognize
22
- the protocol id must disconnect.
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`.
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.
6
+
7
+ ## Framing
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.
23
10
 
24
11
  ```json
25
12
  { "type": "hello", "protocol": "pi-workflows.replay.v1" }
26
13
  ```
27
14
 
28
- ## Run views
15
+ ## Bounded run view
29
16
 
30
- The unit of synchronization is the **run view**, the semantic state of one
31
- run:
17
+ A snapshot contains one bounded run view:
32
18
 
33
19
  ```json
34
20
  {
21
+ "presentationRevision": 42,
22
+ "graphRevision": 17,
35
23
  "manifest": { … },
36
24
  "workflow": { … },
37
- "state": { … },
38
- "events": [ … ],
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
+ },
39
44
  "session": {
45
+ "presentationRevision": 42,
40
46
  "binding": { … },
41
- "entries": [ … ],
42
- "events": [ … ],
43
- "eventsMalformed": false,
44
- "eventsTornTail": false,
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
+ },
45
59
  "capture": { … }
46
60
  },
61
+ "settingsScopes": [ … ],
62
+ "followUpQueue": { … },
47
63
  "live": true,
48
64
  "possiblyInterrupted": false
49
65
  }
50
66
  ```
51
67
 
52
- - `manifest`, `workflow`, `state`, `events`, and every `session` field are
53
- semantic projections from SQLite. `workflow` is the definition snapshot,
54
- top-level `events` are workflow events, `session.entries` are settled Pi
55
- entries, `session.events` are normalized temporal events, and
56
- `session.capture` is capture integrity. `session` is `null` until a binding
57
- exists.
58
- - `live` is true while the run status is non-terminal. `possiblyInterrupted`
59
- is a reader-side diagnostic based on current ownership and update time.
60
- - Values are resolved from content-addressed SQLite blobs.
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.
75
+
76
+ ```json
77
+ {
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
+ ]
94
+ }
95
+ ```
96
+
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.
98
+
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.
100
+
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.
102
+
103
+ The database retains 256 presentation revisions per run. The server does not replay an unbounded patch backlog.
61
104
 
62
- Because the full trace and session history are part of the view, replay
63
- scrubbing is a pure client-side operation; rewinding never requires the
64
- server. Clients order session events by `seq` and use `at` only for playback
65
- timing.
105
+ ## Pages
66
106
 
67
- ## Snapshot, then patches
107
+ A client asks for a page with `fetch_page`:
108
+
109
+ ```json
110
+ {
111
+ "type": "fetch_page",
112
+ "runId": "run-1",
113
+ "kind": "session_events",
114
+ "cursor": 20000
115
+ }
116
+ ```
68
117
 
69
- State synchronization follows a snapshot-then-patch model. After a client
70
- subscribes to a run, the server sends one `run_snapshot`, then a stream of
71
- `run_patch` messages:
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`:
72
119
 
73
120
  ```json
74
- { "type": "run_snapshot", "runId": "…", "revision": 3, "view": { … } }
75
- { "type": "run_patch", "runId": "…", "revision": 4, "patch": [
76
- { "op": "append", "path": "/events", "value": [ { "seq": 18, … } ] },
77
- { "op": "replace", "path": "/state", "value": { … } }
78
- ] }
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
+ }
79
131
  ```
80
132
 
81
- - `revision` increases by exactly 1 per patch. A client that observes a gap
82
- must resubscribe and take a fresh snapshot.
83
- - `patch` is JSON Patch (RFC 6902) plus one extension op: `append`, whose
84
- `value` is an array of items appended to the array at `path`. Semantically
85
- `append` equals a sequence of `add` ops at `/-`; it exists so that the
86
- common case (trace and session growth) stays compact and readable.
87
- - Session growth uses `append` at `/session/entries` and `/session/events`.
88
- Capture changes use `replace` at `/session/capture`. Changes to the derived
89
- tail diagnostics use `replace` at `/session/eventsMalformed` and
90
- `/session/eventsTornTail`.
91
- - The server waits 50 ms after a filesystem notification before refreshing,
92
- so one token burst normally becomes one revision. Batch boundaries never
93
- merge or alter event records.
94
- - Patches are computed against the previous view revision; applying them in
95
- order reproduces the server's view exactly.
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.
96
134
 
97
135
  ## Messages
98
136
 
99
137
  Client to server:
100
138
 
101
- | type | fields | meaning |
102
- | ---------------- | --------------- | ---------------------------- |
103
- | `watch_runs` | | subscribe to the run listing |
104
- | `watch_run` | `runId` | subscribe to one run's view |
105
- | `unwatch_run` | `runId` | end a run subscription |
106
- | `fetch_artifact` | `runId`, `path` | unsupported; returns `error` |
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`. |
107
146
 
108
147
  Server to client:
109
148
 
110
- | type | fields | meaning |
111
- | -------------- | ---------------------------- | ----------------------------------------------- |
112
- | `hello` | `protocol` | sent once on connect |
113
- | `runs` | `runs` | full run listing (summaries), re-sent on change |
114
- | `run_snapshot` | `runId`, `revision`, `view` | full view after subscribe |
115
- | `run_patch` | `runId`, `revision`, `patch` | incremental view update |
116
- | `artifact` | `runId`, `path`, `content` | reserved; not sent by SQLite-backed servers |
117
- | `error` | `message`, `runId?` | request failed |
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. |
118
158
 
119
- SQLite-backed views contain resolved values. A `fetch_artifact` request returns
120
- an `error` because there is no artifact directory.
159
+ The run list contains `presentationRevision`, `manifest`, `live`, and `possiblyInterrupted`. It contains no payload bodies.
121
160
 
122
- Run listing summaries are the manifest plus `live` and
123
- `possiblyInterrupted`:
161
+ ## Several clients
124
162
 
125
- ```json
126
- { "type": "runs", "runs": [ { "manifest": { … }, "live": true, "possiblyInterrupted": false } ] }
127
- ```
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.
128
164
 
129
- The run listing is small and changes rarely, so it is always sent whole; only
130
- run views use patches.
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.
131
166
 
132
167
  ## Reconnection
133
168
 
134
- The native client treats the run listing and selected run as desired state rather
135
- than one-shot commands. After a connection closes,
136
- it keeps the cached run visible with a stale/reconnecting label, retries with
137
- bounded backoff, sends `watch_runs` after the next valid `hello`, and restores
138
- the current `watch_run`. A reconnect receives a fresh snapshot before later
139
- patches.
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.
170
+
171
+ ## SQLite consistency
140
172
 
141
- ## SQLite semantics behind the protocol
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.
142
174
 
143
- The server polls `state.sqlite` through a query-only connection. Each refresh
144
- reads a committed run projection, immutable events, session rows, and blob
145
- values. A transaction is either fully visible or not visible, so a client never
146
- observes half of a state transition.
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.
@@ -52,6 +52,11 @@ Pi and Telegram implement one channel interface. Workflows address a named audie
52
52
  - Accept the first valid response with a no-replace write.
53
53
  - Adopt identical retries and reject conflicting responses.
54
54
  - Derive one continuation identity from the accepted decision.
55
+ - Prepare that continuation through one atomic queue operation.
56
+ - Let the first caller create and claim the queue row.
57
+ - Let compatible concurrent or repeated callers adopt the row without changing its lease, claim generation, queue state, timestamps, or events.
58
+ - Reject incompatible workflow source, definition, input, launch options, parent, or owning session without changing the existing row.
59
+ - Give only the winning caller a claim token and permission to start the engine.
55
60
  - Reject stale responses by decision ID and canonical request digest.
56
61
  - Rebuild the pending-decision index from immutable records.
57
62
 
@@ -83,6 +88,10 @@ Pi and Telegram implement one channel interface. Workflows address a named audie
83
88
  - Promise exactly-once Telegram message creation after an ambiguous Bot API response.
84
89
  - Add arbitrary forms in the first release. Choice buttons and one text input cover the required flows.
85
90
  - Change existing checkpoints or historical run bundles.
91
+ - Add a database field, table, migration, schema version, compatibility reader, or dual-write path for continuation startup.
92
+ - Coordinate continuation startup with a process-local promise map, timing guard, retry loop, or swallowed error.
93
+ - Change general queue claim behavior for unrelated launch paths.
94
+ - Repair an already stranded live continuation as part of this change.
86
95
  - Enable human approval by default in existing built-in workflows.
87
96
 
88
97
  ## Design decisions
@@ -107,7 +116,13 @@ The Telegram adapter accepts replan text only as a reply to the exact `ForceRepl
107
116
 
108
117
  ### Make answer acceptance exact
109
118
 
110
- Channel delivery can be retried or fail independently. Decision acceptance is one atomic no-replace operation. The accepted response and deterministic continuation identity prevent two channels from starting two continuations.
119
+ Channel delivery can be retried or fail independently. Decision acceptance is one atomic no-replace operation. The accepted response and deterministic continuation identity prevent two channels from selecting different continuations.
120
+
121
+ ### Prepare or adopt one continuation
122
+
123
+ Direct answer handling and periodic recovery can both see the accepted decision. They use one continuation coordinator and one durable queue operation. The operation creates and claims a missing queue row or returns a compatible existing row as adopted. Only a newly claimed preparation can call `WorkflowEngine.continueRun()`.
124
+
125
+ An adopted result is normal success. It does not start another engine or change the winning lease, claim generation, queue state, timestamps, or events. The adopter receives no claim token, so it cannot renew, release, park, complete, or replace the winning claim. Incompatible reuse fails without mutation. Existing cleanup handles failure before run initialization, and existing lease expiry and activation recovery handle a winning process that stops after preparation.
111
126
 
112
127
  ### Handle Telegram delivery limits honestly
113
128
 
@@ -171,10 +186,17 @@ A private SQLite index may track pending decisions, channel leases, Telegram upd
171
186
  - Validate the accepted response through the node contract.
172
187
  - Preserve the parent's original workflow input.
173
188
  - Expose the response as the checkpoint output.
174
- - derive and adopt the continuation identity; and
175
- - leave the legacy checkpoint path unchanged.
176
-
177
- Test crashes before acceptance, after acceptance, during continuation creation, and after continuation completion.
189
+ - Derive the deterministic continuation identity.
190
+ - Add a typed atomic prepare-or-adopt operation to the existing SQLite queue store.
191
+ - Compare the stored workflow source, definition digest, input, launch options, parent, and owning session before adoption.
192
+ - Build one prepared continuation value in the Pi extension.
193
+ - Route direct verified answers and recovery through one continuation coordinator.
194
+ - Start `WorkflowEngine.continueRun()` only with the new claim token.
195
+ - Return normal started or already-continuing success to the answer path.
196
+ - Keep enqueue and lease behavior unchanged for other launch paths.
197
+ - Leave legacy checkpoint continuation unchanged.
198
+
199
+ Test crashes before acceptance, after acceptance, during continuation creation, and after continuation completion. Add a focused race test for direct answer handling and recovery. It must prove one claim generation, one engine start, one continuation, one execution of each continuation node, coherent queue and run state, and no duplicate-start or revision-conflict failure. Add a real-Pi version of the same regression with a durable barrier instead of sleep-only timing.
178
200
 
179
201
  ### Channel management
180
202
 
@@ -248,7 +270,9 @@ The setup command uses an existing mode-`0600` token file instead of collecting
248
270
  - The workflow tool cannot answer a protected human decision.
249
271
  - Pi and Telegram can receive the same decision through one audience profile.
250
272
  - The first concurrent valid answer wins and creates one continuation.
251
- - An identical retry is adopted; a conflicting or stale answer is rejected.
273
+ - Direct answer handling and recovery can race without starting it twice or replacing its lease.
274
+ - The first queue preparation starts the engine, and a compatible repeat adopts it without mutation.
275
+ - An incompatible continuation identity, conflicting answer, or stale answer is rejected without mutation.
252
276
  - The original workflow input survives a human-decision continuation.
253
277
  - Old checkpoints and old bundles pass their existing tests unchanged.
254
278
  - The Telegram adapter accepts text only from the bound reply and approved numeric actor.
@@ -267,6 +291,8 @@ npx vitest run test/human-decision-api.test.ts test/human-decision-store.test.ts
267
291
  npx vitest run test/human-decision-engine.test.ts test/run-resume.test.ts
268
292
  npx vitest run test/pi-decision-channel.test.ts test/telegram-decision-channel.test.ts
269
293
  npx vitest run test/plan-approval.test.ts test/composition.test.ts
294
+ npx vitest run test/run-queue.test.ts test/extension.test.ts
295
+ npx vitest run --config vitest.e2e.config.ts test/e2e/workflow.e2e.test.ts
270
296
  ```
271
297
 
272
298
  Run all repository gates before review:
@@ -290,7 +316,7 @@ This work adds compatible public APIs and additive persisted records. Release it
290
316
  ## Contract impact
291
317
 
292
318
  - **Session state:** normal workflow messages and interactive decision results.
293
- - **Other persistent data:** additive decision records, a rebuildable private channel index, and private channel configuration.
319
+ - **Other persistent data:** additive decision records, a rebuildable private channel index, private channel configuration, and existing run queue and lease records. The continuation startup fix adds no persistent field, table, migration, or schema version.
294
320
  - **Pi internals:** none.
295
321
  - **Public Pi API:** documented extension lifecycle plus command and UI methods only.
296
- - **Public pi-workflows API:** typed human choices, `humanDecision()`, `humanDecisionEdge()`, the channel interface, and the `plan-approval` workflow.
322
+ - **Public pi-workflows API:** typed human choices, `humanDecision()`, `humanDecisionEdge()`, the channel interface, the `plan-approval` workflow, and an additive queue prepare-or-adopt operation.