@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.
- package/README.md +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +56 -3
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Test installed workflows end to end
|
|
3
|
+
author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
|
|
4
|
+
date: 2026-09-02
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Test installed workflows end to end
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Add one black-box test runner that starts base Pi with only the packed Pi Workflows package. The runner will execute a workflow through the package-owned host and check Pi's workflow widget throughout its lifecycle. It will also check the Rust `piw` client. A separate phase can use a real model.
|
|
12
|
+
|
|
13
|
+
The runner must keep the Pi provider and model as separate exact values. It must work with Pi's built-in providers instead of containing OpenAI-specific model logic.
|
|
14
|
+
|
|
15
|
+
## Provider boundary
|
|
16
|
+
|
|
17
|
+
Pi has two distinct OpenAI providers.
|
|
18
|
+
|
|
19
|
+
- `openai-codex` uses ChatGPT Plus or Pro subscription credentials. Its GPT-5.6 models use the `openai-codex-responses` API against the ChatGPT backend.
|
|
20
|
+
- `openai` uses an OpenAI API key. Its GPT-5.6 models use the `openai-responses` API against `api.openai.com`.
|
|
21
|
+
|
|
22
|
+
The current base Pi catalog includes `gpt-5.6-luna` under both providers. These are separate provider and model pairs:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
provider=openai-codex model=gpt-5.6-luna
|
|
26
|
+
provider=openai model=gpt-5.6-luna
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The runner will require both `--provider` and `--model` for a real-model run. It will pass them to Pi as separate arguments and then query Pi's RPC state. The selected provider and model id must match the request. The selected API must match that exact pair. The runner will never accept Pi's model fallback.
|
|
30
|
+
|
|
31
|
+
Any built-in Pi provider can be used when its normal authentication is ready and its selected model can call tools. The runner will not write a `models.json` file or register a custom provider.
|
|
32
|
+
|
|
33
|
+
## Base Pi and resource isolation
|
|
34
|
+
|
|
35
|
+
The runner will invoke the `@earendil-works/pi-coding-agent` package entry point directly. It will not invoke the `pi` command found in `PATH`, because that command can be a wrapper that adds personal packages or restart behavior.
|
|
36
|
+
|
|
37
|
+
The default entry point will come from the repository's pinned Pi dependency. An optional `--pi-entry` argument will allow a pre-release check against another installed base Pi version.
|
|
38
|
+
|
|
39
|
+
Each run will use:
|
|
40
|
+
|
|
41
|
+
- one temporary home directory;
|
|
42
|
+
- one temporary project;
|
|
43
|
+
- one temporary session directory;
|
|
44
|
+
- one temporary package installation;
|
|
45
|
+
- either a temporary Pi agent directory or a dedicated user-supplied test profile.
|
|
46
|
+
|
|
47
|
+
Pi will start with `--no-skills`, `--no-themes`, `--no-prompt-templates`, `--no-context-files`, and `--no-builtin-tools`. The runner must not use `--no-extensions`, because that would also disable Pi Workflows.
|
|
48
|
+
|
|
49
|
+
After startup, the runner will call RPC `get_commands`. The `workflow`, `controller`, and `piw` commands must come from the packed Pi Workflows package. Any other user or project extension is a failure. Base Pi's own inline commands are allowed.
|
|
50
|
+
|
|
51
|
+
## Installed package test
|
|
52
|
+
|
|
53
|
+
The test must use the npm package that would be published, including its file allowlist and production dependencies.
|
|
54
|
+
|
|
55
|
+
The runner will:
|
|
56
|
+
|
|
57
|
+
1. Run `npm pack` into the temporary root.
|
|
58
|
+
2. Install that archive into a temporary consumer project with `npm install --omit=dev`.
|
|
59
|
+
3. Find the resulting `@osolmaz/pi-workflows` package directory.
|
|
60
|
+
4. Run base Pi's `install` command against that installed directory.
|
|
61
|
+
5. Start Pi and verify the extension command source paths point into that directory.
|
|
62
|
+
|
|
63
|
+
Passing the `.tgz` file directly to `pi install` is not valid. Pi treats that file as an extension path. Pointing Pi at an extracted package before its dependencies are installed also fails because `better-sqlite3` is absent. The temporary consumer installation avoids both errors and tests the same dependency layout as a normal npm install.
|
|
64
|
+
|
|
65
|
+
## Authentication
|
|
66
|
+
|
|
67
|
+
The runner will let base Pi resolve credentials through Pi's documented authentication rules. It will not read, print, copy, or rewrite credential values.
|
|
68
|
+
|
|
69
|
+
### Subscription profile
|
|
70
|
+
|
|
71
|
+
A subscription run will use a dedicated Pi test profile supplied with `--profile`. The operator logs in to that profile once with base Pi. For ChatGPT Plus or Pro, the requested provider is `openai-codex`.
|
|
72
|
+
|
|
73
|
+
The profile must be dedicated to this smoke test. The runtime resource check will reject unrelated extensions. The run uses a temporary home and temporary workflow state. Its session directory is also temporary even when the profile is reused for OAuth credentials.
|
|
74
|
+
|
|
75
|
+
### API key
|
|
76
|
+
|
|
77
|
+
An API-key run can use an ephemeral agent directory. Pi receives the caller's existing environment and resolves the provider's normal variable, such as `OPENAI_API_KEY` for the `openai` provider. The runner will not name, inspect, or log the variable value.
|
|
78
|
+
|
|
79
|
+
Before a model call, the runner will execute Pi's documented `auth check` command for the exact provider and model. It will stop before the workflow starts when authentication or model resolution fails.
|
|
80
|
+
|
|
81
|
+
## Runtime workflow
|
|
82
|
+
|
|
83
|
+
The first workflow does not call a model. It has one delayed compute step and a fixed final output.
|
|
84
|
+
|
|
85
|
+
The runner will start it through Pi RPC and check the following sequence:
|
|
86
|
+
|
|
87
|
+
1. The package-owned host starts on demand.
|
|
88
|
+
2. RPC emits `setWidget` and `setStatus` for the `pi-workflows` key with `running` state.
|
|
89
|
+
3. `/workflow pause` is accepted.
|
|
90
|
+
4. The widget and status change to `paused`.
|
|
91
|
+
5. Host status reports one parked run and no active workflow worker.
|
|
92
|
+
6. `/workflow resume` is accepted.
|
|
93
|
+
7. The widget and status return to `running`.
|
|
94
|
+
8. The workflow completes with the fixed output.
|
|
95
|
+
9. RPC clears the widget and status after completion.
|
|
96
|
+
10. No `extension_error` event appears.
|
|
97
|
+
|
|
98
|
+
This run proves package loading and client-to-host startup. It also proves supervised worker control and the RPC widget contract. The complete check uses no model usage.
|
|
99
|
+
|
|
100
|
+
## Real-model workflow
|
|
101
|
+
|
|
102
|
+
The second workflow has one structured agent step. Its prompt asks the selected model to submit one fixed object through the `workflow` tool. Built-in tools stay disabled, so the model sees the workflow tool without file or shell mutation tools.
|
|
103
|
+
|
|
104
|
+
The runner will check:
|
|
105
|
+
|
|
106
|
+
- Pi still reports the requested provider and model id, while its API matches that exact pair;
|
|
107
|
+
- one durable workflow step message exists in the origin session;
|
|
108
|
+
- the message has one request id and one current attempt id;
|
|
109
|
+
- one submission is accepted for that attempt;
|
|
110
|
+
- stale or duplicate session delivery does not appear;
|
|
111
|
+
- the workflow completes with the fixed output;
|
|
112
|
+
- the Pi turn settles and no extension error appears.
|
|
113
|
+
|
|
114
|
+
The result will not claim one upstream API request. Pi or the provider can retry a request. The test proves one accepted workflow submission and one origin-session delivery.
|
|
115
|
+
|
|
116
|
+
## Headless piw check
|
|
117
|
+
|
|
118
|
+
The current Rust `piw` command requires a terminal. Add one production command-line option:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
piw RUN_ID --once
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`--once` will connect through the normal Rust client and wait for one complete run view. It will render one frame with the existing `piw` renderer and a Ratatui test backend. It will then print the plain frame and exit. A missing run, connection failure, protocol error, or snapshot timeout will return a nonzero exit code.
|
|
125
|
+
|
|
126
|
+
The E2E runner will install the candidate Rust binary into the temporary root with `cargo install --path tui --root ... --locked`. It will call `piw RUN_ID --once` while the runtime workflow is active and after completion. The output must contain the exact run id, workflow name, step name, and current status.
|
|
127
|
+
|
|
128
|
+
This option also gives operators a normal noninteractive way to capture one viewer frame. It is not a test-only protocol.
|
|
129
|
+
|
|
130
|
+
## Runner interface
|
|
131
|
+
|
|
132
|
+
Add one command:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm run test:e2e:live -- [options]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Supported forms:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm run test:e2e:live -- --runtime-only
|
|
142
|
+
|
|
143
|
+
npm run test:e2e:live -- \
|
|
144
|
+
--profile ~/.config/pi-workflows-e2e/openai-codex \
|
|
145
|
+
--provider openai-codex \
|
|
146
|
+
--model gpt-5.6-luna
|
|
147
|
+
|
|
148
|
+
npm run test:e2e:live -- \
|
|
149
|
+
--provider openai \
|
|
150
|
+
--model gpt-5.6-luna
|
|
151
|
+
|
|
152
|
+
npm run test:e2e:live -- \
|
|
153
|
+
--profile /path/to/dedicated-profile \
|
|
154
|
+
--provider anthropic \
|
|
155
|
+
--model exact-model-id
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A real-model run requires explicit provider and model arguments. `--runtime-only` makes no model call. The runner will not choose a default provider, infer a provider from a model name, or change the model after startup.
|
|
159
|
+
|
|
160
|
+
## Cleanup and failure evidence
|
|
161
|
+
|
|
162
|
+
The standalone runner will own one temporary root and remove it in a `finally` path. It will:
|
|
163
|
+
|
|
164
|
+
- close the Pi RPC process;
|
|
165
|
+
- stop the workflow host through the installed client;
|
|
166
|
+
- wait for the host endpoint to disappear;
|
|
167
|
+
- stop child workers started for the smoke workflows;
|
|
168
|
+
- remove the npm consumer installation, sessions, workflow state, and fixture project.
|
|
169
|
+
|
|
170
|
+
A normal failure will print a small diagnostic summary before cleanup. The summary will identify the failed phase and the software versions. It will also show the exact provider, model name, run id, host status, recent RPC event types, and bounded `piw` output. It will not print credentials, arbitrary environment values, or unrelated session content.
|
|
171
|
+
|
|
172
|
+
An explicit `--keep` option can preserve the one temporary root for manual debugging. Without that option, failures must not leave stale test directories.
|
|
173
|
+
|
|
174
|
+
## Automated tests
|
|
175
|
+
|
|
176
|
+
Add tests for:
|
|
177
|
+
|
|
178
|
+
- `piw --once` argument handling and one-frame rendering;
|
|
179
|
+
- timeout, missing-run, and host connection failures;
|
|
180
|
+
- packed npm installation with production dependencies;
|
|
181
|
+
- extension source isolation in base Pi;
|
|
182
|
+
- RPC widget and status transitions;
|
|
183
|
+
- pause with no active worker;
|
|
184
|
+
- resume and completion;
|
|
185
|
+
- exact provider and model validation;
|
|
186
|
+
- model fallback rejection;
|
|
187
|
+
- cleanup after success and injected failure;
|
|
188
|
+
- bounded and secret-free diagnostics.
|
|
189
|
+
|
|
190
|
+
The existing mock-provider E2E suite remains the deterministic origin-session and model-tool gate. CI will run the new installed-package runner in `--runtime-only` mode. Real-provider runs stay manual because provider availability, subscription usage, API cost, and model behavior are external facts.
|
|
191
|
+
|
|
192
|
+
## Release use
|
|
193
|
+
|
|
194
|
+
Before a release:
|
|
195
|
+
|
|
196
|
+
1. Run all repository checks.
|
|
197
|
+
2. Run the installed-package test in `--runtime-only` mode.
|
|
198
|
+
3. Run one real-model check with an explicit provider and model.
|
|
199
|
+
4. Record the exact Pi version, package version, Rust version, provider, model id, API, and run result.
|
|
200
|
+
5. Do not release after fallback, ambiguous delivery, duplicate session delivery, extension error, cleanup failure, or disagreement between Pi, the host, and `piw`.
|
|
201
|
+
|
|
202
|
+
The GitHub publish jobs will continue to use deterministic checks. They must not receive a personal subscription credential or a broad API key.
|
|
203
|
+
|
|
204
|
+
## Files
|
|
205
|
+
|
|
206
|
+
Implementation will update:
|
|
207
|
+
|
|
208
|
+
- `scripts/live-e2e.mjs`;
|
|
209
|
+
- `test/fixtures/live-e2e/runtime.workflow.ts`;
|
|
210
|
+
- `test/fixtures/live-e2e/model.workflow.ts`;
|
|
211
|
+
- `package.json`;
|
|
212
|
+
- `tui/src/main.rs`;
|
|
213
|
+
- `tui/src/ui/mod.rs`;
|
|
214
|
+
- Rust and TypeScript E2E tests;
|
|
215
|
+
- `docs/workflows.md` and `docs/tui-viewer.md`.
|
|
216
|
+
|
|
217
|
+
## Contract impact
|
|
218
|
+
|
|
219
|
+
The runner creates only temporary Pi sessions and workflow state. A reusable subscription test profile is operator-owned and contains its own normal Pi authentication. The script does not change Pi session schemas, Pi internals, private APIs, or another repository.
|
|
220
|
+
|
|
221
|
+
The implementation uses documented Pi package installation and CLI model selection. It uses `auth check`, RPC state, RPC command discovery, and RPC extension UI events. Workflow state remains behind the existing Pi Workflows client protocol. The only new public interface is `piw RUN_ID --once`.
|
|
222
|
+
|
|
223
|
+
## Completion criteria
|
|
224
|
+
|
|
225
|
+
The work is complete when a clean base Pi process loads only the packed Pi Workflows package. The deterministic runtime workflow must pass every widget and lifecycle check, while `piw --once` must agree with Pi and the host. An explicitly selected built-in model must complete the structured agent workflow. Cleanup must leave no process or large temporary directory, and all repository checks must pass.
|
package/docs/SQLITE_STATE.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.”
|
package/docs/WORKFLOW_HOST.md
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)
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
201
|
+
Each message uses one envelope:
|
|
195
202
|
|
|
196
203
|
```json
|
|
197
204
|
{
|
|
198
|
-
"schema": "pi-workflows.
|
|
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,55 +214,56 @@ Every request uses this envelope:
|
|
|
206
214
|
}
|
|
207
215
|
```
|
|
208
216
|
|
|
209
|
-
A response
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
-
|
|
239
|
-
|
|
240
|
-
- `run.list
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
- `
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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.
|
|
229
|
+
|
|
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.
|
|
231
|
+
|
|
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.
|
|
258
267
|
|
|
259
268
|
## Worker protocol
|
|
260
269
|
|
|
@@ -296,7 +305,7 @@ Add only these records if implementation proves the current rows cannot hold the
|
|
|
296
305
|
|
|
297
306
|
### Host commands
|
|
298
307
|
|
|
299
|
-
`host_commands` stores request ID, client ID, operation, idempotency key, request fingerprint, run ID, accepted revision, outcome, receipt or error hash, and timestamps. The
|
|
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.
|
|
300
309
|
|
|
301
310
|
### Interactive requests
|
|
302
311
|
|
|
@@ -316,9 +325,11 @@ Agent and assistant-message steps for an interactive run execute in the origin P
|
|
|
316
325
|
|
|
317
326
|
The worker commits the node's resolved wall-clock deadline before it proposes `interaction.requested`. The host commits the request, changes the node attempt to waiting, parks the queue row, releases the claim, and acknowledges the worker. The worker then exits. The host continues to enforce the durable deadline while no worker exists. If the deadline passes, one control claim atomically closes the stale request and schedules a supervised timeout-resume child. The child preserves the same attempt and deadline, records `timed_out`, and follows any `$result.outcome` edge. A run with no timeout recovery edge becomes terminal and releases its session reservation. Restart recovery starts this timeout path before it schedules other work.
|
|
318
327
|
|
|
319
|
-
The extension finds pending requests during `session_start`, after `agent_settled`, and once per second while the session is open. One shared session-delivery coordinator handles step prompts, protected decisions, notifications, and terminal presentation turns. It waits until Pi is idle and has no pending messages before it claims new work. Because the host claim is asynchronous, it checks those conditions again immediately before the synchronous call to the documented `pi.sendMessage()` API.
|
|
328
|
+
The extension finds pending requests during `session_start`, after `agent_settled`, and once per second while the session is open. One shared session-delivery coordinator handles step prompts, protected decisions, notifications, and terminal presentation turns. It waits until Pi is idle and has no pending messages before it claims new work. Because the host claim is asynchronous, it checks those conditions again immediately before the synchronous call to the documented `pi.sendMessage()` API. The coordinator remembers the claimed delivery before that final check. If Pi became busy, a later poll can send with that exact claim while its lease remains live. An expired unused claim is discarded. Polling cannot acquire a second claim or send a delivery that is already queued.
|
|
329
|
+
|
|
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.
|
|
320
331
|
|
|
321
|
-
The
|
|
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.
|
|
322
333
|
|
|
323
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.
|
|
324
335
|
|
|
@@ -340,7 +351,7 @@ The run binding records `interactive` or `headless` execution mode. Viewers show
|
|
|
340
351
|
|
|
341
352
|
## Pause and cancellation
|
|
342
353
|
|
|
343
|
-
Pause atomically commits `paused = 1`, parks the queue, releases the exact claim, and stores the command receipt. The fenced worker process group then stops.
|
|
354
|
+
Pause atomically commits `paused = 1`, parks the queue, releases the exact claim, and stores the command receipt. The fenced worker process group then stops. If a Pi model turn ends while the public extension context signal is aborted, or with public stop reason `aborted`, the extension sends this same host pause command only when that `agent_end` event contains the pending interaction's workflow prompt. A parked interaction has no worker or live run claim, so the host marks it paused in place. While paused, updates, submissions, and decision answers are rejected. Resume clears the pause on that same pending interaction; other paused work takes a new generation and starts another worker from the last durable boundary. An uncommitted pure node can run again after resume.
|
|
344
355
|
|
|
345
356
|
Cancellation against a live worker atomically commits terminal cancellation, cancels pending attempt and interaction state, settles effect recovery state, releases the exact claim, and stores the command receipt. A pending effect becomes cancelled. An applying effect becomes ambiguous because the host cannot prove its external outcome. The host then stops the fenced worker process group. A host crash after the receipt cannot resume the cancelled run or retry the ambiguous effect. If the child does not stop by the deadline, the host kills its process group.
|
|
346
357
|
|
|
@@ -433,7 +444,8 @@ When the installed state has the old digest, fail before mutation with the stand
|
|
|
433
444
|
- **Session state:** Pi appends normal messages and tool results. Pi Workflows does not edit session files.
|
|
434
445
|
- **Other persistent data:** The workflow SQLite shape changes in place and older alpha state requires reset.
|
|
435
446
|
- **Pi internals:** None.
|
|
436
|
-
- **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.
|
|
437
449
|
|
|
438
450
|
## Conformance
|
|
439
451
|
|
|
@@ -452,4 +464,9 @@ The implementation conforms when:
|
|
|
452
464
|
- effects are deduplicated or marked ambiguous;
|
|
453
465
|
- the extension and host run no workflow or controller code in their own event loops;
|
|
454
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;
|
|
455
472
|
- real Pi end-to-end tests, repository checks, reviewer checks, and CI pass.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Workflow step messages
|
|
2
2
|
|
|
3
|
-
This specification defines how pi-workflows shows agent-step instructions in an interactive Pi session. The model receives the complete step prompt, while the user sees a small workflow card that can be expanded.
|
|
3
|
+
This specification defines how pi-workflows shows agent-step instructions in an interactive Pi session. The model receives the complete step prompt, while the user sees a small workflow card that can be expanded. [Restore workflow session delivery and controls](2026-09-01-restore-session-delivery-controls-plan.md) records the delivery and session-control repair.
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
|
|
@@ -48,11 +48,11 @@ pi.sendMessage(
|
|
|
48
48
|
|
|
49
49
|
## Session delivery
|
|
50
50
|
|
|
51
|
-
One shared coordinator delivers step prompts, protected decisions, notifications, and final workflow results. It does not claim or send a new message while Pi is busy or another message is pending. It checks these conditions again after the asynchronous host claim and immediately before the synchronous send. If Pi became busy,
|
|
51
|
+
One shared coordinator delivers step prompts, protected decisions, notifications, and final workflow results. It does not claim or send a new message while Pi is busy or another message is pending. It checks these conditions again after the asynchronous host claim and immediately before the synchronous send. The coordinator remembers the stable delivery ID and exact claim expiry before that final check. If Pi became busy, a later poll can use that same claim while it remains live. Before it sends retained work, it revalidates the exact claim and durable resource through the host, then checks Pi and the lease again. Cancelled, paused, or replaced work is discarded. An expired unused claim is also discarded.
|
|
52
52
|
|
|
53
|
-
The coordinator clears the queued ID only after it observes the custom message in the active branch and saves the public Pi session entry ID through the workflow host. If Pi
|
|
53
|
+
Before the coordinator calls `pi.sendMessage()`, it records the delivery in a process-local queued map. The one-second poll can look for the matching session entry, but it cannot send that ID again. The coordinator clears the queued ID only after it observes the custom message in the active branch and saves the public Pi session entry ID through the workflow host. If Pi does not expose that entry after the confirmation interval, or if saving its receipt fails, the coordinator reports an ambiguous delivery and keeps it blocked.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Reload and restart recovery first search the branch for that stable ID. An existing entry is adopted. A new message is sent only when no entry exists and the host grants a new claim. The host does not grant a second live presentation claim. Polling treats another live claim as unavailable work rather than a workflow error.
|
|
56
56
|
|
|
57
57
|
## Engine boundary
|
|
58
58
|
|
package/docs/development.md
CHANGED
|
@@ -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
|
-
|
|
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.
|