pi-crew 0.11.6 → 0.11.8
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/.githooks/pre-commit +22 -0
- package/.githooks/pre-push +56 -0
- package/CHANGELOG.md +216 -1
- package/dist/index.mjs +531 -508
- package/docs/publishing.md +29 -0
- package/index.ts +3 -3
- package/package.json +9 -6
- package/scripts/install-hooks.mjs +32 -0
- package/skills/real-test-pi-crew/SKILL.md +31 -18
- package/src/config/config-validation.ts +35 -0
- package/src/config/env-vars.ts +38 -0
- package/src/config/types.ts +71 -0
- package/src/extension/registration/foreground-run-controller.ts +19 -10
- package/src/extension/registration/lifecycle-handlers.ts +6 -491
- package/src/extension/registration/lifecycle.ts +7 -90
- package/src/extension/registration/render-loop.ts +502 -0
- package/src/extension/team-tool/doctor.ts +6 -0
- package/src/prompt/prompt-runtime.ts +33 -4
- package/src/runtime/child-pi/child-pi-spawn.ts +14 -0
- package/src/runtime/child-pi/child-pi.ts +130 -6
- package/src/runtime/child-pi/pi-version.ts +187 -0
- package/src/runtime/child-pi/session-recovery.ts +334 -0
- package/src/runtime/classifier/classifier-service.ts +298 -0
- package/src/runtime/dispatch-batch.ts +4 -0
- package/src/runtime/live-session/live-session-runtime.ts +31 -0
- package/src/runtime/model/model-scope.ts +141 -21
- package/src/runtime/model/pi-args.ts +166 -6
- package/src/runtime/peer-dep.ts +24 -0
- package/src/runtime/process/zombie-scanner.ts +168 -7
- package/src/runtime/rpc/README.md +139 -0
- package/src/runtime/rpc/frame-client.ts +328 -0
- package/src/runtime/rpc/rpc-worker.ts +271 -0
- package/src/runtime/rpc/ui-request-policy.ts +112 -0
- package/src/runtime/run-worker.ts +91 -3
- package/src/runtime/scheduling/run-coalesced-task-group.ts +3 -0
- package/src/runtime/skill-instructions.ts +66 -1
- package/src/runtime/surface/resolve-surface.ts +34 -19
- package/src/runtime/surface/surface-spawn.ts +8 -1
- package/src/runtime/task-output-context.ts +191 -32
- package/src/runtime/task-packet.ts +3 -0
- package/src/runtime/task-runner/child-executor.ts +172 -2
- package/src/runtime/task-runner/post-execution.ts +5 -3
- package/src/runtime/task-runner/pre-execution.ts +32 -1
- package/src/runtime/task-runner/prompt-builder.ts +10 -2
- package/src/runtime/task-runner.ts +6 -0
- package/src/schema/config-schema.ts +37 -0
- package/src/schema/team-tool-schema.ts +14 -2
- package/src/state/contracts.ts +5 -0
- package/src/ui/format-helpers.ts +11 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +15 -1
- package/src/ui/pi-ui-compat.ts +18 -9
- package/src/ui/rail.ts +14 -1
- package/src/ui/settings-overlay.ts +11 -2
- package/src/ui/widget/index.ts +0 -1
- package/src/ui/widget/task-list.ts +10 -3
- package/src/ui/widget/widget-renderer.ts +53 -4
- package/src/utils/relative-time.ts +11 -3
- package/src/utils/visual.ts +32 -2
|
@@ -18,6 +18,22 @@
|
|
|
18
18
|
* is no longer alive (parent crashed/exited without reaping the child). A sub-agent
|
|
19
19
|
* whose parent is still running is NOT a zombie — it's a legitimate in-flight task.
|
|
20
20
|
*
|
|
21
|
+
* R3-16 (2026-10-04): a SECOND class — "foreign pi processes" — is detected via
|
|
22
|
+
* the CLI-entry markers `AI_AGENT=pi` / `PI_CODING_AGENT=true` (SDK docs
|
|
23
|
+
* environment-variables.md:15-16: both are set at runtime by the CLI/RPC entry
|
|
24
|
+
* points and inherited by children). Because they are set POST-exec, they show
|
|
25
|
+
* up in a process's own frozen /proc/<pid>/environ ONLY when that process was
|
|
26
|
+
* spawned BY another pi process — a top-level interactive main session's
|
|
27
|
+
* at-exec environ never carries them. Discriminator hierarchy:
|
|
28
|
+
* 1. `PI_CREW_KIND=subagent` → crew worker (zombies/live arrays, authoritative);
|
|
29
|
+
* 2. markers + pi-looking argv + not self/not an ancestor → foreign, REPORT ONLY.
|
|
30
|
+
* The argv gate matters: every shell/tool/MCP-server process pi spawns INHERITS
|
|
31
|
+
* the markers (uv/python/bash/xclip …), so marker presence alone over-matches.
|
|
32
|
+
* Residual ambiguity: a pi the USER deliberately starts from inside a pi shell
|
|
33
|
+
* tool (nested pi) inherits the markers too and is indistinguishable from a
|
|
34
|
+
* leak. No 100%-reliable discriminator exists for that residue → foreign
|
|
35
|
+
* entries are a WARN listing with caveat, NEVER offered as kill candidates.
|
|
36
|
+
*
|
|
21
37
|
* This module is READ-ONLY. It never kills anything. The caller (doctor --zombies)
|
|
22
38
|
* prints the list and asks for explicit confirmation before any kill.
|
|
23
39
|
*/
|
|
@@ -51,10 +67,29 @@ export interface ZombieScanResult {
|
|
|
51
67
|
zombies: ZombieSubagent[];
|
|
52
68
|
/** Sub-agents whose parent is still alive — shown for transparency, never killed. */
|
|
53
69
|
live: ZombieSubagent[];
|
|
70
|
+
/** R3-16: non-crew pi processes carrying the CLI markers
|
|
71
|
+
* (AI_AGENT=pi / PI_CODING_AGENT=true) with a pi-looking argv. REPORT ONLY —
|
|
72
|
+
* never a kill candidate (a user-started nested pi is indistinguishable
|
|
73
|
+
* from a leaked one; see module header). */
|
|
74
|
+
foreign: ForeignPiProcess[];
|
|
54
75
|
/** Errors encountered while scanning (per-pid). Never aborts the whole scan. */
|
|
55
76
|
errors: string[];
|
|
56
77
|
}
|
|
57
78
|
|
|
79
|
+
export interface ForeignPiProcess {
|
|
80
|
+
pid: number;
|
|
81
|
+
ppid: number;
|
|
82
|
+
/** Parent dead or re-parented to init (ppid=1) — strongest available orphan signal. */
|
|
83
|
+
orphaned: boolean;
|
|
84
|
+
/** argv carries headless markers (`--mode json` / `-p` / `--print`) — an
|
|
85
|
+
* interactive TUI session never does; combined with orphaned this is the
|
|
86
|
+
* highest-confidence "likely leak" pair, still WARN-only. */
|
|
87
|
+
headless: boolean;
|
|
88
|
+
rssKb: number;
|
|
89
|
+
elapsedSec: number | undefined;
|
|
90
|
+
cmd: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
/** Read /proc/<pid>/environ as a key=value record. Returns {} if unreadable. */
|
|
59
94
|
function readProcEnviron(pid: number): Record<string, string> {
|
|
60
95
|
try {
|
|
@@ -121,12 +156,17 @@ function isPidAlive(pid: number): boolean {
|
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
function readProcCmdline(pid: number): string {
|
|
159
|
+
return readProcCmdlineTokens(pid).join(" ").trim() || `pid ${pid}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** /proc/<pid>/cmdline as argv tokens (NUL-split). Empty array if unreadable. */
|
|
163
|
+
function readProcCmdlineTokens(pid: number): string[] {
|
|
124
164
|
try {
|
|
125
165
|
// /proc/<pid>/cmdline is NUL-separated argv.
|
|
126
166
|
const raw = fs.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
127
|
-
return raw.split("\0").filter(Boolean)
|
|
167
|
+
return raw.split("\0").filter(Boolean);
|
|
128
168
|
} catch {
|
|
129
|
-
return
|
|
169
|
+
return [];
|
|
130
170
|
}
|
|
131
171
|
}
|
|
132
172
|
|
|
@@ -169,13 +209,16 @@ function listCandidatePids(): number[] {
|
|
|
169
209
|
* has no side effects.
|
|
170
210
|
*/
|
|
171
211
|
export function scanZombieSubagents(): ZombieScanResult {
|
|
172
|
-
const result: ZombieScanResult = { zombies: [], live: [], errors: [] };
|
|
212
|
+
const result: ZombieScanResult = { zombies: [], live: [], foreign: [], errors: [] };
|
|
173
213
|
if (process.platform !== "linux") {
|
|
174
214
|
result.errors.push("zombie scan is Linux-only (/proc required); skipping on " + process.platform);
|
|
175
215
|
return result;
|
|
176
216
|
}
|
|
177
217
|
|
|
178
218
|
const myUid = tryGetUid();
|
|
219
|
+
// R3-16: self + ancestors are live hosting sessions (main session / crew
|
|
220
|
+
// host chain) — never reportable as foreign, even when they carry markers.
|
|
221
|
+
const ancestors = collectAncestorPids();
|
|
179
222
|
for (const pid of listCandidatePids()) {
|
|
180
223
|
try {
|
|
181
224
|
// Cheap rejection first: only inspect processes we own (avoid scanning system procs).
|
|
@@ -185,7 +228,33 @@ export function scanZombieSubagents(): ZombieScanResult {
|
|
|
185
228
|
// AUTHORITATIVE GATE: a process is a pi-crew sub-agent ONLY if it carries
|
|
186
229
|
// PI_CREW_KIND=subagent. The user's main session never sets this, so it can
|
|
187
230
|
// never be matched — this is the fix for accidentally killing main sessions.
|
|
188
|
-
|
|
231
|
+
// R3-16: non-crew processes get ONE extra look — the foreign pi branch
|
|
232
|
+
// below (markers + pi-looking argv, REPORT ONLY); all else is invisible.
|
|
233
|
+
if (environ.PI_CREW_KIND !== "subagent") {
|
|
234
|
+
// Exact value match matters: other agents reuse the AI_AGENT name with
|
|
235
|
+
// their own value (claude-code sets AI_AGENT=claude-code_…_agent), and
|
|
236
|
+
// marker INHERITORS (shells, MCP servers, xclip) are filtered by the
|
|
237
|
+
// argv gate below. Crew workers never reach here — the gate above owns
|
|
238
|
+
// them (pi-crew also scrubs these markers from worker env).
|
|
239
|
+
const hasPiMarker = environ.AI_AGENT === "pi" || environ.PI_CODING_AGENT === "true";
|
|
240
|
+
if (hasPiMarker && pid !== process.pid && !ancestors.has(pid)) {
|
|
241
|
+
const tokens = readProcCmdlineTokens(pid);
|
|
242
|
+
if (looksLikePiBinary(tokens)) {
|
|
243
|
+
const foreignStat = readProcStat(pid);
|
|
244
|
+
const ppid = foreignStat?.ppid ?? 0;
|
|
245
|
+
result.foreign.push({
|
|
246
|
+
pid,
|
|
247
|
+
ppid,
|
|
248
|
+
orphaned: ppid <= 1 || !isPidAlive(ppid),
|
|
249
|
+
headless: isHeadlessArgv(tokens),
|
|
250
|
+
rssKb: readProcRssKb(pid),
|
|
251
|
+
elapsedSec: foreignStat?.elapsedSec,
|
|
252
|
+
cmd: readProcCmdline(pid),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
189
258
|
|
|
190
259
|
const crewParentPid = Number.parseInt(environ.PI_CREW_PARENT_PID ?? "", 10);
|
|
191
260
|
const stat = readProcStat(pid);
|
|
@@ -216,9 +285,11 @@ export function scanZombieSubagents(): ZombieScanResult {
|
|
|
216
285
|
}
|
|
217
286
|
}
|
|
218
287
|
|
|
219
|
-
// Sort: zombies first by descending RSS (biggest leaks first), live by pid
|
|
288
|
+
// Sort: zombies first by descending RSS (biggest leaks first), live by pid;
|
|
289
|
+
// foreign: orphans first (the likely leaks), then by descending RSS.
|
|
220
290
|
result.zombies.sort((a, b) => b.rssKb - a.rssKb);
|
|
221
291
|
result.live.sort((a, b) => a.pid - b.pid);
|
|
292
|
+
result.foreign.sort((a, b) => Number(b.orphaned) - Number(a.orphaned) || b.rssKb - a.rssKb);
|
|
222
293
|
return result;
|
|
223
294
|
}
|
|
224
295
|
|
|
@@ -241,6 +312,63 @@ function getProcUid(pid: number): number | undefined {
|
|
|
241
312
|
}
|
|
242
313
|
}
|
|
243
314
|
|
|
315
|
+
function pathBasename(p: string): string {
|
|
316
|
+
const idx = p.lastIndexOf("/");
|
|
317
|
+
return idx === -1 ? p : p.slice(idx + 1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* R3-16: does the argv look like the pi CLI binary itself?
|
|
322
|
+
*
|
|
323
|
+
* Every process pi spawns (bash, uv, python MCP servers, xclip, …) INHERITS the
|
|
324
|
+
* AI_AGENT/PI_CODING_AGENT markers, so marker presence alone over-matches the
|
|
325
|
+
* user's own tool processes. Only pi-looking argv[0..1] counts (argv[0] itself,
|
|
326
|
+
* or a node-style script path at argv[1]): basename `pi` (covers
|
|
327
|
+
* /usr/local/bin/pi, .nvm/…/bin/pi, ./pi), `pi-coding-agent`, or a path
|
|
328
|
+
* containing pi-coding-agent (node …/pi-coding-agent/dist/…). Tokens deeper
|
|
329
|
+
* than argv[1] are ARGUMENTS (e.g. `bash -c "pi …"`) — never the binary.
|
|
330
|
+
*/
|
|
331
|
+
function looksLikePiBinary(tokens: string[]): boolean {
|
|
332
|
+
for (const token of tokens.slice(0, 2)) {
|
|
333
|
+
const base = pathBasename(token);
|
|
334
|
+
if (base === "pi" || base === "pi-coding-agent" || base === "pi.js") return true;
|
|
335
|
+
if (token.includes("pi-coding-agent")) return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** R3-16: headless argv detection — an interactive TUI session never carries these. */
|
|
341
|
+
function isHeadlessArgv(tokens: string[]): boolean {
|
|
342
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
343
|
+
const token = tokens[i] ?? "";
|
|
344
|
+
if (token === "-p" || token === "--print" || token === "--mode=json") return true;
|
|
345
|
+
if (token === "--mode" && tokens[i + 1] === "json") return true;
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* R3-16: the scanner's own ancestor chain (self's parent up to init).
|
|
352
|
+
*
|
|
353
|
+
* Any pi process on this chain is a LIVE hosting session (the user's main
|
|
354
|
+
* session / a crew host) and must never be reported — even in the nested case
|
|
355
|
+
* (worker runs doctor → its parent crew host and the user's main session above
|
|
356
|
+
* it sit on this chain and carry markers when pi-in-pi). Bounded at 64 hops to
|
|
357
|
+
* survive a hypothetical pid cycle.
|
|
358
|
+
*/
|
|
359
|
+
function collectAncestorPids(): Set<number> {
|
|
360
|
+
const chain = new Set<number>();
|
|
361
|
+
let current = process.ppid;
|
|
362
|
+
let guard = 0;
|
|
363
|
+
while (current > 1 && guard++ < 64) {
|
|
364
|
+
chain.add(current);
|
|
365
|
+
const stat = readProcStat(current);
|
|
366
|
+
if (!stat || stat.ppid <= 0) break;
|
|
367
|
+
current = stat.ppid;
|
|
368
|
+
}
|
|
369
|
+
return chain;
|
|
370
|
+
}
|
|
371
|
+
|
|
244
372
|
/**
|
|
245
373
|
* Render a ZombieScanResult as human-readable text for the doctor report.
|
|
246
374
|
* Explicitly labels main-session safety and never suggests killing live parents.
|
|
@@ -252,7 +380,7 @@ export function formatZombieReport(scan: ZombieScanResult): string {
|
|
|
252
380
|
lines.push(`Sub-agents identified by PI_CREW_KIND=subagent marker. Main sessions (no marker) are never listed.`);
|
|
253
381
|
lines.push("");
|
|
254
382
|
|
|
255
|
-
if (scan.zombies.length === 0 && scan.live.length === 0) {
|
|
383
|
+
if (scan.zombies.length === 0 && scan.live.length === 0 && scan.foreign.length === 0) {
|
|
256
384
|
lines.push("No pi-crew sub-agent processes found.");
|
|
257
385
|
if (scan.errors.length > 0) {
|
|
258
386
|
lines.push("");
|
|
@@ -284,6 +412,23 @@ export function formatZombieReport(scan: ZombieScanResult): string {
|
|
|
284
412
|
lines.push("");
|
|
285
413
|
}
|
|
286
414
|
|
|
415
|
+
if (scan.foreign.length > 0) {
|
|
416
|
+
lines.push(`### WARN — Foreign pi processes, REPORT ONLY (${scan.foreign.length})`);
|
|
417
|
+
lines.push("CLI-spawned pi processes (AI_AGENT=pi / PI_CODING_AGENT=true markers) that are");
|
|
418
|
+
lines.push("NOT pi-crew children. A top-level interactive session never carries these");
|
|
419
|
+
lines.push("markers in its own environment (the CLI sets them post-exec) — but a pi YOU");
|
|
420
|
+
lines.push("deliberately started from inside a pi shell looks identical to a leak.");
|
|
421
|
+
lines.push("NO reliable discriminator exists for that residue: verify cmd / session-dir");
|
|
422
|
+
lines.push("/ cwd by hand before ever killing one. These are never kill candidates and");
|
|
423
|
+
lines.push("never feed pane cleanup. This tool never kills.");
|
|
424
|
+
lines.push("");
|
|
425
|
+
lines.push(" PID PPID STATE RSS CMD");
|
|
426
|
+
for (const f of scan.foreign) {
|
|
427
|
+
lines.push(formatForeignRow(f));
|
|
428
|
+
}
|
|
429
|
+
lines.push("");
|
|
430
|
+
}
|
|
431
|
+
|
|
287
432
|
if (scan.errors.length > 0) {
|
|
288
433
|
lines.push(`Scan errors (${scan.errors.length}, first 5 shown):`);
|
|
289
434
|
for (const err of scan.errors.slice(0, 5)) lines.push(` - ${err}`);
|
|
@@ -301,6 +446,14 @@ function formatZombieRow(z: ZombieSubagent): string {
|
|
|
301
446
|
return ` ${String(z.pid).padEnd(9)}${String(z.crewParentPid).padEnd(8)}${formatRss(z.rssKb).padEnd(10)}${(z.role ?? "?").padEnd(14)}${surface.padEnd(16)}${z.cmd.slice(0, 44)}`;
|
|
302
447
|
}
|
|
303
448
|
|
|
449
|
+
/** One table row for the R3-16 foreign section — state carries the triage flags. */
|
|
450
|
+
function formatForeignRow(f: ForeignPiProcess): string {
|
|
451
|
+
// "orphaned" = parent dead/re-parented to init (strongest leak signal);
|
|
452
|
+
// "headless" = --mode json / -p argv (never an interactive TUI session).
|
|
453
|
+
const state = `${f.orphaned ? "orphaned" : "live-parent"}${f.headless ? " headless" : ""}`;
|
|
454
|
+
return ` ${String(f.pid).padEnd(9)}${String(f.ppid).padEnd(8)}${state.padEnd(17)}${formatRss(f.rssKb).padEnd(10)}${f.cmd.slice(0, 40)}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
304
457
|
function formatRss(kb: number): string {
|
|
305
458
|
if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(1)}G`;
|
|
306
459
|
if (kb >= 1024) return `${(kb / 1024).toFixed(0)}M`;
|
|
@@ -308,4 +461,12 @@ function formatRss(kb: number): string {
|
|
|
308
461
|
}
|
|
309
462
|
|
|
310
463
|
// Re-export for tests + callers that want to inspect proc helpers in isolation.
|
|
311
|
-
export const __test = {
|
|
464
|
+
export const __test = {
|
|
465
|
+
readProcEnviron,
|
|
466
|
+
isPidAlive,
|
|
467
|
+
computeElapsedSec,
|
|
468
|
+
readProcCmdlineTokens,
|
|
469
|
+
looksLikePiBinary,
|
|
470
|
+
isHeadlessArgv,
|
|
471
|
+
collectAncestorPids,
|
|
472
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# RPC worker transport — W7 (P2-3) EXPERIMENTAL
|
|
2
|
+
|
|
3
|
+
Opt-in behind `PI_CREW_WORKER_TRANSPORT=rpc`. **WIRED at the run-worker seam as
|
|
4
|
+
of 2026-10-04** (integration phase, lane D6) after the live-fire probe below —
|
|
5
|
+
previously the seam returned a structured not-implemented result. The
|
|
6
|
+
production stdio child-pi path (`child-pi.ts`) is untouched for the default
|
|
7
|
+
transport; the rpc path carries a REDUCED input (see Limitations).
|
|
8
|
+
|
|
9
|
+
## What this is
|
|
10
|
+
|
|
11
|
+
A JSONL frame client that speaks the pi SDK's RPC mode protocol
|
|
12
|
+
(`pi --mode rpc`) on stdin/stdout, implementing the two Round-2 design gates
|
|
13
|
+
the adoption review (§R2.5) proved are mandatory for any RPC transport:
|
|
14
|
+
|
|
15
|
+
1. **`extension_ui_request` drain policy** (`ui-request-policy.ts`) —
|
|
16
|
+
fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`,
|
|
17
|
+
`set_editor_text`) are counted per method and dropped. Round-2 live
|
|
18
|
+
evidence: 15 of the first 16 records a worker emits belong to this family.
|
|
19
|
+
2. **Dialog auto-answer policy** (`cancel` | `block`) — dialog methods
|
|
20
|
+
(`select`/`confirm`/`input`/`editor`) park a server-side promise that NEVER
|
|
21
|
+
resolves if unanswered (ask-over-RPC deadlock, R2.5). `cancel` (default)
|
|
22
|
+
answers the universal safe answer `{type:"extension_ui_response", id,
|
|
23
|
+
cancelled:true}` (select/input → undefined, confirm → false). `block`
|
|
24
|
+
leaves it pending on purpose (debug). **There is no auto-confirm mode —
|
|
25
|
+
deliberate security decision.** An unattended transport must never say
|
|
26
|
+
"yes" for the user.
|
|
27
|
+
|
|
28
|
+
Counters (fake-stream asserted): `uiRequestsDrained`, `drainedByMethod`,
|
|
29
|
+
`dialogsCancelled`, `dialogsBlocked`, `malformedFrames`, `responsesMatched`,
|
|
30
|
+
`responsesUnmatched`.
|
|
31
|
+
|
|
32
|
+
## Deviation from "use SDK RpcClient" — recorded, evidence-based
|
|
33
|
+
|
|
34
|
+
The task's original phrasing said "transport dùng SDK RpcClient/runRpcMode".
|
|
35
|
+
The verified recon packet (adaptive-07) proved `RpcClient` unusable for this
|
|
36
|
+
prototype and the leader routed the packet through; deviation accepted per
|
|
37
|
+
packet §C:
|
|
38
|
+
|
|
39
|
+
1. **No public API to answer `extension_ui_request`** — `send`/`process` are
|
|
40
|
+
private; non-response records go to event listeners only. Gate 2 would
|
|
41
|
+
require bracket-accessing private members.
|
|
42
|
+
2. **Version skew** — `RpcClient.start()` spawns `node <cliPath ?? "dist/cli.js">`
|
|
43
|
+
cwd-relative, NOT the host pi binary the stdio transport resolves
|
|
44
|
+
(`getPiSpawnCommand` / `PI_TEAMS_PI_BIN`).
|
|
45
|
+
3. **Not injectable** — the W7 test requirement ("fake RPC stream, no real
|
|
46
|
+
spawn") is impossible against `RpcClient`.
|
|
47
|
+
|
|
48
|
+
`runRpcMode` is a server-side embed API — inapplicable (pi-crew is a spawner,
|
|
49
|
+
not an embedder).
|
|
50
|
+
|
|
51
|
+
So: **own strict-LF frame loop over the SDK's exported protocol TYPES**
|
|
52
|
+
(`RpcCommand` / `RpcResponse` / `RpcExtensionUIRequest` /
|
|
53
|
+
`RpcExtensionUIResponse` — all imported from `@earendil-works/pi-coding-agent`).
|
|
54
|
+
The framing rule follows the SDK's own `modes/rpc/jsonl.d.ts` spec: split on
|
|
55
|
+
`\n` ONLY. **Node readline is unsafe here** — it also splits on U+2028/U+2029,
|
|
56
|
+
which are legal inside JSON strings. `serializeJsonLine` /
|
|
57
|
+
`attachJsonlLineReader` are not exported top-level, hence the ~15-line
|
|
58
|
+
hand-rolled splitter.
|
|
59
|
+
|
|
60
|
+
## Config & env
|
|
61
|
+
|
|
62
|
+
- `runtime.workerTransport: "stdio" (default) | "rpc"` — declared in
|
|
63
|
+
`src/schema/config-schema.ts` + `src/config/types.ts`. **Config→seam plumbing
|
|
64
|
+
is NOT wired yet** (needs a `parseRuntimeConfig` line in
|
|
65
|
+
`config-validation.ts`, which is outside this lane's file ownership);
|
|
66
|
+
the live gate is env `PI_CREW_WORKER_TRANSPORT` (`runWorker` has no config
|
|
67
|
+
access — packet §A).
|
|
68
|
+
- `PI_CREW_WORKER_TRANSPORT=rpc` — selects the transport at the
|
|
69
|
+
`run-worker.ts` seam. WIRED 2026-10-04: calls `runRpcWorker` for real
|
|
70
|
+
(worker-cap slot semantics identical to stdio; early transport failure →
|
|
71
|
+
one structured-warn fallback to the stdio path inside the same slot).
|
|
72
|
+
- `PI_CREW_RPC_DIALOG_ANSWER=cancel|block` — GATE 2 policy (default `cancel`,
|
|
73
|
+
invalid fails safe to `cancel`).
|
|
74
|
+
|
|
75
|
+
## Why the seam was not wired before 2026-10-04
|
|
76
|
+
|
|
77
|
+
The original lane was not allowed to live-spawn pi, so a live-wired seam would
|
|
78
|
+
have shipped an end-to-end path that NO test could validate — one env var away
|
|
79
|
+
from silently degrading real runs. The integration phase (this lane) ran the
|
|
80
|
+
live-fire probe FIRST and only then flipped the seam.
|
|
81
|
+
|
|
82
|
+
## Live-fire probe 2026-10-04 — GREEN → seam wired
|
|
83
|
+
|
|
84
|
+
Probe setup: real `pi --mode rpc --no-session` spawned by the unmodified
|
|
85
|
+
frame-client default spawner (`getPiSpawnCommand`), scripts importing directly
|
|
86
|
+
from `src/runtime/rpc/` via `node --experimental-strip-types`, PI_CREW_* /
|
|
87
|
+
PI_TEAMS_* env scrubbed, isolated cwd `/tmp/rpc-lf/`, `timeout -k 5 120` per
|
|
88
|
+
run. Raw artifacts (kept on disk): `/tmp/rpc-lf/probe1{,b}.{log,stdout.txt,stderr.txt}`
|
|
89
|
+
(probe1b = default settings model), `probe2.*` (dialog), `probe3.*` (steer).
|
|
90
|
+
|
|
91
|
+
| # | Item | Result |
|
|
92
|
+
|---|------|--------|
|
|
93
|
+
| a | `extension_ui_request` drain (GATE 1 live) | **24 drained** on probe1b (`setStatus`×20, `setWidget`×3, `notify`×1 — pi-crew widget keys visible in raw frames); 0 malformed, 0 unmatched responses |
|
|
94
|
+
| b | prompt round-trip | ack **24 ms**, `agent_settled` **4.2 s**, `rawFinalText="OK"`, child exit **0**, orderly stdin-end shutdown |
|
|
95
|
+
| c | dialog (confirm) | **executed** (not skipped): a `-e` mini extension opened a real `confirm` → policy `cancel` answered `extension_ui_response {cancelled:true}` → `ui.confirm` resolved `false` → the model literally echoed `"false"`; settled 1.7 s, **no deadlock**, `dialogsCancelled=1` |
|
|
96
|
+
| d | steer mid-turn | `steer` at t+1.6 s → response RTT **53 ms**, disposition **`"queued"`**, final assistant text honored (`"STOP"`), settled 6.5 s, exit 0 |
|
|
97
|
+
|
|
98
|
+
First run (probe1) used `--thinking off` against a reasoning-only model
|
|
99
|
+
(`chiase/deepseek-v4.1-flash`, thinkingLevelMap has only `max`) → the assistant
|
|
100
|
+
message came back `content: []` with usage 0. Not a frame-client defect — the
|
|
101
|
+
turn still completed and settled — but a live argv caveat: **do not pass
|
|
102
|
+
`--thinking off` to reasoning-only models**; the wired seam passes the model
|
|
103
|
+
raw and never emits `--thinking`.
|
|
104
|
+
|
|
105
|
+
### Limitations of the wired rpc path (honest list)
|
|
106
|
+
|
|
107
|
+
- Input mapping is the minimal prototype surface: `cwd`/`task`/`model`/`signal`.
|
|
108
|
+
Agent config, system-prompt files, skills, transcript path, steering file,
|
|
109
|
+
lifecycle/stdout callbacks, `maxTurns`/`graceTurns` remain **stdio-only**.
|
|
110
|
+
- The model string is passed RAW (`--model <model>`), no `applyThinkingSuffix`
|
|
111
|
+
composition — thinking level comes from the settings default.
|
|
112
|
+
- Workers are ephemeral (`--no-session`): no deterministic session identity
|
|
113
|
+
(`--session-id`/`--session-dir`, W2 crash recovery) for rpc yet. Attaching a
|
|
114
|
+
worker to the cwd project's default session would pollute the user's session.
|
|
115
|
+
- `runtime.workerTransport` config key is still NOT plumbed to the seam
|
|
116
|
+
(needs a `parseRuntimeConfig` line in `config-validation.ts`, outside the
|
|
117
|
+
lane's ownership); the env var is the only live gate.
|
|
118
|
+
- Early-failure fallback: only a result with `error` set, NO `rawFinalText`,
|
|
119
|
+
and NOT aborted is retried on stdio (double-execution guard,
|
|
120
|
+
`isEarlyRpcTransportFailure` in run-worker.ts).
|
|
121
|
+
|
|
122
|
+
## Files
|
|
123
|
+
|
|
124
|
+
- `frame-client.ts` — strict-LF JSONL frame client, pending-command map with
|
|
125
|
+
per-command timeout, malformed-frame resilience (count + drop, never throw),
|
|
126
|
+
orderly stop (stdin end → exit → SIGKILL fallback, kill-once guard).
|
|
127
|
+
- `ui-request-policy.ts` — the two design gates + counters.
|
|
128
|
+
- `rpc-worker.ts` — env resolvers (`resolveWorkerTransport`,
|
|
129
|
+
`resolveDialogAnswerPolicy`), the live-fire argv builder
|
|
130
|
+
(`buildRpcWorkerArgv`) + the `runRpcWorker` runner the seam calls.
|
|
131
|
+
|
|
132
|
+
## Production-ready? Honestly: NOT for general agent configs.
|
|
133
|
+
|
|
134
|
+
Live-fire validated for the probe scenarios above (small prompt, one confirm
|
|
135
|
+
dialog, one steer, plain model, default extensions). The stdio transport
|
|
136
|
+
remains the default and the only complete path (agent config, system prompts,
|
|
137
|
+
skills, session identity, retries semantics). Do not enable
|
|
138
|
+
`PI_CREW_WORKER_TRANSPORT=rpc` outside experiments until the Limitations
|
|
139
|
+
list above shrinks.
|