pi-session-mcp 0.11.9 → 0.12.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/.pi-session-mcp-cli/doctor.js +89 -4
- package/.pi-session-mcp-cli/execution-modes.js +16 -4
- package/.pi-session-mcp-cli/override-lock.js +4 -4
- package/.pi-session-mcp-cli/setup-process.js +5 -4
- package/.pi-session-mcp-cli/writer-boundary.js +10 -4
- package/.pi-session-mcp-cli/writer-fence.js +50 -0
- package/.pi-session-mcp-cli/writer-lease.js +140 -0
- package/.pi-session-mcp-cli/writer-lifecycle.js +55 -23
- package/.pi-session-mcp-cli/writer-state.js +4 -2
- package/CHANGELOG.md +69 -0
- package/README.md +3 -2
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +4 -1
- package/dist/child-stdio-transport.d.ts +21 -0
- package/dist/child-stdio-transport.js +88 -0
- package/dist/client-adapters/types.d.ts +1 -0
- package/dist/doctor.d.ts +3 -1
- package/dist/doctor.js +89 -4
- package/dist/execution-modes.d.ts +12 -2
- package/dist/execution-modes.js +16 -4
- package/dist/main.js +9 -2
- package/dist/override-lock.d.ts +2 -0
- package/dist/override-lock.js +4 -4
- package/dist/pi-adapter.d.ts +30 -0
- package/dist/pi-adapter.js +11 -0
- package/dist/refusal-server.d.ts +1 -1
- package/dist/refusal-server.js +24 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +18 -53
- package/dist/session-registry.d.ts +9 -2
- package/dist/session-registry.js +17 -12
- package/dist/setup-cli.js +7 -2
- package/dist/setup-hooks.js +0 -2
- package/dist/setup-modes.js +7 -1
- package/dist/setup-process.d.ts +3 -1
- package/dist/setup-process.js +5 -4
- package/dist/setup-writers.d.ts +5 -0
- package/dist/setup-writers.js +200 -92
- package/dist/tool-schemas.d.ts +339 -0
- package/dist/tool-schemas.js +49 -0
- package/dist/wake-hook.d.ts +4 -5
- package/dist/wake-hook.js +3 -6
- package/dist/writer-acceptance.js +10 -1
- package/dist/writer-boundary.d.ts +6 -4
- package/dist/writer-boundary.js +10 -4
- package/dist/writer-fence.d.ts +12 -0
- package/dist/writer-fence.js +50 -0
- package/dist/writer-launch.d.ts +9 -1
- package/dist/writer-launch.js +127 -30
- package/dist/writer-lease.d.ts +28 -0
- package/dist/writer-lease.js +140 -0
- package/dist/writer-lifecycle.d.ts +7 -1
- package/dist/writer-lifecycle.js +55 -23
- package/dist/writer-relay.d.ts +33 -0
- package/dist/writer-relay.js +359 -0
- package/dist/writer-state.d.ts +13 -1
- package/dist/writer-state.js +4 -2
- package/docs/acceptance.md +47 -0
- package/docs/adr/0012-leased-writer-pool.md +357 -0
- package/docs/architecture.md +61 -13
- package/docs/client-setup.md +11 -15
- package/docs/doctor-design.md +10 -4
- package/docs/execution-modes.md +87 -44
- package/docs/installation.md +34 -22
- package/docs/isolation-setup.md +88 -102
- package/docs/roadmap.md +5 -0
- package/docs/threat-model.md +7 -4
- package/docs/tool-contracts.md +29 -12
- package/package.json +1 -1
- package/scripts/isolation-acceptance.mjs +7 -1
- package/skills/pi-session-implementation/SKILL.md +1 -0
- package/skills/pi-session-orchestration/SKILL.md +6 -5
- package/skills/pi-session-orchestration/references/start-errors.md +5 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# ADR 0012: One registration with a leased writer pool
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted (2026-10-28, maintainer decision) for release 0.12.0;
|
|
4
|
+
implementation tracked in
|
|
5
|
+
[#157](https://github.com/plagor44-ux/pi-session-mcp/issues/157). Amends
|
|
6
|
+
[ADR 0002](0002-embedded-pi-sdk.md) (container sessions are relayed to a Pi
|
|
7
|
+
session in a writer process; host sessions stay embedded),
|
|
8
|
+
[ADR 0008](0008-coding-profile-isolation.md) (adopts its option 4 in container
|
|
9
|
+
form and retires the client registration per writer),
|
|
10
|
+
[ADR 0009](0009-execution-modes.md) (container entries record a default base;
|
|
11
|
+
coding on `container` workspaces runs through the host server) and
|
|
12
|
+
[ADR 0011](0011-turn-events-and-controller-wake.md) (relayed sessions use the
|
|
13
|
+
host spool).
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
Today every container writer is its own client registration,
|
|
18
|
+
`pi-coding-<alias>-w<n>`, for Claude Code and optionally for Codex. Its
|
|
19
|
+
`wrapper.sh` runs the writer launcher (`writer-launch`), which checks state and
|
|
20
|
+
acceptance, projects boundary credentials, refreshes them every 60 s and runs
|
|
21
|
+
`podman run -i --rm …` with a pi-session-mcp server inside the writer's
|
|
22
|
+
container, bound to that writer's full clone. This follows ADR 0008: one
|
|
23
|
+
wrapped registration sees the union of what it mounts, so writers are isolated
|
|
24
|
+
from each other only with one boundary per writer.
|
|
25
|
+
|
|
26
|
+
Observed costs on 2026-10-27, with four writers for each of several
|
|
27
|
+
repositories:
|
|
28
|
+
|
|
29
|
+
- Each client session holds one tool set per writer registration plus the host
|
|
30
|
+
set, and the controller has to pick a writer by tool prefix.
|
|
31
|
+
- Each registration starts its container when the client connects and keeps it
|
|
32
|
+
for the whole client session: 15 idle writer containers were running on one
|
|
33
|
+
workstation.
|
|
34
|
+
- Reset, finish and recreate replace the writer's projected credentials, so the
|
|
35
|
+
running writer server needs an MCP reconnect before the next coding start
|
|
36
|
+
(#136, `writer_reconnect_required`).
|
|
37
|
+
- Nothing stops two client processes from launching the same writer at the
|
|
38
|
+
same time. The writer guard's `writer_busy` counts sessions inside one server
|
|
39
|
+
process only.
|
|
40
|
+
- The wake hook accepts writer tool names only up to `-w16`.
|
|
41
|
+
- Each writer has its own base branch. `--reset --base <branch>` moves a writer
|
|
42
|
+
to another branch, for example for a fix round on collected work, and a
|
|
43
|
+
later `--finish` resets it to that same branch again. Today the controller
|
|
44
|
+
reaches that writer by its tool prefix; a pool that picks any writer at
|
|
45
|
+
baseline would ignore which branch the writer is on.
|
|
46
|
+
|
|
47
|
+
ADR 0008 listed "one sandboxed worker process per session, with the server
|
|
48
|
+
relaying MCP calls" as option 4 and deferred it to its own decision. ADR 0002
|
|
49
|
+
chose embedding to avoid subprocess and RPC translation; that choice stays for
|
|
50
|
+
host sessions.
|
|
51
|
+
|
|
52
|
+
## Decision
|
|
53
|
+
|
|
54
|
+
Coding on a `container` workspace runs through the one host registration,
|
|
55
|
+
`pi-session-mcp`. Per coding session the host server leases a writer of that
|
|
56
|
+
workspace, starts the writer's existing launcher as a child process and relays
|
|
57
|
+
the session to the pi-session-mcp server inside the writer's container.
|
|
58
|
+
|
|
59
|
+
### 1. Transport and launch path
|
|
60
|
+
|
|
61
|
+
- Each client still starts its own host server over stdio. No HTTP, socket,
|
|
62
|
+
daemon or shared process is added.
|
|
63
|
+
- The host server spawns the writer's `wrapper.sh` with stdio pipes and talks
|
|
64
|
+
MCP to the inner server through it. The launcher keeps its role: state and
|
|
65
|
+
acceptance checks, credential projection and the maintainer-approved
|
|
66
|
+
`getAuth` refresh (#142) stay in the launcher process, so the AGENTS.md
|
|
67
|
+
exception is not widened.
|
|
68
|
+
- The launcher gives the session container a fixed name derived from the
|
|
69
|
+
writer name (`pi-writer-<name>`), and every container that touches a writer,
|
|
70
|
+
including setup's throwaway containers, a label `pi-session-mcp.writer=<name>`.
|
|
71
|
+
Podman refuses a second container with the fixed name while one exists.
|
|
72
|
+
|
|
73
|
+
### 2. Fence: a lease held by the guardian process, plus container absence
|
|
74
|
+
|
|
75
|
+
Exclusive use of a writer needs both:
|
|
76
|
+
|
|
77
|
+
- **Lease held by the trusted host-side process that acts on the writer.** For a
|
|
78
|
+
session that is the launcher, not the host server; for setup operations it is
|
|
79
|
+
the setup process. A lock file per writer lives in a host-only lease directory
|
|
80
|
+
(`~/.local/state/pi-session-mcp/leases/<name>.lock`), outside every writer
|
|
81
|
+
root, never deleted, opened without following links and checked for type,
|
|
82
|
+
owner and mode. The launcher takes it with a true non-blocking `flock` as its
|
|
83
|
+
first step and holds it for its whole lifetime: credential warm-up and
|
|
84
|
+
projection, container run, refresh, and cleanup. Setup takes it with a
|
|
85
|
+
bounded wait.
|
|
86
|
+
- **The lease descriptor is inherited by every mutating host-side child.** The
|
|
87
|
+
holder passes the locked descriptor to each child that can change the writer
|
|
88
|
+
(Podman client calls, host Git against the bundles, a nested launcher started
|
|
89
|
+
by acceptance), so the lock's open file description stays locked until the
|
|
90
|
+
last of these processes ends. A killed holder therefore cannot leave a
|
|
91
|
+
pending `podman run` or Git child behind that acts after the next holder has
|
|
92
|
+
taken over. A nested launcher receives the descriptor and its number
|
|
93
|
+
explicitly, verifies that it refers to the lease file (device and inode) and
|
|
94
|
+
holds the lock, and uses it instead of opening a new one; this is how
|
|
95
|
+
acceptance launches the wrapper while setup holds the fence. The descriptor
|
|
96
|
+
is never passed into a container (no `--preserve-fds`); tests must confirm
|
|
97
|
+
that neither the container nor Podman's monitor process holds it.
|
|
98
|
+
- **Container absence.** After taking the lease, and before any credential
|
|
99
|
+
write or clone change, the holder checks through Podman that no container
|
|
100
|
+
with the writer's label exists and that no container mounts the writer's
|
|
101
|
+
clone (this also finds unlabeled containers started by older releases). A
|
|
102
|
+
container found while holding the lease belongs to a dead holder: the holder
|
|
103
|
+
stops it, waits a bounded time until Podman reports it removed, and kills it
|
|
104
|
+
if needed. If absence cannot be confirmed, the writer stays fenced and the
|
|
105
|
+
operation fails.
|
|
106
|
+
|
|
107
|
+
A launcher that outlives its host server keeps the lease until it has stopped
|
|
108
|
+
its container and joined its refresh. Only a killed launcher can leave a
|
|
109
|
+
container behind, and the next holder removes that container before it touches
|
|
110
|
+
the writer.
|
|
111
|
+
|
|
112
|
+
### 3. Every mutating operation takes the fence
|
|
113
|
+
|
|
114
|
+
Coding sessions, `--collect`, `--reset`, `--finish`, `--recreate`, `--remove`,
|
|
115
|
+
acceptance and every provisioning reconciliation or recovery path run under the
|
|
116
|
+
same fence for the whole operation, not after a separate busy check. A setup
|
|
117
|
+
operation that cannot get the fence reports `writer_busy`.
|
|
118
|
+
|
|
119
|
+
### 4. Base selection
|
|
120
|
+
|
|
121
|
+
- The controller selects work by base branch, not by writer. `pi_session_start`
|
|
122
|
+
gains an optional `base`: a short branch name, validated with the same
|
|
123
|
+
safe-ref rules setup applies to `--base`, without `refs/` prefix, commit IDs,
|
|
124
|
+
`HEAD` or path syntax. It is accepted only for coding profiles on `container`
|
|
125
|
+
workspaces; elsewhere the start fails with `base_not_applicable` before
|
|
126
|
+
creating a session.
|
|
127
|
+
- Each container workspace records a **default base** in its
|
|
128
|
+
`execution-modes.json` entry (`base`, a short branch name). Provisioning sets
|
|
129
|
+
it from `--base` or, without one, from the existing resolution
|
|
130
|
+
(`origin/HEAD`, else `main`). A repeated mode request without `--base` keeps
|
|
131
|
+
the recorded value.
|
|
132
|
+
- A start without `base` uses the default base. A writer is a candidate only if
|
|
133
|
+
its recorded `base.ref` equals `refs/heads/<base>`. Matching reads host-side
|
|
134
|
+
writer state only, never the clone, and compares the branch, not the commit:
|
|
135
|
+
the start result reports the writer's base commit, so the controller sees
|
|
136
|
+
which state it got. Selection happens before the lease; once the inner start
|
|
137
|
+
succeeded, the relay reads the writer state again and closes the session and
|
|
138
|
+
moves on if the base changed in between.
|
|
139
|
+
- `--finish` resets a persistent writer, and recreates an ephemeral one, on the
|
|
140
|
+
workspace default base instead of the writer's current base. Moving a writer
|
|
141
|
+
to another branch stays an explicit `--reset --base <branch>`. A writer
|
|
142
|
+
therefore leaves the default pool only for the time between that reset and
|
|
143
|
+
its finish.
|
|
144
|
+
- Doctor warns offline with `writer_off_default_base` and a count when ready
|
|
145
|
+
writers sit on another branch, because they shrink the default pool.
|
|
146
|
+
|
|
147
|
+
The fix-round flow becomes: create a local branch at the collected ref, run
|
|
148
|
+
`--writer <name> --reset --base <branch> --apply`, start with
|
|
149
|
+
`base: "<branch>"`. Default starts, from this or another client, cannot land on
|
|
150
|
+
that writer, and the fix-round start cannot land on a writer at the default
|
|
151
|
+
base.
|
|
152
|
+
|
|
153
|
+
### 5. Allocation
|
|
154
|
+
|
|
155
|
+
- Candidates are the workspace's writers in index order whose state is `ready`,
|
|
156
|
+
whose acceptance is current and whose base matches (section 4). There is no
|
|
157
|
+
host-side pre-filter of the clone's contents.
|
|
158
|
+
- The host server starts the launcher of the first candidate. A launcher that
|
|
159
|
+
cannot take the lease returns `writer_busy`; an inner start that refuses
|
|
160
|
+
because the clone is not at baseline or holds uncollected work also moves on.
|
|
161
|
+
In both cases the host server closes that launcher and tries the next
|
|
162
|
+
candidate. The inner writer guard stays the authority for baseline admission
|
|
163
|
+
inside the boundary.
|
|
164
|
+
- Before the inner start the relay compares the requested profile's entry in
|
|
165
|
+
the writer's validated `pi_capabilities_get` (permission profile, provider,
|
|
166
|
+
model, thinking level) with the host's configured profile and moves on with
|
|
167
|
+
`writer_settings_changed` on any difference, so a writer never runs an
|
|
168
|
+
outdated version of a host profile. The launcher cannot do this check: it
|
|
169
|
+
starts before the profile is known.
|
|
170
|
+
- If no candidate is admitted, the start fails with `writer_unavailable`, a
|
|
171
|
+
content-free reason and content-free counts. `none_ready`: no writer of the
|
|
172
|
+
workspace is ready, migrated and has a recorded acceptance (whether that
|
|
173
|
+
acceptance is still current is decided by the launcher, which refuses a
|
|
174
|
+
stale one). `none_at_base`: ready writers
|
|
175
|
+
exist, none on the requested base. `none_admitted`: candidates existed and
|
|
176
|
+
each refused; the counts `busy`, `notAtBaseline` and `settingsChanged` say
|
|
177
|
+
why.
|
|
178
|
+
- The controller cannot name a writer in this version (see Alternatives).
|
|
179
|
+
|
|
180
|
+
### 6. Start, budgets and uncertain outcomes
|
|
181
|
+
|
|
182
|
+
- A container start has its own end-to-end budget (initial value 60 s, to be
|
|
183
|
+
measured), split into launch and credential warm-up, MCP handshake,
|
|
184
|
+
validated `pi_capabilities_get`, and inner `pi_session_start`. Every step has
|
|
185
|
+
a bound below the remaining budget (cleanup of a refused candidate has its
|
|
186
|
+
own bound on top), and cancellation of the outer start is
|
|
187
|
+
propagated.
|
|
188
|
+
- The launcher's refusal server answers the handshake and lists tools, then
|
|
189
|
+
returns refusal codes. Readiness is therefore decided by the validated
|
|
190
|
+
capabilities and the inner start result, never by exit status or the tool
|
|
191
|
+
list. Inner refusals map to the existing public codes.
|
|
192
|
+
- Relayed prompts get an outer preflight deadline longer than the inner one.
|
|
193
|
+
If the outer side times out or loses the response while the inner turn may
|
|
194
|
+
have been accepted, the relay aborts the inner turn, waits for its terminal
|
|
195
|
+
state within the abort budget and reports the outer prompt as failed. If the
|
|
196
|
+
inner state cannot be determined, the session is marked lost (see 8).
|
|
197
|
+
|
|
198
|
+
### 7. Relay behind `PiSessionAdapter`
|
|
199
|
+
|
|
200
|
+
- The relay is a `PiSessionAdapter`. The host registry keeps owning session
|
|
201
|
+
and turn IDs, the one-running-turn rule, terminal state and the host event
|
|
202
|
+
spool, so the wake hook sees relayed turns like host turns.
|
|
203
|
+
- The relay observes the inner turn with bounded `pi_turn_get` polling inside
|
|
204
|
+
the host process. It advances activity only when the inner `updatedAt`
|
|
205
|
+
changes, and carries the inner `truncated` flag into the outer result
|
|
206
|
+
(`PiTurnOutcome` gains an optional `truncated`).
|
|
207
|
+
- **Abort.** The relay sends `pi_session_abort`, then reads the inner turn and
|
|
208
|
+
settles the outer turn with the inner turn's actual terminal state: a turn
|
|
209
|
+
that completed before the abort stays `completed`. A failed or timed-out
|
|
210
|
+
abort marks the session lost (see 8): further prompts are refused, get and
|
|
211
|
+
close keep working.
|
|
212
|
+
- Inner responses are untrusted data: schema-validated, size-bounded and never
|
|
213
|
+
passed through as raw frames.
|
|
214
|
+
|
|
215
|
+
### 8. Lost sessions and close
|
|
216
|
+
|
|
217
|
+
- If the child exits or the transport breaks, the running turn, if any, ends
|
|
218
|
+
`failed`, and the session becomes lost: further prompts return
|
|
219
|
+
`writer_session_lost`; get and close keep working. This also applies while
|
|
220
|
+
the session is idle.
|
|
221
|
+
- `pi_session_close`, server shutdown and client transport loss close the
|
|
222
|
+
inner session and then the launcher's stdin. The launcher runs in its own
|
|
223
|
+
process group and treats stdin end, `SIGTERM` and `SIGHUP` alike: it stops
|
|
224
|
+
scheduling refreshes, joins an outstanding refresh, stops and removes its
|
|
225
|
+
container, confirms its absence, and only then exits and so releases the
|
|
226
|
+
lease. Its cleanup has its own bound, longer than the longest refresh.
|
|
227
|
+
- The host server does not wait for the full cleanup: close returns once the
|
|
228
|
+
inner session is closed and the launcher has been told to stop. Fencing does
|
|
229
|
+
not depend on the host server, because the launcher still holds the lease. A
|
|
230
|
+
launcher whose cleanup exceeds its bound exits without releasing the
|
|
231
|
+
container check: the next holder finds and removes the container (section 2).
|
|
232
|
+
|
|
233
|
+
### 9. Visibility
|
|
234
|
+
|
|
235
|
+
`pi_session_start` and `pi_session_get` report the leased writer name and its
|
|
236
|
+
base branch and commit for container sessions, so the controller can collect
|
|
237
|
+
that writer's work and see which state the session started from.
|
|
238
|
+
`pi_capabilities_get` reports each container workspace's configured writer
|
|
239
|
+
count and default base, not a free count or the branches of other writers.
|
|
240
|
+
|
|
241
|
+
### 10. Events
|
|
242
|
+
|
|
243
|
+
Relayed sessions emit terminal events through the host registry into the host
|
|
244
|
+
spool. The launcher starts relayed containers without the writer spool mount.
|
|
245
|
+
The wake hook matches the host tool names only.
|
|
246
|
+
|
|
247
|
+
### 11. Setup, migration and acceptance
|
|
248
|
+
|
|
249
|
+
- Container mode no longer registers writer servers with any client.
|
|
250
|
+
- Migration needs a quiescence barrier. The upgrade runbook keeps the existing
|
|
251
|
+
rule to close coding sessions and controllers first, so no launcher of the
|
|
252
|
+
old release is in flight, and the wrapper rebinding below makes every later
|
|
253
|
+
launch, also through a not yet removed writer registration, run the new
|
|
254
|
+
launcher, which takes the lease and checks container absence.
|
|
255
|
+
- The migration step then, per writer and under the fence, confirms that no
|
|
256
|
+
container mounts the clone, removes the `pi-coding-<alias>-w<n>`
|
|
257
|
+
registrations that setup owns, and marks the writer `launch: "relay"` in its
|
|
258
|
+
state. Removal uses the existing ownership-checked unregister path. A
|
|
259
|
+
client's inspection can health-check the registration and so start its
|
|
260
|
+
wrapper, but only while setup holds the writer's lease: that launch then
|
|
261
|
+
serves a `writer_busy` refusal and starts no container. A
|
|
262
|
+
writer whose registration is divergent stays out of the pool and is reported;
|
|
263
|
+
setup never replaces or deletes a divergent registration. Because every
|
|
264
|
+
launch repeats the absence check, a legacy launch that slips through later is
|
|
265
|
+
still fenced.
|
|
266
|
+
- Migration records the workspace default base (section 4). If all writers
|
|
267
|
+
share one base branch, that branch is recorded; otherwise migration requires
|
|
268
|
+
an explicit `--base` and stops with `workspace_base_ambiguous` without it.
|
|
269
|
+
- `wrapper.sh` stays as the launch entry point at its stable path. Wrappers
|
|
270
|
+
embed an absolute release root, so migration rewrites the wrapper of every
|
|
271
|
+
writer it migrates for the new release before the writer is marked
|
|
272
|
+
`launch: "relay"`. A writer held back by settings drift or a divergent
|
|
273
|
+
registration keeps its wrapper unchanged; it stays out of the pool, and its
|
|
274
|
+
retained registration must not be used until the operator resolves it. With
|
|
275
|
+
a release installed at the same path, such a wrapper already runs the new,
|
|
276
|
+
fenced launcher. Acceptance keeps binding the wrapper, release version and
|
|
277
|
+
launch settings. Because the launch arguments
|
|
278
|
+
change (container name, no spool mount for relayed sessions), every writer is
|
|
279
|
+
re-accepted provider-free through the relay launch path before it becomes a
|
|
280
|
+
pool candidate, as on any release upgrade.
|
|
281
|
+
- Verify checks the host registration, Doctor, and for each writer a bounded
|
|
282
|
+
spawn through the relay path with credential refresh suppressed, MCP
|
|
283
|
+
handshake, `tools/list` and validated capabilities, without starting a
|
|
284
|
+
session or using a provider.
|
|
285
|
+
- `writer_reconnect_required` is removed: reset and finish run under the fence
|
|
286
|
+
and no long-lived writer server exists between sessions.
|
|
287
|
+
- Doctor stays offline. It gains checks for the lease directory, for writers
|
|
288
|
+
not yet migrated, for container workspaces without a recorded default base,
|
|
289
|
+
and the `writer_off_default_base` warning (section 4).
|
|
290
|
+
|
|
291
|
+
## Boundary
|
|
292
|
+
|
|
293
|
+
The product boundary stays local stdio, in-memory state and one trusted local
|
|
294
|
+
MCP peer per server process. The writer boundary is unchanged: one container
|
|
295
|
+
and one full clone per writer, the same mounts and exclusions, `.git/config`
|
|
296
|
+
and hooks read-only, and no host Git inside a clone. What changes is who starts
|
|
297
|
+
the launcher (the host server instead of the client) and when (per session
|
|
298
|
+
instead of per client connection). The host server already runs with the
|
|
299
|
+
operator's rights; setup and the launcher already run Podman today.
|
|
300
|
+
|
|
301
|
+
## Consequences
|
|
302
|
+
|
|
303
|
+
- One tool set per client; no per-writer registrations; no idle writer
|
|
304
|
+
containers.
|
|
305
|
+
- A coding start on a container workspace takes seconds longer.
|
|
306
|
+
- No reconnect after reset or finish. Reset, finish and collect refuse while a
|
|
307
|
+
session holds the writer.
|
|
308
|
+
- Two clients cannot use the same writer at the same time, including after a
|
|
309
|
+
crash of the previous holder.
|
|
310
|
+
- Follow-up rounds on collected work keep the branch and reset steps and add
|
|
311
|
+
`base: "<branch>"` to the start (section 4). Default starts never land on a
|
|
312
|
+
writer moved to another branch.
|
|
313
|
+
- `--finish` returns a writer to the workspace default base instead of keeping
|
|
314
|
+
the branch it was last reset to.
|
|
315
|
+
- Contract change: the optional `base` start argument, new error codes
|
|
316
|
+
(`writer_unavailable` with reason and counts, `writer_session_lost`,
|
|
317
|
+
`base_not_applicable`), the writer and base fields in session results, the
|
|
318
|
+
default base in capabilities and an optional `truncated` in the adapter
|
|
319
|
+
outcome. Minor version bump (0.12.0) with re-acceptance of every writer.
|
|
320
|
+
|
|
321
|
+
## Alternatives considered
|
|
322
|
+
|
|
323
|
+
- **Keep one registration per writer and add only the fence.** Fixes the
|
|
324
|
+
concurrent-launch gap, but keeps the tool sets, idle containers and
|
|
325
|
+
reconnects. Rejected as the end state; the fence is reused.
|
|
326
|
+
- **Fewer writers per repository.** Configuration only; reduces but keeps every
|
|
327
|
+
cost. Rejected by the maintainer (2026-10-28).
|
|
328
|
+
- **A shared pool daemon over HTTP or a Unix socket.** Would let clients share
|
|
329
|
+
sessions, but adds a long-lived service, persistence and multi-client trust,
|
|
330
|
+
all outside the product boundary. Not needed for the goal.
|
|
331
|
+
- **Spawn `podman run` from the host server directly.** Moves the credential
|
|
332
|
+
refresh into the host server and widens the AGENTS.md exception, and drops
|
|
333
|
+
the acceptance binding to the wrapper. Rejected in favour of spawning the
|
|
334
|
+
launcher.
|
|
335
|
+
- **Pass the lease descriptor into the container.** Would keep the lock until
|
|
336
|
+
the container ends, but a process inside could release it with `LOCK_UN` on
|
|
337
|
+
the shared open file description. Rejected in favour of a host-side holder.
|
|
338
|
+
- **Hold the lease in the host server.** The host server can die while its
|
|
339
|
+
launcher still writes credentials or starts a container. Rejected in favour of
|
|
340
|
+
the launcher as the holder.
|
|
341
|
+
- **Server picks any writer at baseline, ignoring its base.** Simplest
|
|
342
|
+
contract, but a fix-round start can land on a writer at the default base, and
|
|
343
|
+
an unrelated start can land on a writer just reset to a fix branch, also from
|
|
344
|
+
another client. Rejected.
|
|
345
|
+
- **Controller names the writer.** Deterministic and close to today's flow,
|
|
346
|
+
but capabilities would have to list writer names, a pinned busy writer fails
|
|
347
|
+
instead of falling back, and a start without a name could still land on a
|
|
348
|
+
writer moved to another branch, so base matching would be needed anyway.
|
|
349
|
+
Deferred; a diagnostic writer argument can be added later without breaking
|
|
350
|
+
the contract.
|
|
351
|
+
- **Preferred writer with silent fallback.** Can silently start work from the
|
|
352
|
+
wrong branch. Rejected.
|
|
353
|
+
|
|
354
|
+
## Open questions
|
|
355
|
+
|
|
356
|
+
- Measured start budget on the reference machines.
|
|
357
|
+
- Whether ephemeral writers recreate on close instead of on finish.
|
package/docs/architecture.md
CHANGED
|
@@ -13,9 +13,9 @@ validated config --> capability projection --> McpServer discovery tool
|
|
|
13
13
|
|
|
|
14
14
|
+---------> SessionRegistry --> PiSessionAdapter
|
|
15
15
|
| |
|
|
16
|
-
in-memory state
|
|
17
|
-
session + turn ModelRuntime + AgentSession
|
|
18
|
-
read models
|
|
16
|
+
in-memory state routing adapter
|
|
17
|
+
session + turn host: SDK ModelRuntime + AgentSession
|
|
18
|
+
read models container coding: writer relay
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
There are three separate local CLI paths: the MCP stdio server (`main.ts`),
|
|
@@ -49,6 +49,55 @@ registration.
|
|
|
49
49
|
|
|
50
50
|
`pi_capabilities_get` projects the validated workspace and execution-profile maps plus the immutable configuration metadata captured while loading the file. It returns aliases plus the configured permission/provider/model/thinking intent, the configured extension specs and the selected skill aliases, sorted by alias, `configuration: { fingerprint, reloadPolicy: "restart-required" }` and `events: { delivery: "spool" | "off" }`. The fingerprint is SHA-256 over the exact loaded file bytes, including formatting; it is not a current-file freshness check. The projection makes no provider, authentication, filesystem, network, server-reachability, or catalog-state probe, so it cannot assert availability.
|
|
51
51
|
|
|
52
|
+
## Leased container writers
|
|
53
|
+
|
|
54
|
+
[ADR 0012](adr/0012-leased-writer-pool.md) routes coding on `container`
|
|
55
|
+
workspaces through the one host registration `pi-session-mcp`, without
|
|
56
|
+
per-writer registrations, tool sets or writer selection by prefix. The host
|
|
57
|
+
spawns a writer's stable wrapper per session and relays MCP stdio behind
|
|
58
|
+
`PiSessionAdapter`. Host and read-only sessions stay embedded. Each client
|
|
59
|
+
still owns a separate host process; there is no daemon or shared session store.
|
|
60
|
+
|
|
61
|
+
Optional start `base` is a short branch name, applicable only to container
|
|
62
|
+
coding; elsewhere it fails with `base_not_applicable`. Omission uses the
|
|
63
|
+
workspace default base. Allocation tries ready accepted relay writers in index
|
|
64
|
+
order on that branch, checks the inner profile selection against the host and
|
|
65
|
+
leaves baseline admission to the inner guard. Start and get report `writer`
|
|
66
|
+
and `base: { ref, commit }`; capabilities report configured `writers` and
|
|
67
|
+
`defaultBase`, not free capacity. `writer_unavailable` gives content-free
|
|
68
|
+
reason/counts; transport loss makes the session lost (`writer_session_lost`
|
|
69
|
+
on later prompts), while get and close still work.
|
|
70
|
+
|
|
71
|
+
The launcher is the session lease holder, not the host server. Its host-only
|
|
72
|
+
`~/.local/state/pi-session-mcp/leases/<name>.lock` is opened without following
|
|
73
|
+
links, checked for type/owner/mode, never deleted, and held throughout warm-up,
|
|
74
|
+
credential projection, refresh, container use and cleanup. The directory is
|
|
75
|
+
0700 and file 0600. Setup takes the same lease for every mutating operation,
|
|
76
|
+
including collection, acceptance and reconciliation, with a 10-second wait
|
|
77
|
+
before `writer_busy`. Mutating host children inherit the locked descriptor;
|
|
78
|
+
nested acceptance launchers verify and adopt it. It never enters a container
|
|
79
|
+
or Podman's monitor.
|
|
80
|
+
|
|
81
|
+
After taking the lease, before credential or clone changes, the holder confirms
|
|
82
|
+
absence of containers labeled `pi-session-mcp.writer=<name>` and any container
|
|
83
|
+
mounting the clone, including unlabeled legacy containers. It removes remnants
|
|
84
|
+
of a dead holder and fails closed (`writer_fenced`) if absence is uncertain.
|
|
85
|
+
Session containers use the fixed name `pi-writer-<name>`; setup throwaway
|
|
86
|
+
containers also carry the label. On stdin end, SIGTERM or SIGHUP, the launcher
|
|
87
|
+
stops refresh scheduling, joins outstanding refresh, stops/removes the
|
|
88
|
+
container and checks absence before exiting. Close tells the launcher to stop
|
|
89
|
+
without waiting for all cleanup; its lease still fences the clone. A killed
|
|
90
|
+
launcher leaves cleanup to the next holder's absence check.
|
|
91
|
+
|
|
92
|
+
`--finish` returns writers to the workspace default base. A fix round uses a
|
|
93
|
+
local branch at the collected ref, `--writer <name> --reset --base <branch>
|
|
94
|
+
--apply`, then start with `base: "<branch>"` and collect the returned `writer`.
|
|
95
|
+
No reconnect is needed after reset, finish or recreate. Inner replies are
|
|
96
|
+
validated and bounded; the host owns IDs, turn state and terminal events.
|
|
97
|
+
Relayed turns use bounded inner polling; only inner `updatedAt` changes advance
|
|
98
|
+
activity, and inner truncation survives projection. Abort observes the actual
|
|
99
|
+
inner outcome, so a completed turn remains completed in an abort race.
|
|
100
|
+
|
|
52
101
|
## Deterministic Pi selection
|
|
53
102
|
|
|
54
103
|
The SDK adapter owns one durable `ModelRuntime` for profiles without extensions. (A profile with `extensions` creates a fresh runtime for each session; see [Pi extensions design](pi-extensions-design.md).) For the configured profile, it uses that runtime to call `getProvider(providerId)`, `getModel(providerId, modelId)`, `checkAuth()`, `getAuth(model)`, and `getAvailable(providerId)`. It rejects an absent provider or model, missing or unavailable local authentication, and a model not present in the local available-model set before constructing a session. Authentication uses only Pi's existing local mechanism; auth objects are presence-checked and never retained, serialized, or logged.
|
|
@@ -73,9 +122,9 @@ The registry owns public UUIDs, lifecycle state, the turn read model, background
|
|
|
73
122
|
|
|
74
123
|
`pi_session_prompt` creates a provisional turn identity, invokes Pi preflight, and only after accepted preflight publishes `{session, turn}`; rejected preflight leaves no orphan turn. Completion returns to `idle`; failure is generic. Each active session retains at most 20 turns. A turn is `running`, `completed`, `failed`, or `aborted`; its public view contains IDs, timestamps, and terminal `completedAt`. While a turn runs, its `updatedAt` is the time of the last observed public SDK activity (assistant message and tool execution events, content-free) and is a liveness lower bound only; see [Tool contracts](tool-contracts.md). Completed text is normalized and capped at 64 KiB UTF-8 with Unicode-safe truncation. It can contain model-visible workspace data and remains potentially sensitive.
|
|
75
124
|
|
|
76
|
-
Session metadata contains only whitelisted values: public session ID, workspace alias, execution-profile alias, derived permission `profile`, post-verified provider/model/effective thinking level, lifecycle state, and
|
|
125
|
+
Session metadata contains only whitelisted values: public session ID, workspace alias, execution-profile alias, derived permission `profile`, post-verified provider/model/effective thinking level, lifecycle state, timestamps, and for container sessions the writer and base ref/commit. Capability metadata uses a separate whitelist: server name/version, loaded configuration fingerprint/reload policy, workspace aliases, and configured profile fields, including the selected skill aliases. Neither view contains workspace paths, full model objects, SDK session IDs, auth sources, credential paths, headers, base URLs, prompts, transcripts, or raw provider errors.
|
|
77
126
|
|
|
78
|
-
|
|
127
|
+
Embedded profile-specific authentication/catalog operations and session creation have fixed server-side deadlines of 5,000 ms and 10,000 ms. Relayed container starts have a 60-second budget; their prompt preflight, abort and close cleanup use 15,000 ms, 15,000 ms and 10,000 ms respectively. Embedded prompt preflight, abort and close cleanup, plus registry shutdown and process shutdown, have fixed server-side deadlines of 5,000 ms, 5,000 ms, 5,000 ms, 12,000 ms, and 15,000 ms respectively. A preflight timeout returns `prompt_timeout` without publishing a turn. The unpublished operation remains quarantined until its prompt and abort cannot race with later work; late settlement can release that quarantine but never publish the turn. An abort timeout returns `abort_timeout` without falsely marking the turn aborted; a later normal outcome or successful SDK abort may still terminalize it exactly once. Close marks the session terminal, removes its public registry entry deterministically, and performs best-effort abort/dispose. Repeated close calls return a cached closed view while its tombstone is retained. Each SDK handle is disposed at most once. A late promise cannot revive a removed session or mutate a removed turn.
|
|
79
128
|
|
|
80
129
|
The public SDK does not provide a hard-kill guarantee for a blocked provider/network operation, so a timeout must not be described as graceful provider cancellation. `SIGINT`, `SIGTERM`, and stdin EOF from the MCP client start the same process shutdown. The first trigger wins and sets the exit status (130, 143, or 0). Stdin EOF or the first `SIGINT` or `SIGTERM` during a running shutdown neither restarts it nor changes that status; a signal that arrives a second time keeps Node's default effect and ends the process immediately. Shutdown closes registry entries and the MCP transport best-effort, then explicitly exits the Pi Session MCP process no later than its 15,000 ms deadline. That terminal process policy may cut off still-running external work and does not prove the provider stopped cleanly. Closed tombstones are FIFO-retained up to 100 entries and contain no turn data. All sessions, turns, tombstones, and loaded configuration snapshots disappear on process exit; there is no persistence or reconnect/resume.
|
|
81
130
|
|
|
@@ -94,7 +143,7 @@ Claude Code asyncRewake <-- exit 2 + one line <-- pi-session-mcp-wake <---+
|
|
|
94
143
|
```
|
|
95
144
|
|
|
96
145
|
- **Observer.** `SessionRegistry` calls the optional `onTurnTerminal` listener from the one place that makes a published turn terminal (`notifyTerminal`), once per turn, with session ID, turn ID, state (`completed`, `failed` or `aborted`) and `via`. `via` is `close` when `close` marked a still running turn aborted and `registry` otherwise. A rejected, timed-out or restored preflight never had a public turn and emits nothing. A throwing listener is swallowed; it cannot change registry state or reach a caller.
|
|
97
|
-
- **Spool.** `src/main.ts` opens a `TurnEventSpool` at startup only when delivery is `spool` and writes one `turn_terminal` event per notice. An unusable spool root stops the server with the startup code `event_spool_unavailable`. Without any spool location (no `PI_SESSION_MCP_EVENTS_DIR` and no absolute `XDG_RUNTIME_DIR`, as when Codex starts the server) delivery stays inactive instead: the server starts, `pi_capabilities_get` reports `events.delivery: "off"`, and one `event_delivery_inactive` warning goes to stderr (#113). Later write failures never reach the registry: they are counted, and one sanitized `event_spool_write_failed` diagnostic goes to stderr. An instance keeps only its newest 256 event files (it deletes older ones by name from an in-memory list and never lists the directory), so a long-lived server cannot outgrow the hook's bounded scan.
|
|
146
|
+
- **Spool.** `src/main.ts` opens a `TurnEventSpool` at startup only when delivery is `spool` and writes one `turn_terminal` event per notice. An unusable spool root stops the server with the startup code `event_spool_unavailable`. Without any spool location (no `PI_SESSION_MCP_EVENTS_DIR` and no absolute `XDG_RUNTIME_DIR`, as when Codex starts the server) delivery stays inactive instead: the server starts, `pi_capabilities_get` reports `events.delivery: "off"`, and one `event_delivery_inactive` warning goes to stderr (#113). Later write failures never reach the registry: they are counted, and one sanitized `event_spool_write_failed` diagnostic goes to stderr. An instance keeps only its newest 256 event files (it deletes older ones by name from an in-memory list and never lists the directory), so a long-lived server cannot outgrow the hook's bounded scan.
|
|
98
147
|
- **Event.** One file per event, `<8-digit sequence>-<kind>.json`, at most 1,024 bytes, mode 0600, written to a temporary name and renamed into place. It holds `v`, `seq`, `at`, `instance` and, for `turn_terminal`, `session`, `turn`, `state` and `via`; `instance_ended` holds a `reason` (`eof`, `signal` or `shutdown`) instead. Never a prompt, an answer, a path or an error text. `src/turn-events.ts` owns the contract and its strict parser; the server and the hook program share it.
|
|
99
148
|
- **Hook.** `pi-session-mcp-wake` (`dist/wake-main.js`) is a Claude Code `PostToolUse` command hook with `asyncRewake`. It arms on `pi_session_prompt` and on `pi_turn_get` results that show a running turn, waits for that turn's event, claims it exactly once and exits 2 with one line on stderr, which Claude Code turns into a new model request. After its wait budget it wakes with a different line so that the controller reads once and re-arms. Every other path exits 0 silently.
|
|
100
149
|
- **Controller.** It reads `pi_turn_get` once. An event is a wake trigger and never state: the registry read model stays the only truth, and the controller never acts on a state taken from a wake line.
|
|
@@ -106,20 +155,19 @@ $XDG_RUNTIME_DIR/pi-session-mcp/ runtime tmpfs, cleared at logout
|
|
|
106
155
|
events/ 0700, owned by the operator
|
|
107
156
|
inst-<16 hex>/ one per host server process
|
|
108
157
|
claims/ wake claims, never inside events/
|
|
109
|
-
|
|
110
|
-
~/.local/state/pi-session-mcp/writers/<name>/ on disk, one per container writer
|
|
111
|
-
events/ 0700, mounted at /pi/events
|
|
112
|
-
inst-<16 hex>/ one per server process in that writer
|
|
113
158
|
```
|
|
114
159
|
|
|
115
|
-
A host server uses `events/` itself as its spool root unless `PI_SESSION_MCP_EVENTS_DIR` names an absolute directory
|
|
160
|
+
A host server uses `events/` itself as its spool root unless `PI_SESSION_MCP_EVENTS_DIR` names an absolute directory. The root is created with mode 0700 if missing and must be a real directory of the current user without group or other access. Each server process creates its own instance directory.
|
|
116
161
|
|
|
117
162
|
- **Start.** The instance directory is created when the server starts with delivery enabled.
|
|
118
163
|
- **Stop.** After the registry shutdown has finished, so that the terminal events which `close` generates come first, the spool writes `instance_ended` and removes its instance directory immediately afterwards. A reader therefore normally never sees `instance_ended`: it is a best-effort marker for a reader that is already scanning the directory at that moment, and no waiter relies on it. A waiter ends with a wake, silently on a `via: "close"` event for its turn, when its namespace has no instance directory after a short grace period, when its client process goes away, or at its budget. This runs on stdin EOF, signals, a failed start and the shutdown deadline. A crash or `SIGKILL` leaves a stale instance directory of content-free files; nothing cleans it up automatically, and the hook reads only the 64 newest instance directories of a namespace.
|
|
119
164
|
- **Claims.** The hook creates `claims/<namespace>-<instance>-<sequence>` exclusively; the winner wakes, so one event wakes at most once even with several waiting hooks. A hook that is killed before its wake was delivered releases its claim. Claims older than 24 hours are removed by later hook runs.
|
|
120
|
-
- **Not state.** The server never reads the spool back, nothing of the host spool survives a logout (the runtime directory is a tmpfs on systemd hosts),
|
|
165
|
+
- **Not state.** The server never reads the spool back, nothing of the host spool survives a logout (the runtime directory is a tmpfs on systemd hosts), and no event is replayed. This is the narrow, content-free exception to in-memory-only operation that ADR 0011 records.
|
|
121
166
|
|
|
122
|
-
|
|
167
|
+
Relayed sessions emit events through the host registry and host spool, without
|
|
168
|
+
per-session writer spool mounts or writer hook entries. The wake hook matches
|
|
169
|
+
only `mcp__pi-session-mcp__*`; see [Isolation setup](isolation-setup.md#event-spool-mount).
|
|
170
|
+
Codex has no external wake and keeps polling.
|
|
123
171
|
|
|
124
172
|
## Scope
|
|
125
173
|
|
package/docs/client-setup.md
CHANGED
|
@@ -208,10 +208,10 @@ of the target's scope, after the registration succeeded. Guided steps and limits
|
|
|
208
208
|
|
|
209
209
|
**Where the hook reads.** The hook that a `pi-session-mcp` tool call arms reads only
|
|
210
210
|
the `inst-*` directories directly under `$XDG_RUNTIME_DIR/pi-session-mcp/events`.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
It reads no other location, and
|
|
214
|
-
`$XDG_RUNTIME_DIR/pi-session-mcp/claims`, outside
|
|
211
|
+
Relayed container sessions emit through that host spool too. The hook matches
|
|
212
|
+
only `mcp__pi-session-mcp__*`; writer hook entries and per-session writer spool
|
|
213
|
+
mounts are gone. It reads no other location, and keeps claims and waiter locks
|
|
214
|
+
in `$XDG_RUNTIME_DIR/pi-session-mcp/claims`, outside the spool.
|
|
215
215
|
|
|
216
216
|
`<project>` is the real path of the directory in which setup runs. Claude Code
|
|
217
217
|
has no command for hooks, so setup follows the contract of the settings file,
|
|
@@ -262,17 +262,13 @@ applicable), delivery off while setup holds no entry (`hook_not_enabled`), and
|
|
|
262
262
|
setup report with delivery off and no owned entry therefore has the same
|
|
263
263
|
findings, order and exit code as before the hook existed.
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
writer
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
every project). When in doubt the entry stays. A hook problem while removing a
|
|
273
|
-
writer is reported (`settings_*`, `hook_divergent`) but does not keep the
|
|
274
|
-
writer: its registration and files are removed, and the record keeps the entry
|
|
275
|
-
owned for the next removal. Codex writer registrations get no hook.
|
|
265
|
+
Container provisioning creates no writer client registrations or writer hooks.
|
|
266
|
+
Coding for either client uses the one host `pi-session-mcp` registration and a
|
|
267
|
+
coding profile, not a per-writer tool prefix. `--client codex` has no writer
|
|
268
|
+
effect. Migration removes owned legacy registrations and hook ownership under
|
|
269
|
+
the writer fence; divergent entries are retained and reported, never replaced.
|
|
270
|
+
An owned shared legacy hook can stay while another registration needs it.
|
|
271
|
+
Use the [container upgrade runbook](installation.md#container-writers).
|
|
276
272
|
|
|
277
273
|
Setup never replaces or deletes an entry that it did not write or that was
|
|
278
274
|
edited, and it keeps all other settings. The entry is not part of the MCP
|
package/docs/doctor-design.md
CHANGED
|
@@ -34,10 +34,15 @@ and owners are:
|
|
|
34
34
|
| `workspace_not_writable` | A workspace is not writable while a coding profile can target it | validated workspace alias |
|
|
35
35
|
| `workspace_gitfile` | Warning. A workspace's `.git` is a regular file starting with `gitdir:` while at least one execution profile has the `coding` permission profile | validated workspace alias |
|
|
36
36
|
| `workspace_mode_unset` | Warning. A workspace has mode `unset` while at least one execution profile has the `coding` permission profile | validated workspace alias |
|
|
37
|
+
| `workspace_base_unset` | Warning. A workspace has mode `container` without a recorded default base in its execution-modes entry | validated workspace alias |
|
|
37
38
|
| `workspace_host_writes_accepted` | Always `ok`, without remediation. The operator has explicitly accepted host writes for this workspace | validated workspace alias |
|
|
38
39
|
| `container_runtime_unavailable` | A workspace has mode `container` and the local rootless Podman probe fails | validated workspace alias |
|
|
39
40
|
| `container_profile_refused` | A selected container coding profile cannot be rendered: missing profile, extensions or MCP grants, non-false default, non-coding permission, invalid skills, non-builtin/unshipped/operator-overridden skills, or schema-invalid rendered configuration (error); one ok per workspace when all selected profiles pass; one workspace error saying selected profiles could not be checked if the original configuration cannot be read | workspace alias:profile alias on refusal; workspace alias on success or unreadable configuration |
|
|
41
|
+
| `writer_lease_directory` | Error when `~/.local/state/pi-session-mcp/leases` cannot be inspected, or exists as a symbolic link, is not a real directory, is not owned by the current user, or has a mode other than exactly 0700; an absent directory is not reported. A wrong owner or mode is repaired in place: lease files keep their inodes, so the directory is never moved or deleted while a writer may hold a lease | `writer lease` |
|
|
40
42
|
| `writer_not_ready` | State is missing or invalid, phase is not ready, acceptance fingerprint is stale, or the local image is absent (error) | validated writer name |
|
|
43
|
+
| `writer_not_migrated` | Warning. The writer state still records the legacy launch, so the writer is not in the relay pool | validated writer name |
|
|
44
|
+
| `writer_off_default_base` | Warning. One or more ready writers of a container workspace have a base other than the workspace default base; the remediation states only their count | validated workspace alias |
|
|
45
|
+
| `writer_git_identity_missing` | Clone Git config lacks a readable user name and email (warning); set both in the operator checkout, then reset the writer | validated writer name |
|
|
41
46
|
| `writer_dirty` | HEAD is not the base branch, its commit differs, another branch exists, or Git metadata cannot safely be inspected (warning) | validated writer name |
|
|
42
47
|
| `writer_uncollected_bundle` | Bundles directory is unsafe/unreadable or contains entries other than base.bundle (warning; an absent directory alone does not warn) | validated writer name |
|
|
43
48
|
| `extension_missing` | Pi has no user install of a package that a profile's `extensions` names | `<profile>:<package>` |
|
|
@@ -70,9 +75,10 @@ never Doctor actions. Under Plan B, `--isolation-control --repo <alias> --apply`
|
|
|
70
75
|
writes real canaries below $HOME and removes them; omitting apply only plans.
|
|
71
76
|
Doctor never runs this control, refreshes credentials, inspects client
|
|
72
77
|
registrations or starts containers. In contrast, setup's writer verify checks
|
|
73
|
-
Doctor
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
Doctor and a bounded relay-path MCP handshake/tools/list/capabilities;
|
|
79
|
+
client-target verify checks host registration equality separately. Claude's host
|
|
80
|
+
registration inspection starts no writer container. The direct writer
|
|
81
|
+
verification launch disables credential refresh and starts no session.
|
|
76
82
|
Doctor's metadata-only dirty warning is not the server's stronger coding guard:
|
|
77
83
|
that guard also checks stash and uncommitted/untracked work on every admission.
|
|
78
84
|
See [Execution modes](execution-modes.md#writer-storage-launch-and-readiness).
|
|
@@ -85,7 +91,7 @@ The skill checks run after the extension checks. They look only at file availabi
|
|
|
85
91
|
|
|
86
92
|
`skill_override_outdated` concerns the operator's skill overrides, not a profile. It is `ok` for a recorded base that still matches the shipped skill, and a warning (exit status 1, never blocking `setup`) when the shipped skill changed after the operator ran `pi-session-mcp-setup --customize-skill <name>`; its remediation names `pi-session-mcp-setup --apply --customize-skill <name>` to acknowledge a reviewed change. Only skills with a recorded base are compared, and a recorded name that is no longer shipped is skipped. A recorded base of 0.8.0 (a version 1 fingerprint) is compared through the version 2 value of the released 0.8.0 skill it names; a version 1 base that names no released skill cannot be confirmed and yields a warning that says so, with the same remediation. A corrupt bases file yields a warning with the subject `skill:bases`. Doctor reads the bases file and the shipped skill files only; nothing is executed and the subject is a validated skill name, never a path. The check sorts after `skill_duplicate`.
|
|
87
93
|
|
|
88
|
-
`event_spool_root` is the last check and exists only for a configuration that enables turn-event delivery ([ADR 0011](adr/0011-turn-events-and-controller-wake.md)); with delivery off or unset it is absent from the report. It inspects `$XDG_RUNTIME_DIR/pi-session-mcp` and, when they exist, its `events` and `claims` entries with one `lstat` each, and never creates, repairs or reads anything below them. The rule is the one the server applies to the events root at startup (`TurnEventSpool.open`: a real directory of the current user without group or other access) and the hook applies to the claims directory, so Doctor is not green where startup would fail with `event_spool_unavailable` for these directories. When `PI_SESSION_MCP_EVENTS_DIR` is set in the server's environment the server uses that directory instead of `events`; Doctor does not read it. A missing runtime directory variable is only a warning, because Doctor may run in another environment than the server; the server itself
|
|
94
|
+
`event_spool_root` is the last check and exists only for a configuration that enables turn-event delivery ([ADR 0011](adr/0011-turn-events-and-controller-wake.md)); with delivery off or unset it is absent from the report. It inspects `$XDG_RUNTIME_DIR/pi-session-mcp` and, when they exist, its `events` and `claims` entries with one `lstat` each, and never creates, repairs or reads anything below them. The rule is the one the server applies to the events root at startup (`TurnEventSpool.open`: a real directory of the current user without group or other access) and the hook applies to the claims directory, so Doctor is not green where startup would fail with `event_spool_unavailable` for these directories. When `PI_SESSION_MCP_EVENTS_DIR` is set in the server's environment the server uses that directory instead of `events`; Doctor does not read it. A missing runtime directory variable is only a warning, because Doctor may run in another environment than the server; the server itself keeps delivery inactive when it has neither that directory nor `PI_SESSION_MCP_EVENTS_DIR`, with an `event_delivery_inactive` warning. Relayed container sessions use the host spool; there are no per-session writer spool mounts (see [Isolation setup](isolation-setup.md#event-spool-mount)). The report holds the fixed subject `event spool` and a fixed remediation, never a path.
|
|
89
95
|
|
|
90
96
|
The Doctor reuses `loadConfig`; it does not duplicate alias, profile, thinking
|
|
91
97
|
level, or read-only-default rules. Workspace values are used only for local
|