pi-crew 0.9.62 → 0.9.64

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.
@@ -31,7 +31,7 @@ const createdTempDirs = new Set<string>();
31
31
  * /tmp directory. Uses `userPiRoot()` so the path stays consistent with
32
32
  * the rest of pi-crew (respects PI_TEAMS_HOME / PI_CODING_AGENT_DIR).
33
33
  */
34
- function getPiTempBase(): string {
34
+ export function getPiTempBase(): string {
35
35
  return path.join(userPiRoot(), "tmp");
36
36
  }
37
37
 
@@ -98,7 +98,7 @@ export function readManifestWithTransientRetry(manifestPath: string, maxRetries
98
98
  throw new Error(`unreachable: readManifestWithTransientRetry exhausted for ${manifestPath}`);
99
99
  }
100
100
 
101
- function shouldRecoverTask(task: TeamTaskState, deadMs: number): boolean {
101
+ export function shouldRecoverTask(task: TeamTaskState, deadMs: number): boolean {
102
102
  if (task.status !== "running") return false;
103
103
  if (!task.heartbeat) return true;
104
104
  return task.heartbeat.alive === false || isWorkerHeartbeatStale(task.heartbeat, deadMs);
@@ -0,0 +1,184 @@
1
+ # Spike go/no-go — pi-rlm → Node port (pattern 01+04+05+08+09)
2
+
3
+ > **Kết quả: ✅ GO** — 17/17 test GREEN, spawn subprocess Node thật (không mock, không Bun).
4
+ > Ngày: 2026-08-08. Spec: `../../rlm-apply-pi-crew.md` mục 5 + 7.1.
5
+
6
+ ## Mục đích
7
+
8
+ Chứng minh 8 pattern FLAGSHIP của pi-rlm port sang Node thuần chạy được TRƯỚC khi đầu tư full FLAGSHIP. 2 invariant quyết định:
9
+
10
+ - **(a) Bindings survive mid-cell failure** (pattern 05): cell 1 `throw` giữa chừng → cell sau vẫn thấy biến đã gán trước throw.
11
+ - **(b) Namespace revives across process boundary** (pattern 08+09): engine 1 snapshot → kill → engine 2 (process mới) restore → cell mới đọc được biến.
12
+
13
+ ## Cấu trúc
14
+
15
+ ```
16
+ src/runtime/scratchpad/
17
+ ├── protocol.ts (88 dòng) — fd3 + nonce + envelope, port 1:1 từ pi-rlm
18
+ ├── transform.ts (363 dòng) — esbuild transformSync (strip) + acorn parse, decl→assignment, trailing-expr→setResult
19
+ ├── guest.ts (330 dòng) — namespace proxy + with(SCOPE) + v8.serialize + AsyncLocalStorage, KHÔNG Bun
20
+ ├── engine.ts (564 dòng) — EngineManager host, spawn(process.execPath, [--experimental-strip-types, guest.ts])
21
+ └── index.ts (22 dòng) — barrel
22
+
23
+ test/runtime/scratchpad/
24
+ ├── protocol.test.ts (72 dòng)
25
+ ├── transform.test.ts (59 dòng)
26
+ └── engine.spike.test.ts (126 dòng) — 2 invariant + phụ trợ, spawn subprocess thật
27
+ ```
28
+
29
+ ## Cách chạy
30
+
31
+ ```bash
32
+ cd /home/bom/source/my_pi/pi-crew
33
+ node scripts/test-runner.mjs --test-force-exit 'test/runtime/scratchpad/**/*.test.ts'
34
+ ```
35
+
36
+ ## Kết quả
37
+
38
+ ```
39
+ # tests 17
40
+ # pass 17
41
+ # fail 0
42
+ ```
43
+
44
+ ## Port Node quan trọng (không Bun)
45
+
46
+ | pi-rlm (Bun) | port Node | xác nhận |
47
+ |---|---|---|
48
+ | `bun:jsc serialize/deserialize` | `node:v8` serialize/deserialize | ✅ guest.ts:32, snapshot/restore pass invariant (b) |
49
+ | `Bun.Transpiler` (DCE off) | `esbuild` transformSync + acorn | ✅ transform.ts, trailing-expr không bị drop |
50
+ | `Bun.inspect` | `node:util` inspect | ✅ guest.ts:31 |
51
+ | `spawn("bun", ["run", guest])` | `spawn(process.execPath, ["--experimental-strip-types", guest])` | ✅ engine.ts:173 |
52
+ | `Bun.$` guard / host bridge | **bỏ** (không cần cho spike) | — |
53
+
54
+ ## Kết luận
55
+
56
+ **GO** — FLAGSHIP được xanh-light. Bước tiếp: FLAGSHIP Phase 1 (tool `execute` opt-in per role + snapshot vào artifact-store), rồi Phase 2 (crash-resume trong retry loop).
57
+
58
+ ---
59
+
60
+ # Phase 2 — Crash-Resume (cross-attempt restore)
61
+
62
+ Phase 2 closes the crash-resume loop: a worker attempt N+1 (retry / crash-recovery
63
+ re-queue / manual re-run) automatically revives the namespace from the previous
64
+ attempt's snapshot, **without the model knowing** (besides a one-line notice).
65
+
66
+ ## Flow
67
+
68
+ ```
69
+ attempt N (scratchpad worker)
70
+ └─ execute → EngineManager → guest namespace
71
+ └─ flush (debounce / shutdown-quit / post-ok) → writeArtifact REDACTED
72
+ → artifactsRoot/scratchpad/<taskId>.attempt-<i>.snapshot.json
73
+ │ worker dies / fails / cancelled
74
+ ▼
75
+ attempt N+1 spawn (prepareSpawnContext — the single choke point)
76
+ └─ findLatestScratchpadSnapshot(artifactsRoot, taskId) ← snapshot-lookup.ts
77
+ └─ latest MTIME wins (model-fallback `i` resets each retry round → number is
78
+ NOT write-order; tie-break: lowest attempt = newest round)
79
+ └─ env PI_CREW_SCRATCHPAD_RESTORE (+ RESTORE_MTIME hint)
80
+ ▼
81
+ attempt N+1 worker (scratchpad-lifecycle.ts)
82
+ └─ FIRST execute call → re-validate at READ time (D10) → restoreState →
83
+ notice "[scratchpad] restored N vars from attempt-K; restored:[...]; failed:[...]"
84
+ ```
85
+
86
+ ## Env keys (parent → worker, set in `prepareSpawnContext` scratchpad gate)
87
+
88
+ | Key | Direction | Purpose |
89
+ |---|---|---|
90
+ | `PI_CREW_SCRATCHPAD` | parent→worker | "1" arms the execute tool (dormant gate) |
91
+ | `PI_CREW_TASK_ID` | parent→worker | snapshot relativePath provenance |
92
+ | `PI_CREW_ATTEMPT` | parent→worker | model-fallback index (per-attempt suffix) |
93
+ | `PI_CREW_ARTIFACTS_ROOT` | parent→worker | writeArtifact root |
94
+ | `PI_CREW_SCRATCHPAD_SNAPSHOT` | parent→worker | WRITE target (raw temp, never in artifacts) |
95
+ | `PI_CREW_SCRATCHPAD_RESTORE` | parent→worker | **Phase 2**: READ source (redacted artifact) |
96
+ | `PI_CREW_SCRATCHPAD_RESTORE_MTIME` | parent→worker | **Phase 2**: swap-detection HINT (forgeable, not authn) |
97
+ | `PI_CREW_KIND` / `PI_CREW_PARENT_PID` / `PI_CREW_GUEST` | engine→guest | **Phase 2 (D5)**: guest reports the WORKER pid (not the leader's) so an orphaned guest is flagged by the zombie scanner |
98
+
99
+ ## Guards (D1–D13)
100
+
101
+ - **D1/D1b'** lookup at spawn, latest mtime, tie-break lowest attempt.
102
+ - **D3** restore once per session, lazy on first execute (D7 invariant kept).
103
+ - **D4** redacted secret → literal `"***"` placeholder (guest special-case; base64
104
+ of a real value is never `"***"`).
105
+ - **D5/MAJOR-S1** production wiring: `getScratchpadEngine` overrides
106
+ `PI_CREW_PARENT_PID=worker pid` (pure inheritance leaves guests LIVE forever).
107
+ - **D6** cap 4 MiB two-sided: write-side raw byteLength (trim failed→50 only when
108
+ over cap); read-side file size + guest per-var 256 KiB.
109
+ - **D10** restore path re-validated at READ time (TOCTOU): containment +
110
+ filename pattern + lstat regular + size + mtime pin.
111
+ - **D11** restore fail-open: any failure logs + continues on an empty namespace.
112
+ - **D12** scan strict (lstat/pattern/regular) — cross-agent poisoning is NOT a new
113
+ trust boundary (same-uid team worker already writes artifacts); notice lists
114
+ var names so the model re-verifies.
115
+ - **D13** base64 round-trip check before deserialize (flat redaction can inject
116
+ `"***"` into a valid payload → silent corruption → failed[]).
117
+
118
+ ## Threat model (Phase 2 additions — defense-in-depth within the same-uid boundary)
119
+
120
+ - **v8.deserialize of restore content is unauthenticated** (no HMAC). Bounded by
121
+ the 4 MiB file cap + 256 KiB per-var cap + same-uid artifact dir. A planted
122
+ snapshot with a crafted v8 blob can run deserialize gadgets in the guest — but
123
+ the guest already runs at full worker trust (it holds provider keys + broker
124
+ token), so this does not cross the existing boundary. An HMAC over the payload
125
+ is a Phase 2.5/3 hardening if artifacts ever land in a shared location.
126
+ - **Secret-at-rest under a benign key name** persists as base64 for the run's
127
+ retention (structural redaction is key-name based, best-effort). A sanitized-
128
+ namespace policy is a Phase 2.5 concern.
129
+ - **Restore-source trust = same as any artifact**: a same-uid team worker can
130
+ plant a matching-named snapshot; restore removes the "model chooses to read"
131
+ step, so the notice deliberately lists the revived var names.
132
+
133
+ ## CI note
134
+
135
+ The `test/runtime/scratchpad/*` spike tests (incl. the D1/D4/D6/D13 restore pins)
136
+ are NOT wired into `npm run test:unit` (which globs `test/unit/**`). Run them
137
+ directly per the Phase runbook: `node scripts/test-runner.mjs --test-force-exit
138
+ test/runtime/scratchpad/restore-e2e.spike.test.ts`.
139
+
140
+ ---
141
+
142
+ # Phase 3 — Cancellation: kill-and-restore (verified) + atomic snapshot
143
+
144
+ ## Kill-and-restore ALREADY WORKS (no new handler needed)
145
+
146
+ A worker killed via SIGTERM is flushed by the **existing** F3 quit-path — no
147
+ Phase 3 worker-side handler is required. The chain (verified against installed
148
+ pi 0.80.3/0.84.1):
149
+
150
+ ```
151
+ parent abort() / timeout / drain
152
+ └─ child-pi killProcessTree → group SIGTERM
153
+ └─ pi print-mode signal handler (modes/print-mode.js:31-44 registerSignalHandlers)
154
+ └─ disposeRuntime() → runtimeHost.dispose()
155
+ └─ emitSessionShutdownEvent({ reason: "quit" }) [awaited]
156
+ └─ scratchpad-lifecycle F3 handler (reason === "quit" gate passes)
157
+ └─ performShutdownFlush: snapshot → writeArtifact (redact) → engine.kill
158
+ └─ process.exit(143)
159
+ ```
160
+
161
+ The next attempt then restores from this F3 flush (Phase 2), closing the ≤1.5s
162
+ debounce gap. (pi-crew's own `extension/crew-cleanup.ts:98` also installs a
163
+ worker SIGTERM handler for child-process cleanup; both coexist.)
164
+
165
+ ## Phase 3 hardening
166
+
167
+ - **Atomic `snapshotState` (D2')**: the raw snapshot temp is now written via
168
+ `temp + rename` (same dir → atomic). Eliminates the theoretical torn-write
169
+ race between the debounce timer and the F3 quit flush.
170
+ - **EngineBusyError: SKIPPED** (confirmed). The spike dropped it (engine.ts:15);
171
+ the engine serializes concurrent `execute` calls via a FIFO queue, and
172
+ ping-before-execute (`scratchpad-lifecycle.ts`) already detects a wedged guest.
173
+ Re-adding a busy-reject would break the queue contract for no new coverage.
174
+ - **Eviction is a non-scenario**: live agents are in-process SDK sessions
175
+ (`live-session-runtime.ts`), `abort()` is in-process (no SIGTERM, no worker
176
+ process). Scratchpad is never armed in the host process (gate requires
177
+ `PI_CREW_KIND=subagent`), so there is nothing to flush on eviction.
178
+
179
+ ## Pin test
180
+
181
+ `test/runtime/scratchpad/sigterm-kill-restore.spike.test.ts` (gated
182
+ `PI_CREW_TEST_REAL_MODEL=1`) spawns a real `pi --mode json -p` worker, runs one
183
+ `execute` cell, sends SIGTERM inside the debounce window, and asserts an F3
184
+ artifact appears with a SIGTERM-time mtime. Skipped by default (CI-safe).