@junghanacs/entwurf 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +112 -0
  3. package/DELIVERY.md +1 -0
  4. package/README.md +47 -7
  5. package/VERIFY.md +17 -1
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/setup-clean-host.md +104 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
  19. package/mcp/entwurf-bridge/src/index.ts +7 -7
  20. package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
  21. package/package.json +13 -10
  22. package/pi/entwurf-capabilities.json +1 -0
  23. package/pi/pi-durable/bootstrap.mjs +281 -0
  24. package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
  25. package/pi/pi-durable/overlay/upstream-pin.json +16 -0
  26. package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
  27. package/pi-extensions/lib/fresh-call-composition.ts +42 -7
  28. package/pi-extensions/lib/meta-sender-identity.ts +7 -1
  29. package/pi-extensions/lib/meta-session.ts +28 -2
  30. package/pi-extensions/lib/mux-fresh-call.ts +34 -2
  31. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
  32. package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
  33. package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
  34. package/run.sh +200 -33
  35. package/scripts/check-fresh-call-dispatch.ts +18 -0
  36. package/scripts/check-gate-qualification.ts +2 -1
  37. package/scripts/check-pi-durable-contact.ts +834 -0
  38. package/scripts/check-pi-durable-receive.ts +1116 -0
  39. package/scripts/check-pi-durable-send.ts +1661 -0
  40. package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
  41. package/scripts/lib/pi-durable-r1.ts +100 -0
  42. package/scripts/lib/pi-durable-reap.ts +27 -0
  43. package/scripts/lib/pi-durable-receive.ts +137 -0
  44. package/scripts/mutants/control-send-receipt.json +1 -1
  45. package/scripts/mutants/copilot-birth.json +2 -2
  46. package/scripts/mutants/meta-hook-session-switch.json +4 -2
  47. package/scripts/mutants/mux-fresh-call.json +69 -3
  48. package/scripts/mutants/omp-birth.json +2 -2
  49. package/scripts/mutants/omp-fresh.json +6 -4
  50. package/scripts/mutants/pack-install.json +3 -3
  51. package/scripts/mutants/pi-durable.json +390 -0
  52. package/scripts/mutants/pi-floor.json +2 -2
  53. package/scripts/pi-durable-contact-host.mjs +105 -0
  54. package/scripts/pi-durable-receive-host.mjs +127 -0
  55. package/scripts/pi-durable-send-host.mjs +213 -0
  56. package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
  57. package/scripts/smoke-setup-verdict.sh +21 -2
  58. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,1661 @@
1
+ /**
2
+ * G-send (#129 L-send, FIRST CELL) — one native durable turn sends one `entwurf_v2`.
3
+ * CHECKOUT-ONLY dev gate: an installed package refuses it in `run_ts` before it reads an input.
4
+ *
5
+ * EVIDENCE BOXES, kept apart and never substituted for one another:
6
+ * S local scripted model protocol — a 127.0.0.1 OpenAI-compatible endpoint THIS gate serves,
7
+ * configured through the app's documented `models.json` compatible-endpoint support. It
8
+ * answers the first request with exactly one `entwurf_v2` tool call and the second with text.
9
+ * No vendor, no credential (a literal dummy key), no runtime or provider patch.
10
+ * H actual native harness path — the app's own controller input → GenerationTask → ToolTask →
11
+ * the adapter's registered tool → the private compiled bridge. The host driver submits once
12
+ * and only watches the native view (scripts/pi-durable-send-host.mjs).
13
+ * V an actual vendor model turn — NOT run here.
14
+ *
15
+ * Target: a SEEDED self-fetch fixture (the check-bridge-delivery C2 recipe): one claude-code
16
+ * record whose sender and receiver markers are owned by a separate idle child of this gate —
17
+ * outside the gate → host → bridge ancestry the bridge scans for its own sender, so the host's
18
+ * pi-durable marker is the only identity in that chain. Delivery here means a physical .msg and a
19
+ * poked inbox.signal for this one forced call: not a recipient turn, not exactly-once, not recovery.
20
+ *
21
+ * Independent oracles: the wire (what the app sent the endpoint), the raw private store and
22
+ * mailbox, the bridge's own tools/list as asked by this gate, and the native view the host
23
+ * reports. None imports the adapter's expected constants.
24
+ *
25
+ * Inputs as check-pi-durable-contact (missing → SKIP 97, never a pass):
26
+ * ENTWURF_PI_DURABLE_RUNTIME, ENTWURF_PI_DURABLE_BRIDGE_ENTRY
27
+ * Optional:
28
+ * ENTWURF_PI_DURABLE_RECEIPTS a NEW absolute directory (refused if it exists) that receives the
29
+ * cell's raw bytes: wire.json, bridge-tools.json, native-view.json (a
30
+ * projection by the host driver, not native internals), the landed
31
+ * mailbox files, the raw host/target records and markers, and a
32
+ * correlation.json. Export failure fails the gate; cleanup still runs.
33
+ * ENTWURF_PI_DURABLE_SEND_FAULT `malformed-report`: the gate's own cleanup control — the host
34
+ * driver births, sends a non-JSON report line and gives the app no
35
+ * input; the gate must fail for that cause and still reap.
36
+ *
37
+ * ENTWURF_PI_DURABLE_SEND_CELL `valid` (default); `invalid-args` — the scripted call omits the
38
+ * REQUIRED `intent`, and the app's own validation must settle it as
39
+ * `invalid_arguments`; `reject-undeliverable` — valid arguments, a
40
+ * target with no receiver marker, and the bridge must refuse by name
41
+ * (`mailbox-undeliverable`), surfacing natively as `tool_error`.
42
+ * Unknown selector exits 2 before anything spawns.
43
+ *
44
+ * Both cells read the bridge child's kernel counters (/proc/<pid>/io `rchar`, `syscr`) at sampled
45
+ * points: idleA, idleB (one sampled idle window), beforeGo (just before the one native input) and
46
+ * afterSettle (after the turn settles, before any close). The valid cell is the CALIBRATION: both
47
+ * counters must rise over beforeGo→afterSettle. The invalid cell must leave both unchanged — "no
48
+ * additional counted reads over the observed interval", NOT an interception of RPC writes or of
49
+ * adapter.execute. An unreadable counter, idle movement or a missing rise is a named GAP.
50
+ *
51
+ * Every wire assertion is judged on the FINAL snapshot, after the host has exited and the endpoint
52
+ * is closed, so a late request cannot slip past "exactly two".
53
+ */
54
+ import { spawn } from "node:child_process";
55
+ import { createHash, randomBytes } from "node:crypto";
56
+ import * as fs from "node:fs";
57
+ import * as http from "node:http";
58
+ import type { AddressInfo } from "node:net";
59
+ import * as os from "node:os";
60
+ import * as path from "node:path";
61
+ import { createInterface } from "node:readline";
62
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
63
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
64
+ import {
65
+ metaReceiverMarkerPath,
66
+ upsertMetaSession,
67
+ writeMetaReceiverMarker,
68
+ writeMetaSenderMarker,
69
+ } from "../pi-extensions/lib/meta-session.ts";
70
+ import {
71
+ inspectPiDurableRuntime,
72
+ PI_DURABLE_RESOLVER_RELATIVE,
73
+ readPiDurablePin,
74
+ } from "../pi-extensions/lib/pi-durable-runtime.ts";
75
+ import { skipLive } from "./lib/live-skip.ts";
76
+ import { checkpointVerdict, intentFrameVerdict, preCopyVerdict } from "./lib/pi-durable-r1.ts";
77
+ import { ownedReapVerdict } from "./lib/pi-durable-reap.ts";
78
+
79
+ const LABEL = "check-pi-durable-send";
80
+ const REPO = path.resolve(import.meta.dirname, "..");
81
+ const OVERLAY = path.join(REPO, "pi", "pi-durable", "overlay");
82
+ const HOST_DRIVER = path.join(REPO, "scripts", "pi-durable-send-host.mjs");
83
+ const REPORT_TIMEOUT_MS = 120_000;
84
+ const CLOSE_TIMEOUT_MS = 30_000;
85
+ const REAP_TIMEOUT_MS = 5_000;
86
+ const CELL = process.env.ENTWURF_PI_DURABLE_SEND_CELL?.trim() || "valid";
87
+ if (CELL !== "valid" && CELL !== "invalid-args" && CELL !== "reject-undeliverable" && CELL !== "r1-unsafe-recovery") {
88
+ console.error(
89
+ `check-pi-durable-send: unknown ENTWURF_PI_DURABLE_SEND_CELL ${JSON.stringify(CELL)} (only "valid", "invalid-args", "reject-undeliverable", "r1-unsafe-recovery")`,
90
+ );
91
+ process.exit(2);
92
+ }
93
+ const VALID = CELL === "valid";
94
+ /** invalid-args: the scripted call omits the REQUIRED `intent`; native validation must settle it. */
95
+ const INVALID = CELL === "invalid-args";
96
+ /**
97
+ * reject-undeliverable: valid arguments reach the bridge, whose target is the same seeded record and
98
+ * sender marker as the valid cell with ONE declared cause axis changed — no receiver marker — so the
99
+ * bridge must refuse it by name. Fresh ids, nonce and pids differ run to run; that is not a
100
+ * byte-identical one-variable fixture.
101
+ */
102
+ const REJECT = CELL === "reject-undeliverable";
103
+ /**
104
+ * r1-unsafe-recovery: the native ToolTask commits the call's intent while the host's own bridge is
105
+ * paused, so the bridge reads no request; the host is killed, the session is reopened with no new
106
+ * input, and the call must settle as interrupted without the tool implementation running again.
107
+ */
108
+ const R1 = CELL === "r1-unsafe-recovery";
109
+ const QK = INVALID ? "PDI" : REJECT ? "PDR" : R1 ? "PR1" : "PDS";
110
+ const CALL_ID = INVALID ? "call_pdi_1" : REJECT ? "call_pdr_1" : R1 ? "call_pr1_1" : "call_pds_1";
111
+ const FINAL_TEXT = INVALID ? "invalid noted." : REJECT ? "reject noted." : R1 ? "recovered." : "sent.";
112
+ /** The committed-intent frame must arrive within this of `go` — a validity cutoff under the MCP
113
+ * SDK's 60s request timeout, never an elapsed-time success inference. */
114
+ const INTENT_DEADLINE_MS = 30_000;
115
+ const INTERRUPTED_MESSAGE = "Tool entwurf_v2 was interrupted and may have partially run";
116
+ /** One sampled idle window between two counter reads, before the input. Not a quiescence proof. */
117
+ const IDLE_SAMPLE_MS = 300;
118
+
119
+ const runtimeInput = process.env.ENTWURF_PI_DURABLE_RUNTIME?.trim();
120
+ const bridgeInput = process.env.ENTWURF_PI_DURABLE_BRIDGE_ENTRY?.trim();
121
+ if (!runtimeInput || !bridgeInput) {
122
+ skipLive(
123
+ LABEL,
124
+ "set ENTWURF_PI_DURABLE_RUNTIME=<overlay checkout at the pin, patch applied, deps and model data installed> and " +
125
+ "ENTWURF_PI_DURABLE_BRIDGE_ENTRY=<bridge index.js emitted from this checkout into a private bundle>; " +
126
+ "this gate provisions neither",
127
+ );
128
+ }
129
+ const runtime = path.resolve(runtimeInput);
130
+ const bridgeEntry = path.resolve(bridgeInput);
131
+ const FAULT = process.env.ENTWURF_PI_DURABLE_SEND_FAULT?.trim() || undefined;
132
+ if (FAULT !== undefined && FAULT !== "malformed-report") {
133
+ console.error(`${LABEL}: unknown ENTWURF_PI_DURABLE_SEND_FAULT ${JSON.stringify(FAULT)} (only "malformed-report")`);
134
+ process.exit(2);
135
+ }
136
+ const receiptsInput = process.env.ENTWURF_PI_DURABLE_RECEIPTS?.trim() || undefined;
137
+ if (receiptsInput !== undefined) {
138
+ // Fail closed BEFORE anything is spawned: absolute, and a directory this run creates itself.
139
+ if (!path.isAbsolute(receiptsInput)) {
140
+ console.error(`${LABEL}: ENTWURF_PI_DURABLE_RECEIPTS must be absolute (got ${JSON.stringify(receiptsInput)})`);
141
+ process.exit(2);
142
+ }
143
+ try {
144
+ fs.mkdirSync(receiptsInput);
145
+ } catch (error) {
146
+ console.error(`${LABEL}: ENTWURF_PI_DURABLE_RECEIPTS must be a new directory: ${(error as Error).message}`);
147
+ process.exit(2);
148
+ }
149
+ }
150
+
151
+ let passed = 0;
152
+ let failed = 0;
153
+ function ok(label: string, cond: boolean): void {
154
+ if (cond) {
155
+ console.log(` ok ${label}`);
156
+ passed++;
157
+ } else {
158
+ console.log(` FAIL ${label}`);
159
+ failed++;
160
+ }
161
+ }
162
+ /** Pre-spawn only. */
163
+ function fatal(message: string): never {
164
+ console.error(`${LABEL}: ${message}`);
165
+ process.exit(1);
166
+ }
167
+ const sha256 = (data: string | Buffer): string => createHash("sha256").update(data).digest("hex");
168
+ function canonicalJson(value: unknown): string {
169
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
170
+ if (value !== null && typeof value === "object") {
171
+ const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
172
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
173
+ }
174
+ return JSON.stringify(value);
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // 0. Inputs — the same pinned overlay / model data / bundle registry facts as G-contact.
179
+ // ---------------------------------------------------------------------------
180
+ // The pin and the runtime facts come from the ONE verifier the managed launch, setup and the fresh
181
+ // preflight use (pi-extensions/lib/pi-durable-runtime.ts).
182
+ const { pin } = readPiDurablePin(OVERLAY);
183
+ const facts = inspectPiDurableRuntime(runtime, OVERLAY);
184
+ const resolver = path.join(runtime, PI_DURABLE_RESOLVER_RELATIVE);
185
+ const bundleRegistry = path.join(
186
+ path.resolve(bridgeEntry, "..", "..", "..", ".."),
187
+ "pi-extensions",
188
+ "entwurf-capabilities.json",
189
+ );
190
+ console.log("0. inputs");
191
+ ok(
192
+ `[QK:${QK}-INPUTS] runtime is the pin with exactly the tracked overlay (index == HEAD, nothing staged), the pin's ignored model data, the resolver, and a bridge bundle whose registry matches this checkout`,
193
+ facts.gitAnswered &&
194
+ facts.head === pin.commit &&
195
+ facts.indexMatchesHead &&
196
+ facts.diffEqualsPatch &&
197
+ facts.untracked === "" &&
198
+ facts.modelDataFiles === pin.modelData.files &&
199
+ facts.modelDataAggregate === pin.modelData.aggregateSha256 &&
200
+ facts.manifestSha256 === pin.modelData.manifestSha256 &&
201
+ facts.resolverPresent &&
202
+ fs.existsSync(bridgeEntry) &&
203
+ fs.existsSync(bundleRegistry) &&
204
+ fs.readFileSync(bundleRegistry).equals(fs.readFileSync(path.join(REPO, "pi", "entwurf-capabilities.json"))),
205
+ );
206
+ // The bundle's compiled owner join decides the armed host's replyability in the landed body.
207
+ const bundleDeliverability = path.join(
208
+ path.resolve(bridgeEntry, "..", "..", "..", ".."),
209
+ "pi-extensions",
210
+ "lib",
211
+ "entwurf-deliverability.js",
212
+ );
213
+ const joinLine = fs.existsSync(bundleDeliverability)
214
+ ? (/SENDER_JOINED_RECEIVER_OWNER_KINDS\s*=\s*(\[[^\]]*\])/.exec(fs.readFileSync(bundleDeliverability, "utf8"))?.[1] ??
215
+ "")
216
+ : "";
217
+ ok(
218
+ `[QK:${QK}-BUNDLE-OWNER-JOIN] the bridge bundle's compiled owner join admits pi-durable-host (${joinLine || "not found"})`,
219
+ joinLine.includes('"pi-durable-host"'),
220
+ );
221
+ if (failed > 0)
222
+ fatal("inputs are not the pinned overlay, its model data, this checkout's registry and owner join — refusing to run");
223
+
224
+ // Coarse pi-durable-specific operator guard (as G-contact): no operator-byte claim beyond it.
225
+ const operatorAgent = path.join(os.homedir(), ".pi", "agent");
226
+ function operatorPiDurableFacts(): string {
227
+ const sessions = path.join(operatorAgent, "meta-sessions");
228
+ const durableRecords = fs.existsSync(sessions)
229
+ ? fs
230
+ .readdirSync(sessions)
231
+ .filter((n) => n.endsWith(".meta.json"))
232
+ .filter((n) => {
233
+ try {
234
+ return (
235
+ (JSON.parse(fs.readFileSync(path.join(sessions, n), "utf8")) as { backend?: unknown }).backend ===
236
+ "pi-durable"
237
+ );
238
+ } catch {
239
+ return false;
240
+ }
241
+ })
242
+ : [];
243
+ return JSON.stringify({
244
+ durableRecords,
245
+ senders: fs.existsSync(path.join(operatorAgent, "meta-senders", "pi-durable")),
246
+ durableSessions: fs.existsSync(path.join(operatorAgent, "experimental", "durable-sessions")),
247
+ });
248
+ }
249
+ const operatorBefore = operatorPiDurableFacts();
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Sandbox: one private world — HOME/XDG/agent, the four garden roots and ENTWURF_DIR.
253
+ // ---------------------------------------------------------------------------
254
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-durable-send-"));
255
+ const dirs = {
256
+ home: path.join(root, "home"),
257
+ agent: path.join(root, "agent"),
258
+ tmp: path.join(root, "tmp"),
259
+ project: path.join(root, "project"),
260
+ sessions: path.join(root, "garden", "meta-sessions"),
261
+ senders: path.join(root, "garden", "meta-senders"),
262
+ receivers: path.join(root, "garden", "meta-receivers"),
263
+ mailbox: path.join(root, "garden", "meta-mailbox"),
264
+ entwurf: path.join(root, "entwurf"),
265
+ xdgConfig: path.join(root, "xdg", "config"),
266
+ xdgCache: path.join(root, "xdg", "cache"),
267
+ xdgData: path.join(root, "xdg", "data"),
268
+ xdgState: path.join(root, "xdg", "state"),
269
+ };
270
+ for (const dir of Object.values(dirs)) fs.mkdirSync(dir, { recursive: true });
271
+ process.umask(0o022);
272
+ const env: Record<string, string> = {
273
+ PATH: process.env.PATH ?? "/run/current-system/sw/bin",
274
+ HOME: dirs.home,
275
+ TMPDIR: dirs.tmp,
276
+ XDG_CONFIG_HOME: dirs.xdgConfig,
277
+ XDG_CACHE_HOME: dirs.xdgCache,
278
+ XDG_DATA_HOME: dirs.xdgData,
279
+ XDG_STATE_HOME: dirs.xdgState,
280
+ PI_CODING_AGENT_DIR: dirs.agent,
281
+ ENTWURF_META_SESSIONS_DIR: dirs.sessions,
282
+ ENTWURF_META_SENDERS_DIR: dirs.senders,
283
+ ENTWURF_META_RECEIVERS_DIR: dirs.receivers,
284
+ ENTWURF_META_MAILBOX_DIR: dirs.mailbox,
285
+ ENTWURF_DIR: dirs.entwurf,
286
+ PI_OFFLINE: "1",
287
+ NOSYSBASHRC: "1",
288
+ TERM: "dumb",
289
+ };
290
+ const hostEnv: Record<string, string> = {
291
+ ...env,
292
+ ...(FAULT === undefined ? {} : { ENTWURF_PI_DURABLE_SEND_FAULT: FAULT }),
293
+ };
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // S: the scripted endpoint. It records every request; anything beyond the two it scripts is
297
+ // answered 500 and counted, never satisfied.
298
+ // ---------------------------------------------------------------------------
299
+ interface WireRequest {
300
+ method: string;
301
+ url: string;
302
+ body: Record<string, unknown> | null;
303
+ }
304
+ const wire: WireRequest[] = [];
305
+ let scriptedArgs: Record<string, unknown> = {};
306
+ const sse = (res: http.ServerResponse, chunks: unknown[]) => {
307
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
308
+ for (const chunk of chunks) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
309
+ res.end("data: [DONE]\n\n");
310
+ };
311
+ const chunk = (delta: Record<string, unknown>, finish: string | null) => ({
312
+ id: "chatcmpl-pds",
313
+ object: "chat.completion.chunk",
314
+ created: 0,
315
+ model: "scripted",
316
+ choices: [{ index: 0, delta, finish_reason: finish }],
317
+ });
318
+ const usage = {
319
+ id: "chatcmpl-pds",
320
+ object: "chat.completion.chunk",
321
+ created: 0,
322
+ model: "scripted",
323
+ choices: [],
324
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
325
+ };
326
+ /** R1's second request must be the recovered turn carrying the interrupted result for the call. */
327
+ function isRecoveryRequest(body: Record<string, unknown> | null): boolean {
328
+ const messages = (body?.messages as { role?: string; tool_call_id?: string; content?: unknown }[] | undefined) ?? [];
329
+ return messages.some(
330
+ (m) =>
331
+ m.role === "tool" && m.tool_call_id === CALL_ID && JSON.stringify(m.content ?? "").includes("was interrupted"),
332
+ );
333
+ }
334
+ const server = http.createServer((req, res) => {
335
+ let raw = "";
336
+ req.on("data", (c: Buffer) => {
337
+ raw += c.toString("utf8");
338
+ });
339
+ req.on("end", () => {
340
+ let body: Record<string, unknown> | null = null;
341
+ try {
342
+ body = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
343
+ } catch {
344
+ body = null;
345
+ }
346
+ wire.push({ method: req.method ?? "", url: req.url ?? "", body });
347
+ const n = wire.length;
348
+ if (req.method === "POST" && req.url === "/v1/chat/completions" && n === 1) {
349
+ sse(res, [
350
+ chunk(
351
+ {
352
+ role: "assistant",
353
+ tool_calls: [
354
+ {
355
+ index: 0,
356
+ id: CALL_ID,
357
+ type: "function",
358
+ function: { name: "entwurf_v2", arguments: JSON.stringify(scriptedArgs) },
359
+ },
360
+ ],
361
+ },
362
+ null,
363
+ ),
364
+ chunk({}, "tool_calls"),
365
+ usage,
366
+ ]);
367
+ } else if (
368
+ req.method === "POST" &&
369
+ req.url === "/v1/chat/completions" &&
370
+ n === 2 &&
371
+ (!R1 || isRecoveryRequest(body))
372
+ ) {
373
+ sse(res, [chunk({ role: "assistant", content: FINAL_TEXT }, null), chunk({}, "stop"), usage]);
374
+ } else {
375
+ res.writeHead(500, { "content-type": "application/json" });
376
+ res.end(
377
+ JSON.stringify({ error: { message: `scripted endpoint: unexpected request #${n} ${req.method} ${req.url}` } }),
378
+ );
379
+ }
380
+ });
381
+ });
382
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
383
+ const port = (server.address() as AddressInfo).port;
384
+ fs.writeFileSync(
385
+ path.join(dirs.agent, "models.json"),
386
+ `${JSON.stringify(
387
+ {
388
+ providers: {
389
+ loopback: {
390
+ baseUrl: `http://127.0.0.1:${port}/v1`,
391
+ api: "openai-completions",
392
+ apiKey: "pds-loopback-dummy",
393
+ models: [{ id: "scripted" }],
394
+ },
395
+ },
396
+ },
397
+ null,
398
+ 2,
399
+ )}\n`,
400
+ );
401
+ fs.writeFileSync(
402
+ path.join(dirs.agent, "settings.json"),
403
+ `${JSON.stringify({ defaultProvider: "loopback", defaultModel: "scripted" }, null, 2)}\n`,
404
+ );
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Processes this gate owns, and their bounded reaping.
408
+ // ---------------------------------------------------------------------------
409
+ function procStat(pid: number): { ppid: number; start: string } | null {
410
+ try {
411
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
412
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
413
+ return { ppid: Number(fields[1]), start: fields[19] as string };
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+ function procArgv(pid: number): string[] {
419
+ try {
420
+ return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
421
+ } catch {
422
+ return [];
423
+ }
424
+ }
425
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
426
+ interface Owned {
427
+ pid: number;
428
+ start: string;
429
+ what: string;
430
+ /** Paused by this gate with SIGSTOP: reaped with SIGKILL only, never left stopped. */
431
+ stopped?: boolean;
432
+ }
433
+ const owned: Owned[] = [];
434
+ /**
435
+ * Owned under `start`: for a bridge, the start key of the snapshot that qualified it (observeBridges);
436
+ * otherwise one read here, for this gate's own direct child right after its spawn event.
437
+ */
438
+ function own(pid: number, what: string, start: string = procStat(pid)?.start ?? ""): Owned {
439
+ const existing = owned.find((p) => p.pid === pid && p.start === start);
440
+ if (existing) return existing;
441
+ const entry = { pid, start, what };
442
+ owned.push(entry);
443
+ return entry;
444
+ }
445
+ /** Owned hosts — that exact pid and start — whose bridge children were observed while the host was alive. */
446
+ const childrenObserved = new Set<Owned>();
447
+ /**
448
+ * Own every bridge that /proc shows as a DIRECT child of a live owned host (PPID + entry argv). A child
449
+ * qualifies only if the `{ppid, start}` snapshot taken before its argv reads the same after it, and is owned
450
+ * under THAT start; the host must be the observed process before and after the scan. Any drift is a named
451
+ * note: nothing from the scan is owned and the host's children stay unobserved.
452
+ */
453
+ function observeBridges(host: Owned): boolean {
454
+ if (procStat(host.pid)?.start !== host.start) return false;
455
+ const qualified: { pid: number; start: string }[] = [];
456
+ const drift: string[] = [];
457
+ for (const pid of fs
458
+ .readdirSync("/proc")
459
+ .filter((n) => /^\d+$/.test(n))
460
+ .map(Number)) {
461
+ const before = procStat(pid);
462
+ if (before?.ppid !== host.pid) continue;
463
+ const bridge = procArgv(pid).includes(bridgeEntry);
464
+ const after = procStat(pid);
465
+ if (after?.ppid !== before.ppid || after.start !== before.start) {
466
+ drift.push(`child ${pid} of host ${host.pid} changed or vanished while it was qualified`);
467
+ } else if (bridge) qualified.push({ pid, start: before.start });
468
+ }
469
+ if (procStat(host.pid)?.start !== host.start) drift.push(`host ${host.pid} changed identity during its child scan`);
470
+ if (drift.length > 0) {
471
+ observationNotes.push(...drift);
472
+ return false;
473
+ }
474
+ for (const child of qualified) own(child.pid, "bridge", child.start);
475
+ childrenObserved.add(host);
476
+ return true;
477
+ }
478
+ /** Spawn completion or its named failure — never an error swallowed into an empty sink. */
479
+ function spawned(child: ReturnType<typeof spawn>, what: string): Promise<number> {
480
+ return new Promise((resolve, reject) => {
481
+ child.once("spawn", () => (child.pid === undefined ? reject(new Error(`${what}: no pid`)) : resolve(child.pid)));
482
+ child.once("error", (error) => reject(new Error(`${what} did not start: ${error.message}`)));
483
+ });
484
+ }
485
+ const observationNotes: string[] = [];
486
+ async function reapAll(): Promise<string[]> {
487
+ const problems: string[] = [];
488
+ // Before any signal: a still-live host's direct children become owned, from /proc, not from the
489
+ // host's own report. A host already gone cannot be asked, and that is said, not painted green.
490
+ for (const host of owned.filter((p) => p.what === "host")) {
491
+ if (procStat(host.pid)?.start === host.start) observeBridges(host);
492
+ else if (!childrenObserved.has(host)) {
493
+ observationNotes.push(`host ${host.pid} exited before its children were observed`);
494
+ }
495
+ }
496
+ for (const p of [...owned].reverse()) {
497
+ if (procStat(p.pid)?.start !== p.start) continue;
498
+ for (const signal of p.stopped ? (["SIGKILL"] as const) : (["SIGTERM", "SIGKILL"] as const)) {
499
+ try {
500
+ process.kill(p.pid, signal);
501
+ } catch (error) {
502
+ problems.push(`${p.what} ${p.pid}: ${signal} failed (${(error as Error).message})`);
503
+ }
504
+ const deadline = Date.now() + REAP_TIMEOUT_MS;
505
+ while (Date.now() < deadline && procStat(p.pid)?.start === p.start) await sleep(50);
506
+ if (procStat(p.pid)?.start !== p.start) break;
507
+ }
508
+ if (procStat(p.pid)?.start === p.start) problems.push(`${p.what} ${p.pid} is still alive after SIGKILL`);
509
+ }
510
+ return problems;
511
+ }
512
+
513
+ // ---------------------------------------------------------------------------
514
+ // The cell
515
+ // ---------------------------------------------------------------------------
516
+ interface EntrySummary {
517
+ kind: string;
518
+ role?: string;
519
+ toolCalls: { id: string; name: string; arguments: unknown }[];
520
+ toolCallId?: string;
521
+ toolName?: string;
522
+ isError?: boolean;
523
+ /** The tool-result entry's `data.diagnostics`, copied as the driver read them. */
524
+ diagnostics?: { severity: string; code: string; message: string }[];
525
+ text: string;
526
+ }
527
+
528
+ interface IoSnapshot {
529
+ label: string;
530
+ at: string;
531
+ pid: number;
532
+ startKey: string;
533
+ raw: string;
534
+ rchar: number;
535
+ syscr: number;
536
+ }
537
+ const ioSnapshots: IoSnapshot[] = [];
538
+ /** One raw read of the owned bridge's /proc io, with its identity checked; any doubt is a named GAP. */
539
+ function ioSnapshot(p: Owned, label: string): IoSnapshot {
540
+ if (procStat(p.pid)?.start !== p.start)
541
+ throw new Error(`GAP: bridge ${p.pid} is no longer the observed process at ${label}`);
542
+ let raw: string;
543
+ try {
544
+ raw = fs.readFileSync(`/proc/${p.pid}/io`, "utf8");
545
+ } catch (error) {
546
+ throw new Error(`GAP: /proc/${p.pid}/io is unreadable at ${label}: ${(error as Error).message}`);
547
+ }
548
+ const field = (name: string): number => {
549
+ const match = new RegExp(`^${name}: (\\d+)$`, "m").exec(raw);
550
+ const value = match ? Number(match[1]) : Number.NaN;
551
+ if (!Number.isSafeInteger(value)) throw new Error(`GAP: /proc/${p.pid}/io has no integer ${name} at ${label}`);
552
+ return value;
553
+ };
554
+ const snapshot = {
555
+ label,
556
+ at: new Date().toISOString(),
557
+ pid: p.pid,
558
+ startKey: p.start,
559
+ raw,
560
+ rchar: field("rchar"),
561
+ syscr: field("syscr"),
562
+ };
563
+ ioSnapshots.push(snapshot);
564
+ return snapshot;
565
+ }
566
+ interface Report {
567
+ hostPid: number;
568
+ session: { id: string };
569
+ attachment: { gardenId: string; bridgePid: number | null };
570
+ conversationIdBefore: number;
571
+ conversationIdAfter: number;
572
+ entries: EntrySummary[];
573
+ busy: boolean;
574
+ notices: string[];
575
+ error?: string;
576
+ }
577
+
578
+ async function bridgeToolList(): Promise<Map<string, unknown>> {
579
+ const transport = new StdioClientTransport({
580
+ command: process.execPath,
581
+ args: [bridgeEntry],
582
+ env,
583
+ cwd: dirs.project,
584
+ stderr: "pipe",
585
+ });
586
+ transport.stderr?.on("data", () => {});
587
+ const client = new Client({ name: LABEL, version: "1" });
588
+ try {
589
+ await client.connect(transport);
590
+ } catch (error) {
591
+ try {
592
+ await transport.close();
593
+ } catch (closeError) {
594
+ throw new AggregateError(
595
+ [error, closeError],
596
+ `bridge tools/list client did not start (${(error as Error).message}); closing it also failed`,
597
+ );
598
+ }
599
+ throw error;
600
+ }
601
+ try {
602
+ return new Map((await client.listTools()).tools.map((t) => [t.name, t.inputSchema]));
603
+ } finally {
604
+ await client.close();
605
+ }
606
+ }
607
+
608
+ function messageText(content: unknown): string {
609
+ if (typeof content === "string") return content;
610
+ if (Array.isArray(content)) {
611
+ return content
612
+ .flatMap((p) =>
613
+ p && typeof p === "object" && typeof (p as { text?: unknown }).text === "string"
614
+ ? [(p as { text: string }).text]
615
+ : [],
616
+ )
617
+ .join("\n");
618
+ }
619
+ return "";
620
+ }
621
+
622
+ interface CellContext {
623
+ listed: Map<string, unknown>;
624
+ report: Report;
625
+ hostPid: number;
626
+ targetGid: string;
627
+ targetRecordPath: string;
628
+ targetSenderMarker: string;
629
+ targetReceiverMarker: string | null;
630
+ msgs: string[];
631
+ box: string;
632
+ }
633
+ let context: CellContext | undefined;
634
+ interface TargetFacts {
635
+ recordSha: string;
636
+ recordBackend: string | undefined;
637
+ senderMarkerPresent: boolean;
638
+ receiverMarkerPresent: boolean;
639
+ receiversDirListing: string[] | null;
640
+ }
641
+ let causeBefore: TargetFacts | undefined;
642
+ let causeAfterSettle: TargetFacts | undefined;
643
+
644
+ async function cell(): Promise<void> {
645
+ // The target: a separate idle child owns both of its markers (C2 recipe). It runs in the same
646
+ // constructed private world as everything else this gate starts.
647
+ const targetOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 600000)"], {
648
+ stdio: "ignore",
649
+ env,
650
+ cwd: dirs.project,
651
+ });
652
+ own(await spawned(targetOwner, "the target owner"), "target-owner");
653
+ if (targetOwner.pid === undefined) throw new Error("the target owner has no pid");
654
+ const target = upsertMetaSession({
655
+ input: { backend: "claude-code", nativeSessionId: `pds-target-${process.pid}`, cwd: dirs.project },
656
+ dir: dirs.sessions,
657
+ });
658
+ const targetGid = target.record.gardenId;
659
+ const targetSenderMarker = writeMetaSenderMarker({
660
+ backend: "claude-code",
661
+ gardenId: targetGid,
662
+ nativeSessionId: target.record.nativeSessionId,
663
+ cwd: dirs.project,
664
+ ownerPid: targetOwner.pid,
665
+ sendersDir: dirs.senders,
666
+ });
667
+ // The reject cell's declared cause axis: this one marker is not written.
668
+ const targetReceiverMarker = REJECT
669
+ ? null
670
+ : writeMetaReceiverMarker({
671
+ gardenId: targetGid,
672
+ backend: "claude-code",
673
+ nativeSessionId: target.record.nativeSessionId,
674
+ ownerPid: targetOwner.pid,
675
+ armProvenance: "session-start",
676
+ receiversDir: dirs.receivers,
677
+ });
678
+ const receiverPath = metaReceiverMarkerPath(targetGid, dirs.receivers);
679
+ const targetFacts = (): TargetFacts => ({
680
+ recordSha: sha256(fs.readFileSync(target.path)),
681
+ recordBackend: (JSON.parse(fs.readFileSync(target.path, "utf8")) as { backend?: string }).backend,
682
+ senderMarkerPresent: fs.existsSync(targetSenderMarker),
683
+ receiverMarkerPresent: fs.existsSync(receiverPath),
684
+ receiversDirListing: fs.existsSync(dirs.receivers) ? fs.readdirSync(dirs.receivers) : null,
685
+ });
686
+ const before = targetFacts();
687
+ causeBefore = before;
688
+ if (REJECT) {
689
+ ok(
690
+ `[QK:PDR-CAUSE-FIXTURE] before the input: the target's record (claude-code) and sender marker exist and its receiver marker path is ABSENT (${receiverPath}; receivers dir: ${JSON.stringify(before.receiversDirListing)})`,
691
+ before.recordBackend === "claude-code" &&
692
+ before.senderMarkerPresent &&
693
+ !before.receiverMarkerPresent &&
694
+ !(before.receiversDirListing ?? []).some((n) => n.includes(targetGid)),
695
+ );
696
+ }
697
+ const message = `${INVALID ? "pds invalid" : REJECT ? "pds reject" : "pds first cell"} ${randomBytes(6).toString("hex")}`;
698
+ scriptedArgs = INVALID
699
+ ? { target: targetGid, message, wants_reply: false }
700
+ : { target: targetGid, intent: "fire-and-forget", message, wants_reply: false };
701
+
702
+ const listed = await bridgeToolList();
703
+
704
+ console.log(
705
+ `1. ${CELL} cell — one native turn: controller input → GenerationTask → ToolTask${INVALID ? " (validation)" : " → bridge"}${REJECT ? " (named refusal)" : ""} (S + H, V not run)`,
706
+ );
707
+ const host = spawn(
708
+ process.execPath,
709
+ ["--import", resolver, HOST_DRIVER, bridgeEntry, "send the scripted message", "--await-go"],
710
+ {
711
+ cwd: dirs.project,
712
+ env: hostEnv,
713
+ stdio: ["pipe", "pipe", "pipe"],
714
+ },
715
+ );
716
+ let streamError = "";
717
+ host.stdin?.on("error", (e) => {
718
+ streamError += ` [host stdin error: ${e.message}]`;
719
+ });
720
+ const hostOwned = own(await spawned(host, "the host"), "host");
721
+ host.on("error", (e) => {
722
+ streamError += ` [host process error: ${e.message}]`;
723
+ });
724
+ let stderr = "";
725
+ host.stderr?.on("data", (c: Buffer) => {
726
+ stderr = (stderr + c.toString("utf8")).slice(-8192);
727
+ });
728
+ const lines: string[] = [];
729
+ const waiters: ((line: string | null) => void)[] = [];
730
+ createInterface({ input: host.stdout as NodeJS.ReadableStream })
731
+ .on("line", (line) => {
732
+ const w = waiters.shift();
733
+ if (w) w(line);
734
+ else lines.push(line);
735
+ })
736
+ .on("close", () => {
737
+ for (const w of waiters.splice(0)) w(null);
738
+ });
739
+ let exitCode: number | null | undefined;
740
+ const exited = new Promise<void>((resolve) =>
741
+ host.on("exit", (code) => {
742
+ exitCode = code;
743
+ resolve();
744
+ }),
745
+ );
746
+ const next = (ms: number) =>
747
+ new Promise<Record<string, unknown>>((resolve, reject) => {
748
+ const settle = (line: string | null) => {
749
+ if (line === null)
750
+ return reject(new Error(`host ended without a line${streamError}; stderr: ${stderr.trim()}`));
751
+ try {
752
+ resolve(JSON.parse(line) as Record<string, unknown>);
753
+ } catch (error) {
754
+ reject(new Error(`host sent a malformed line (${(error as Error).message}): ${line.slice(0, 200)}`));
755
+ }
756
+ };
757
+ const queued = lines.shift();
758
+ if (queued !== undefined) return settle(queued);
759
+ const timer = setTimeout(
760
+ () =>
761
+ reject(
762
+ new Error(
763
+ `gate timeout: no host line within ${ms}ms (a gate failure, not a dispatch verdict); stderr: ${stderr.trim()}`,
764
+ ),
765
+ ),
766
+ ms,
767
+ );
768
+ waiters.push((line) => {
769
+ clearTimeout(timer);
770
+ settle(line);
771
+ });
772
+ });
773
+
774
+ const born = await next(REPORT_TIMEOUT_MS);
775
+ if (born.error !== undefined)
776
+ throw new Error(`host failed before input: ${String(born.error)}; stderr: ${stderr.trim()}`);
777
+ if (born.born !== true) throw new Error(`host sent no born line: ${JSON.stringify(born).slice(0, 200)}`);
778
+ // Own the bridge from /proc BEFORE reading any of its counters.
779
+ if (!observeBridges(hostOwned)) throw new Error(`GAP: host ${hostOwned.pid}'s children were not observed coherently`);
780
+ const bridges = owned.filter((p) => p.what === "bridge");
781
+ if (bridges.length !== 1)
782
+ throw new Error(`GAP: expected exactly one bridge child of the host, observed ${bridges.length}`);
783
+ const bridge = bridges[0] as Owned;
784
+ const idleA = ioSnapshot(bridge, "idleA");
785
+ await sleep(IDLE_SAMPLE_MS);
786
+ const idleB = ioSnapshot(bridge, "idleB");
787
+ if (idleA.rchar !== idleB.rchar || idleA.syscr !== idleB.syscr) {
788
+ throw new Error(
789
+ `GAP: the bridge's counters moved in the sampled idle window (rchar ${idleA.rchar}→${idleB.rchar}, syscr ${idleA.syscr}→${idleB.syscr}); the read-count oracle cannot be used`,
790
+ );
791
+ }
792
+ ok(
793
+ `[QK:${QK}-IO-IDLE-SAMPLE-STABLE] the bridge child's counters are readable and unchanged over one sampled idle window (rchar ${idleA.rchar}, syscr ${idleA.syscr}) — that window only`,
794
+ true,
795
+ );
796
+ const beforeGo = ioSnapshot(bridge, "beforeGo");
797
+ host.stdin?.write("go\n");
798
+
799
+ const report = (await next(REPORT_TIMEOUT_MS)) as unknown as Report;
800
+ if (report.error !== undefined) throw new Error(`host failed: ${report.error}; stderr: ${stderr.trim()}`);
801
+ const afterSettle = ioSnapshot(bridge, "afterSettle");
802
+ const hostGid = report.attachment.gardenId;
803
+ // The receipt the app carried back is compared against the native view here and against the
804
+ // wire on the FINAL snapshot below; the native view records the same text.
805
+ const resultTextInView = report.entries.find((e) => e.kind === "pi.tool-result")?.text ?? "";
806
+
807
+ // --- raw mailbox (the bridge's ENQUEUE, read here)
808
+ const box = path.join(dirs.mailbox, targetGid);
809
+ const msgs = fs.existsSync(box) ? fs.readdirSync(box).filter((f) => f.endsWith(".msg")) : [];
810
+ const body = msgs.length === 1 ? fs.readFileSync(path.join(box, msgs[0] as string), "utf8") : "";
811
+ const rise = { rchar: afterSettle.rchar - beforeGo.rchar, syscr: afterSettle.syscr - beforeGo.syscr };
812
+ if (INVALID) {
813
+ ok(
814
+ `[QK:PDI-MAILBOX-EMPTY] side-effect check: the armed target's mailbox holds no .msg and no inbox.signal (${fs.existsSync(box) ? fs.readdirSync(box).join(", ") || "empty" : "no mailbox dir"}) — a side-effect check only: the bridge also requires intent, so an empty mailbox does NOT by itself show that no call was made`,
815
+ msgs.length === 0 && !fs.existsSync(path.join(box, "inbox.signal")),
816
+ );
817
+ ok(
818
+ `[QK:PDI-BRIDGE-READ-NOTHING] the bridge child made no additional counted reads over beforeGo→afterSettle (rchar +${rise.rchar}, syscr +${rise.syscr}); an increase is inconclusive, not dispatch evidence`,
819
+ rise.rchar === 0 && rise.syscr === 0,
820
+ );
821
+ } else if (REJECT) {
822
+ ok(
823
+ `[QK:PDR-BRIDGE-ACTIVITY] over beforeGo→afterSettle the bridge child made counted reads (rchar +${rise.rchar}, syscr +${rise.syscr}) — activity only, not a decoding of the request`,
824
+ rise.rchar > 0 && rise.syscr > 0,
825
+ );
826
+ const after = targetFacts();
827
+ causeAfterSettle = after;
828
+ ok(
829
+ `[QK:PDR-NO-SIDE-EFFECT] the target's mailbox holds no .msg and no inbox.signal (${fs.existsSync(box) ? fs.readdirSync(box).join(", ") || "empty" : "no mailbox dir"}), its record bytes are unchanged (${(causeBefore?.recordSha ?? "").slice(0, 12)} → ${after.recordSha.slice(0, 12)}), its sender marker is still there and its receiver marker is still absent`,
830
+ msgs.length === 0 &&
831
+ !fs.existsSync(path.join(box, "inbox.signal")) &&
832
+ after.recordSha === causeBefore?.recordSha &&
833
+ after.senderMarkerPresent &&
834
+ !after.receiverMarkerPresent,
835
+ );
836
+ } else {
837
+ ok(
838
+ `[QK:PDS-IO-CALIBRATED] calibration: over beforeGo→afterSettle the one dispatched call raised both bridge counters (rchar +${rise.rchar}, syscr +${rise.syscr}); no rise is a sensitivity GAP`,
839
+ rise.rchar > 0 && rise.syscr > 0,
840
+ );
841
+ }
842
+ if (VALID) {
843
+ ok(
844
+ `[QK:${QK}-DELIVERED-ONCE] the seeded target's mailbox holds exactly one .msg and its inbox.signal was poked (${msgs.join(", ") || "none"})`,
845
+ msgs.length === 1 && fs.existsSync(path.join(box, "inbox.signal")),
846
+ );
847
+ ok(
848
+ `[QK:${QK}-SENDER-IS-THE-HOST] the landed body names the host's garden id as a meta-session/pi-durable sender and carries the scripted message`,
849
+ body.includes("from: meta-session/pi-durable @") &&
850
+ body.includes(`session: ${hostGid} (meta-session, `) &&
851
+ body.includes(message),
852
+ );
853
+ // Replyability is a separate claim with its own oracle: the host's raw receiver marker, owned
854
+ // by the host this gate spawned (pid + /proc start key), read while the host is still alive.
855
+ const hostReceiverFile = metaReceiverMarkerPath(hostGid, dirs.receivers);
856
+ const hostReceiver = fs.existsSync(hostReceiverFile)
857
+ ? (JSON.parse(fs.readFileSync(hostReceiverFile, "utf8")) as Record<string, unknown>)
858
+ : {};
859
+ const hostArmed =
860
+ hostReceiver.gardenId === hostGid &&
861
+ hostReceiver.ownerPid === hostOwned.pid &&
862
+ hostReceiver.ownerStartKey === `linux:${hostOwned.start}` &&
863
+ hostReceiver.ownerKind === "pi-durable-host";
864
+ ok(
865
+ `[QK:${QK}-SENDER-REPLYABILITY] the host's receiver marker is armed and owned by the spawned host, and the landed body says what that predicts: replyable (armed ${hostArmed})`,
866
+ hostArmed && body.includes(`session: ${hostGid} (meta-session, replyable — reply via entwurf_v2`),
867
+ );
868
+ const hostRecords = fs
869
+ .readdirSync(dirs.sessions)
870
+ .filter((n) => n.endsWith(".meta.json"))
871
+ .map((n) => JSON.parse(fs.readFileSync(path.join(dirs.sessions, n), "utf8")) as Record<string, unknown>)
872
+ .filter((r) => r.backend === "pi-durable");
873
+ ok(
874
+ `[QK:${QK}-HOST-RECORD] that garden id is the one pi-durable record in the private store, keyed by the host's durable session`,
875
+ hostRecords.length === 1 &&
876
+ hostRecords[0]?.gardenId === hostGid &&
877
+ hostRecords[0]?.nativeSessionId === report.session.id,
878
+ );
879
+ }
880
+
881
+ if (!VALID) {
882
+ const durableRecords = fs
883
+ .readdirSync(dirs.sessions)
884
+ .filter((n) => n.endsWith(".meta.json"))
885
+ .map((n) => JSON.parse(fs.readFileSync(path.join(dirs.sessions, n), "utf8")) as Record<string, unknown>)
886
+ .filter((r) => r.backend === "pi-durable");
887
+ ok(
888
+ `[QK:${QK}-HOST-RECORD] the host's garden id is the one pi-durable record in the private store, keyed by the host's durable session`,
889
+ durableRecords.length === 1 &&
890
+ durableRecords[0]?.gardenId === hostGid &&
891
+ durableRecords[0]?.nativeSessionId === report.session.id,
892
+ );
893
+ }
894
+
895
+ // --- native view (what the app itself recorded; a PROJECTION by the host driver)
896
+ const calls = report.entries.filter((e) => e.kind === "pi.assistant").flatMap((e) => e.toolCalls);
897
+ const results = report.entries.filter((e) => e.kind === "pi.tool-result");
898
+ const lastAssistant = report.entries.filter((e) => e.kind === "pi.assistant").at(-1);
899
+ ok(
900
+ `[QK:${QK}-NATIVE-TOOL-CALL] the native view records exactly one tool call, ${CALL_ID} → entwurf_v2 with the scripted arguments`,
901
+ calls.length === 1 &&
902
+ calls[0]?.id === CALL_ID &&
903
+ calls[0]?.name === "entwurf_v2" &&
904
+ canonicalJson(calls[0]?.arguments) === canonicalJson(scriptedArgs),
905
+ );
906
+ if (INVALID) {
907
+ const diagnostics = results[0]?.diagnostics ?? [];
908
+ const diag = diagnostics[0];
909
+ ok(
910
+ `[QK:PDI-NATIVE-INVALID-ARGUMENTS] the native view records ${CALL_ID}'s settled result as an error whose one diagnostic is invalid_arguments, and its text is that diagnostic as the harness renders it — the validator's message names intent and echoes the received arguments (${JSON.stringify(resultTextInView.slice(0, 160))}…)`,
911
+ results.length === 1 &&
912
+ results[0]?.toolCallId === CALL_ID &&
913
+ results[0]?.isError === true &&
914
+ diagnostics.length === 1 &&
915
+ diag?.severity === "error" &&
916
+ diag?.code === "invalid_arguments" &&
917
+ resultTextInView === `<harness>\n[error] ${diag.message}\n</harness>` &&
918
+ diag.message.startsWith('Validation failed for tool "entwurf_v2":') &&
919
+ diag.message.includes("intent") &&
920
+ diag.message.includes("Received arguments:") &&
921
+ diag.message.includes(message),
922
+ );
923
+ } else if (REJECT) {
924
+ const diagnostics = results[0]?.diagnostics ?? [];
925
+ const diag = diagnostics[0];
926
+ ok(
927
+ `[QK:PDR-NATIVE-TOOL-ERROR] the native view records ${CALL_ID}'s settled result as an error whose one diagnostic is tool_error (not invalid_arguments), carrying the bridge's named refusal, and its text is that diagnostic as the harness renders it (${JSON.stringify(diag?.message ?? "")})`,
928
+ results.length === 1 &&
929
+ results[0]?.toolCallId === CALL_ID &&
930
+ results[0]?.isError === true &&
931
+ diagnostics.length === 1 &&
932
+ diag?.severity === "error" &&
933
+ diag?.code === "tool_error" &&
934
+ diag.message.startsWith("entwurf_v2 rejected: mailbox-undeliverable") &&
935
+ diag.message.includes("mailbox-undeliverable: self-fetch receiver inactive —") &&
936
+ resultTextInView === `<harness>\n[error] ${diag.message}\n</harness>`,
937
+ );
938
+ } else {
939
+ ok(
940
+ `[QK:PDS-NATIVE-TOOL-RESULT] the native view records that call's settled result, not an error, naming the .msg that landed (${JSON.stringify(resultTextInView)})`,
941
+ results.length === 1 &&
942
+ results[0]?.toolCallId === CALL_ID &&
943
+ results[0]?.isError === false &&
944
+ msgs.length === 1 &&
945
+ resultTextInView === `entwurf_v2 meta-mailbox → enqueued (${msgs[0]})`,
946
+ );
947
+ }
948
+ ok(
949
+ `[QK:${QK}-NATIVE-TURN-SETTLED] the turn ends on the scripted text and the conversation is no longer busy (last: ${JSON.stringify(lastAssistant?.text)}, busy ${report.busy})`,
950
+ lastAssistant?.text === FINAL_TEXT && lastAssistant.toolCalls.length === 0 && report.busy === false,
951
+ );
952
+ ok(
953
+ `[QK:${QK}-ROOT-CURRENT] input and turn stayed on the root conversation (${report.conversationIdBefore} → ${report.conversationIdAfter})`,
954
+ report.conversationIdBefore === 1 && report.conversationIdAfter === 1,
955
+ );
956
+ console.log(` notices: ${report.notices.join(" | ") || "none"}`);
957
+
958
+ context = {
959
+ listed,
960
+ report,
961
+ hostPid: hostOwned.pid,
962
+ targetGid,
963
+ targetRecordPath: target.path,
964
+ targetSenderMarker,
965
+ targetReceiverMarker,
966
+ msgs,
967
+ box,
968
+ };
969
+ host.stdin?.write("close\n");
970
+ const closed = await next(CLOSE_TIMEOUT_MS);
971
+ await Promise.race([exited, sleep(CLOSE_TIMEOUT_MS)]);
972
+ ok(
973
+ `[QK:${QK}-CLOSE] the host closes and exits 0 (closed ${String(closed.closed)}, exit ${exitCode})`,
974
+ closed.closed === true && exitCode === 0,
975
+ );
976
+ }
977
+
978
+ /** The wire assertions, judged on the closed run's final snapshot. */
979
+ function finalWire(ctx: CellContext): void {
980
+ console.log("3. wire, final snapshot (host exited, endpoint closed)");
981
+ const first = wire[0]?.body ?? {};
982
+ const offered = new Map(
983
+ ((first.tools as { function?: { name?: string; parameters?: unknown } }[] | undefined) ?? []).map((t) => [
984
+ t.function?.name ?? "",
985
+ t.function?.parameters,
986
+ ]),
987
+ );
988
+ ok(
989
+ `[QK:${QK}-WIRE-TOOLS-OFFERED] the app's first model request offers entwurf_self, entwurf_peers and entwurf_v2 (offered: ${[...offered.keys()].join(", ")})`,
990
+ ["entwurf_self", "entwurf_peers", "entwurf_v2"].every((name) => offered.has(name)),
991
+ );
992
+ ok(
993
+ `[QK:${QK}-WIRE-V2-SCHEMA-IS-BRIDGE] the entwurf_v2 parameters on the wire are the bridge's own tools/list inputSchema, as this gate listed it`,
994
+ ctx.listed.has("entwurf_v2") &&
995
+ canonicalJson(offered.get("entwurf_v2")) === canonicalJson(ctx.listed.get("entwurf_v2")),
996
+ );
997
+ ok(
998
+ `[QK:${QK}-WIRE-TWO-REQUESTS] over the whole closed run the endpoint saw exactly the two scripted requests, nothing unexpected (${wire.map((w) => `${w.method} ${w.url}`).join(" | ")})`,
999
+ !server.listening &&
1000
+ wire.length === 2 &&
1001
+ wire.every((w) => w.method === "POST" && w.url === "/v1/chat/completions"),
1002
+ );
1003
+ const second =
1004
+ (wire[1]?.body?.messages as { role?: string; tool_call_id?: string; content?: unknown }[] | undefined) ?? [];
1005
+ const toolMessages = second.filter((m) => m.role === "tool");
1006
+ const wireResult = messageText(toolMessages[0]?.content);
1007
+ const viewText = ctx.report.entries.find((e) => e.kind === "pi.tool-result")?.text ?? "";
1008
+ if (INVALID) {
1009
+ ok(
1010
+ `[QK:PDI-WIRE-VALIDATION-RESULT] the app's second model request carries ${CALL_ID}'s tool result with exactly the validation error text the native view recorded (got ${JSON.stringify(wireResult.slice(0, 120))}…)`,
1011
+ toolMessages.length === 1 &&
1012
+ toolMessages[0]?.tool_call_id === CALL_ID &&
1013
+ wireResult.startsWith('<harness>\n[error] Validation failed for tool "entwurf_v2":') &&
1014
+ wireResult === viewText,
1015
+ );
1016
+ } else if (REJECT) {
1017
+ ok(
1018
+ `[QK:PDR-WIRE-REJECT-RESULT] the app's second model request carries ${CALL_ID}'s tool result with exactly the refusal text the native view recorded (got ${JSON.stringify(wireResult.slice(0, 140))}…)`,
1019
+ toolMessages.length === 1 &&
1020
+ toolMessages[0]?.tool_call_id === CALL_ID &&
1021
+ wireResult.startsWith("<harness>\n[error] entwurf_v2 rejected: mailbox-undeliverable") &&
1022
+ wireResult === viewText,
1023
+ );
1024
+ } else {
1025
+ ok(
1026
+ `[QK:PDS-RESULT-REACHES-MODEL] the app's second model request carries the ${CALL_ID} tool result with the bridge's receipt for that .msg (got ${JSON.stringify(wireResult)})`,
1027
+ toolMessages.length === 1 &&
1028
+ toolMessages[0]?.tool_call_id === CALL_ID &&
1029
+ ctx.msgs.length === 1 &&
1030
+ wireResult === `entwurf_v2 meta-mailbox → enqueued (${ctx.msgs[0]})` &&
1031
+ wireResult === viewText,
1032
+ );
1033
+ }
1034
+ }
1035
+
1036
+ /** Copy the cell's raw bytes into the receipts directory, with a correlation summary. */
1037
+ function exportReceipts(dir: string, ctx: CellContext | undefined): void {
1038
+ const files: Record<string, string> = {};
1039
+ const write = (name: string, bytes: string | Buffer) => {
1040
+ const file = path.join(dir, name);
1041
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1042
+ fs.writeFileSync(file, bytes);
1043
+ files[name] = sha256(bytes);
1044
+ };
1045
+ const copy = (name: string, from: string) => write(name, fs.readFileSync(from));
1046
+ write("wire.json", `${JSON.stringify(wire, null, 1)}\n`);
1047
+ if (ctx === undefined) {
1048
+ write(
1049
+ "correlation.json",
1050
+ `${JSON.stringify({ cell: CELL, completed: false, bridgeIo: ioSnapshots, files }, null, 1)}\n`,
1051
+ );
1052
+ return;
1053
+ }
1054
+ write("bridge-tools.json", `${JSON.stringify(Object.fromEntries(ctx.listed), null, 1)}\n`);
1055
+ write(
1056
+ "native-view.json",
1057
+ `${JSON.stringify(
1058
+ {
1059
+ label:
1060
+ "PROJECTION of the durable app's native view by scripts/pi-durable-send-host.mjs (entry kind/role/tool calls/tool result/text/data.diagnostics, ids, busy, notices) — not native internal checkpoint state",
1061
+ report: ctx.report,
1062
+ },
1063
+ null,
1064
+ 1,
1065
+ )}\n`,
1066
+ );
1067
+ const boxNames = fs.existsSync(ctx.box) ? fs.readdirSync(ctx.box) : [];
1068
+ for (const name of boxNames) copy(`mailbox/${name}`, path.join(ctx.box, name));
1069
+ const signalFile = path.join(ctx.box, "inbox.signal");
1070
+ const signalMtime = fs.existsSync(signalFile) ? fs.statSync(signalFile).mtime.toISOString() : null;
1071
+ const hostRecord = fs
1072
+ .readdirSync(dirs.sessions)
1073
+ .filter((n) => n.endsWith(".meta.json"))
1074
+ .find(
1075
+ (n) =>
1076
+ (JSON.parse(fs.readFileSync(path.join(dirs.sessions, n), "utf8")) as { backend?: string }).backend ===
1077
+ "pi-durable",
1078
+ );
1079
+ if (hostRecord === undefined) throw new Error("no pi-durable record to export");
1080
+ copy(`garden/host-record/${hostRecord}`, path.join(dirs.sessions, hostRecord));
1081
+ copy(`garden/host-sender-marker/${ctx.hostPid}.json`, path.join(dirs.senders, "pi-durable", `${ctx.hostPid}.json`));
1082
+ copy(`garden/target-record/${path.basename(ctx.targetRecordPath)}`, ctx.targetRecordPath);
1083
+ copy(`garden/target-sender-marker/${path.basename(ctx.targetSenderMarker)}`, ctx.targetSenderMarker);
1084
+ if (ctx.targetReceiverMarker !== null) {
1085
+ copy(`garden/target-receiver-marker/${path.basename(ctx.targetReceiverMarker)}`, ctx.targetReceiverMarker);
1086
+ }
1087
+ const call = ctx.report.entries.flatMap((e) => e.toolCalls)[0];
1088
+ const result = ctx.report.entries.find((e) => e.kind === "pi.tool-result");
1089
+ write(
1090
+ "correlation.json",
1091
+ `${JSON.stringify(
1092
+ {
1093
+ cell: CELL,
1094
+ callId: call?.id,
1095
+ tool: call?.name,
1096
+ arguments: call?.arguments,
1097
+ receiptFile: ctx.msgs[0] ?? null,
1098
+ mailbox: { files: boxNames, msgCount: ctx.msgs.length, signal: signalMtime !== null },
1099
+ diagnostics: result?.diagnostics ?? [],
1100
+ bridgeIo: ioSnapshots,
1101
+ // The target's cause-side facts as observed, absence included; nothing absent is fabricated.
1102
+ targetFacts: { beforeInput: causeBefore ?? null, afterSettle: causeAfterSettle ?? null },
1103
+ targetReceiverMarker: ctx.targetReceiverMarker ?? "absent (not written by this cell)",
1104
+ receiptText: result?.text,
1105
+ resultIsError: result?.isError,
1106
+ senderGardenId: ctx.report.attachment.gardenId,
1107
+ targetGardenId: ctx.targetGid,
1108
+ root: { before: ctx.report.conversationIdBefore, after: ctx.report.conversationIdAfter },
1109
+ wireRequests: wire.length,
1110
+ inboxSignalMtime: signalMtime,
1111
+ files,
1112
+ },
1113
+ null,
1114
+ 1,
1115
+ )}\n`,
1116
+ );
1117
+ }
1118
+
1119
+ // ---------------------------------------------------------------------------
1120
+ // R1 — committed intent, unread request, kill, reopen, unsafe recovery
1121
+ // ---------------------------------------------------------------------------
1122
+ function procState(pid: number): string | null {
1123
+ try {
1124
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
1125
+ return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0] ?? null;
1126
+ } catch {
1127
+ return null;
1128
+ }
1129
+ }
1130
+
1131
+ interface LaunchedHost {
1132
+ owned: Owned;
1133
+ child: ReturnType<typeof spawn>;
1134
+ next(ms: number): Promise<Record<string, unknown>>;
1135
+ exited: Promise<void>;
1136
+ exit(): { code: number | null | undefined; signal: string | null | undefined };
1137
+ stderr(): string;
1138
+ }
1139
+
1140
+ async function launchHost(args: string[], what: string): Promise<LaunchedHost> {
1141
+ const child = spawn(process.execPath, ["--import", resolver, HOST_DRIVER, bridgeEntry, ...args], {
1142
+ cwd: dirs.project,
1143
+ env: hostEnv,
1144
+ stdio: ["pipe", "pipe", "pipe"],
1145
+ });
1146
+ let streamError = "";
1147
+ child.stdin?.on("error", (e) => {
1148
+ streamError += ` [${what} stdin error: ${e.message}]`;
1149
+ });
1150
+ const hostOwned = own(await spawned(child, what), "host");
1151
+ child.on("error", (e) => {
1152
+ streamError += ` [${what} process error: ${e.message}]`;
1153
+ });
1154
+ let stderr = "";
1155
+ child.stderr?.on("data", (c: Buffer) => {
1156
+ stderr = (stderr + c.toString("utf8")).slice(-8192);
1157
+ });
1158
+ const lines: string[] = [];
1159
+ const waiters: ((line: string | null) => void)[] = [];
1160
+ createInterface({ input: child.stdout as NodeJS.ReadableStream })
1161
+ .on("line", (line) => {
1162
+ const w = waiters.shift();
1163
+ if (w) w(line);
1164
+ else lines.push(line);
1165
+ })
1166
+ .on("close", () => {
1167
+ for (const w of waiters.splice(0)) w(null);
1168
+ });
1169
+ let exitCode: number | null | undefined;
1170
+ let exitSignal: string | null | undefined;
1171
+ const exited = new Promise<void>((resolve) =>
1172
+ child.on("exit", (code, signal) => {
1173
+ exitCode = code;
1174
+ exitSignal = signal;
1175
+ resolve();
1176
+ }),
1177
+ );
1178
+ return {
1179
+ owned: hostOwned,
1180
+ child,
1181
+ exited,
1182
+ exit: () => ({ code: exitCode, signal: exitSignal }),
1183
+ stderr: () => stderr,
1184
+ next: (ms) =>
1185
+ new Promise((resolve, reject) => {
1186
+ const settle = (line: string | null) => {
1187
+ if (line === null)
1188
+ return reject(new Error(`${what} ended without a line${streamError}; stderr: ${stderr.trim()}`));
1189
+ try {
1190
+ resolve(JSON.parse(line) as Record<string, unknown>);
1191
+ } catch (error) {
1192
+ reject(new Error(`${what} sent a malformed line (${(error as Error).message}): ${line.slice(0, 200)}`));
1193
+ }
1194
+ };
1195
+ const queued = lines.shift();
1196
+ if (queued !== undefined) return settle(queued);
1197
+ const timer = setTimeout(
1198
+ () =>
1199
+ reject(
1200
+ new Error(`GAP: no ${what} line within ${ms}ms (a cutoff, not a verdict); stderr: ${stderr.trim()}`),
1201
+ ),
1202
+ ms,
1203
+ );
1204
+ waiters.push((line) => {
1205
+ clearTimeout(timer);
1206
+ settle(line);
1207
+ });
1208
+ }),
1209
+ };
1210
+ }
1211
+
1212
+ interface R1Context {
1213
+ listed: Map<string, unknown>;
1214
+ born: Record<string, unknown>;
1215
+ intent: Record<string, unknown>;
1216
+ recovered: Report;
1217
+ targetGid: string;
1218
+ targetRecordPath: string;
1219
+ targetSenderMarker: string;
1220
+ targetReceiverMarker: string;
1221
+ box: string;
1222
+ host1Pid: number;
1223
+ host2Pid: number;
1224
+ pauseReceipt: Record<string, unknown>;
1225
+ killReceipt: Record<string, unknown>;
1226
+ forensic: { files: Record<string, string | null>; dir: string };
1227
+ inspection: Record<string, unknown>;
1228
+ }
1229
+ let r1Context: R1Context | undefined;
1230
+
1231
+ async function r1Cell(): Promise<void> {
1232
+ const targetOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 600000)"], {
1233
+ stdio: "ignore",
1234
+ env,
1235
+ cwd: dirs.project,
1236
+ });
1237
+ own(await spawned(targetOwner, "the target owner"), "target-owner");
1238
+ if (targetOwner.pid === undefined) throw new Error("the target owner has no pid");
1239
+ const target = upsertMetaSession({
1240
+ input: { backend: "claude-code", nativeSessionId: `pds-target-${process.pid}`, cwd: dirs.project },
1241
+ dir: dirs.sessions,
1242
+ });
1243
+ const targetGid = target.record.gardenId;
1244
+ const targetSenderMarker = writeMetaSenderMarker({
1245
+ backend: "claude-code",
1246
+ gardenId: targetGid,
1247
+ nativeSessionId: target.record.nativeSessionId,
1248
+ cwd: dirs.project,
1249
+ ownerPid: targetOwner.pid,
1250
+ sendersDir: dirs.senders,
1251
+ });
1252
+ const targetReceiverMarker = writeMetaReceiverMarker({
1253
+ gardenId: targetGid,
1254
+ backend: "claude-code",
1255
+ nativeSessionId: target.record.nativeSessionId,
1256
+ ownerPid: targetOwner.pid,
1257
+ armProvenance: "session-start",
1258
+ receiversDir: dirs.receivers,
1259
+ });
1260
+ const message = `pds r1 ${randomBytes(6).toString("hex")}`;
1261
+ scriptedArgs = { target: targetGid, intent: "fire-and-forget", message, wants_reply: false };
1262
+ const listed = await bridgeToolList();
1263
+
1264
+ console.log("1. r1 host1 — one native input; intent committed while the host's bridge is paused (S + H, V not run)");
1265
+ const h1 = await launchHost(["send the scripted message", "--await-go", "--report-intent", CALL_ID], "host1");
1266
+ const born = await h1.next(REPORT_TIMEOUT_MS);
1267
+ if (born.error !== undefined) throw new Error(`host1 failed before input: ${String(born.error)}`);
1268
+ if (born.born !== true) throw new Error(`host1 sent no born line: ${JSON.stringify(born).slice(0, 200)}`);
1269
+ if (!observeBridges(h1.owned)) throw new Error(`GAP: host1 ${h1.owned.pid}'s children were not observed coherently`);
1270
+ const bridges = owned.filter((p) => p.what === "bridge");
1271
+ if (bridges.length !== 1)
1272
+ throw new Error(`GAP: expected exactly one bridge child of host1, observed ${bridges.length}`);
1273
+ const bridge1 = bridges[0] as Owned;
1274
+ const idleA = ioSnapshot(bridge1, "idleA");
1275
+ await sleep(IDLE_SAMPLE_MS);
1276
+ const idleB = ioSnapshot(bridge1, "idleB");
1277
+ if (idleA.rchar !== idleB.rchar || idleA.syscr !== idleB.syscr) {
1278
+ throw new Error(`GAP: bridge1's counters moved in the sampled idle window; the read-count oracle cannot be used`);
1279
+ }
1280
+ // Pause exactly the observed bridge; confirm the kernel shows it stopped before the input.
1281
+ if (procStat(bridge1.pid)?.start !== bridge1.start) throw new Error("GAP: bridge1 changed identity before pause");
1282
+ process.kill(bridge1.pid, "SIGSTOP");
1283
+ bridge1.stopped = true;
1284
+ const pauseDeadline = Date.now() + REAP_TIMEOUT_MS;
1285
+ while (Date.now() < pauseDeadline && procState(bridge1.pid) !== "T") await sleep(20);
1286
+ const pauseReceipt = {
1287
+ pid: bridge1.pid,
1288
+ startKey: bridge1.start,
1289
+ state: procState(bridge1.pid),
1290
+ startKeyNow: procStat(bridge1.pid)?.start,
1291
+ at: new Date().toISOString(),
1292
+ };
1293
+ if (pauseReceipt.state !== "T" || pauseReceipt.startKeyNow !== bridge1.start) {
1294
+ throw new Error(`GAP: bridge1 is not observed stopped before the input (${JSON.stringify(pauseReceipt)})`);
1295
+ }
1296
+ ok(
1297
+ `[QK:PR1-BRIDGE-PAUSED] bridge1 ${bridge1.pid} is observed stopped (state T, same start key) before the input`,
1298
+ true,
1299
+ );
1300
+ const beforeGo = ioSnapshot(bridge1, "beforeGo");
1301
+ h1.child.stdin?.write("go\n");
1302
+ const goAt = Date.now();
1303
+
1304
+ const intent = await h1.next(INTENT_DEADLINE_MS);
1305
+ if (intent.error !== undefined) throw new Error(`host1 failed after input: ${String(intent.error)}`);
1306
+ if (intent.intent !== true)
1307
+ throw new Error(`host1 reported something other than the intent frame: ${JSON.stringify(intent).slice(0, 300)}`);
1308
+ const frame = intent.frame as {
1309
+ node: { id: number; kind: string; conversationId: number; state: { status: string; phase: string } };
1310
+ slot: { taskId: number; name: string; callId: string; status: string };
1311
+ };
1312
+ const intentVerdict = intentFrameVerdict(frame, CALL_ID);
1313
+ ok(
1314
+ `[QK:PR1-INTENT-OBSERVED] the native task graph shows the pi.tool node ${frame.node?.id} running in phase execute on the root conversation, and the live slot with the same taskId is ${CALL_ID} → entwurf_v2, running${intentVerdict.ok ? "" : ` — refused: ${intentVerdict.reasons.join("; ")}`}`,
1315
+ intentVerdict.ok,
1316
+ );
1317
+ // A frame that does not match stops the kill/copy/reopen flow here; owned cleanup still runs.
1318
+ if (!intentVerdict.ok) throw new Error(`GAP: the reported frame is not the committed intent of ${CALL_ID}`);
1319
+
1320
+ // Kill host1 at once; the bridge stays paused while its counters are read, then is killed too.
1321
+ if (procStat(h1.owned.pid)?.start !== h1.owned.start) throw new Error("GAP: host1 changed identity before kill");
1322
+ const killAt = Date.now();
1323
+ process.kill(h1.owned.pid, "SIGKILL");
1324
+ await Promise.race([h1.exited, sleep(REAP_TIMEOUT_MS)]);
1325
+ const afterHostKill = ioSnapshot(bridge1, "afterHostKill");
1326
+ const bridgeStateAtRead = procState(bridge1.pid);
1327
+ // The signal names the observed paused process: its start key is re-read right before it.
1328
+ if (procStat(bridge1.pid)?.start !== bridge1.start)
1329
+ throw new Error(`GAP: bridge1 ${bridge1.pid} is not the observed process immediately before its SIGKILL`);
1330
+ process.kill(bridge1.pid, "SIGKILL");
1331
+ const goneDeadline = Date.now() + REAP_TIMEOUT_MS;
1332
+ while (Date.now() < goneDeadline && procStat(bridge1.pid)?.start === bridge1.start) await sleep(20);
1333
+ const killReceipt = {
1334
+ host1: { pid: h1.owned.pid, ...h1.exit() },
1335
+ bridge1: { pid: bridge1.pid, stateAtRead: bridgeStateAtRead, gone: procStat(bridge1.pid)?.start !== bridge1.start },
1336
+ at: new Date().toISOString(),
1337
+ };
1338
+ ok(
1339
+ `[QK:PR1-OLD-BRIDGE-READ-NOTHING] paused bridge1 made no counted reads from beforeGo to after host1's kill (rchar +${afterHostKill.rchar - beforeGo.rchar}, syscr +${afterHostKill.syscr - beforeGo.syscr}, state ${bridgeStateAtRead}) — it consumed no request; bytes may still have been written to its pipe`,
1340
+ afterHostKill.rchar === beforeGo.rchar && afterHostKill.syscr === beforeGo.syscr && bridgeStateAtRead === "T",
1341
+ );
1342
+ ok(
1343
+ `[QK:PR1-KILLED] host1 died by SIGKILL and paused bridge1 is gone (${JSON.stringify(killReceipt)})`,
1344
+ killReceipt.host1.signal === "SIGKILL" && killReceipt.bridge1.gone,
1345
+ );
1346
+ // Nothing is copied or reopened unless every pre-copy fact holds, timing included.
1347
+ const preCopy = preCopyVerdict({
1348
+ hostExitSignal: killReceipt.host1.signal,
1349
+ bridgeStateAtRead,
1350
+ bridgeGone: killReceipt.bridge1.gone,
1351
+ readDelta: { rchar: afterHostKill.rchar - beforeGo.rchar, syscr: afterHostKill.syscr - beforeGo.syscr },
1352
+ goAt,
1353
+ killAt,
1354
+ deadlineMs: INTENT_DEADLINE_MS,
1355
+ });
1356
+ ok(
1357
+ `[QK:PR1-PRE-COPY] go→kill ${killAt - goAt}ms within the ${INTENT_DEADLINE_MS}ms validity cutoff (a cutoff, not a success delay), host1 SIGKILLed, bridge1 read nothing while stopped and is gone${preCopy.ok ? "" : ` — refused: ${preCopy.reasons.join("; ")}`}`,
1358
+ preCopy.ok,
1359
+ );
1360
+ if (!preCopy.ok)
1361
+ throw new Error(`GAP: pre-copy conditions failed (${preCopy.reasons.join("; ")}); no database copy, no reopen`);
1362
+
1363
+ // Forensic copy of the quiescent database set, then a read-only look at a SEPARATE working copy.
1364
+ const session = born.session as { id: string; directory: string };
1365
+ const forensicDir = path.join(root, "forensic");
1366
+ const workDir = path.join(root, "forensic-work");
1367
+ fs.mkdirSync(forensicDir);
1368
+ fs.mkdirSync(workDir);
1369
+ const forensicFiles: Record<string, string | null> = {};
1370
+ for (const name of ["session.sqlite", "session.sqlite-wal", "session.sqlite-shm"]) {
1371
+ const from = path.join(session.directory, name);
1372
+ if (fs.existsSync(from)) {
1373
+ fs.copyFileSync(from, path.join(forensicDir, name));
1374
+ fs.copyFileSync(from, path.join(workDir, name));
1375
+ forensicFiles[name] = sha256(fs.readFileSync(path.join(forensicDir, name)));
1376
+ } else forensicFiles[name] = null;
1377
+ }
1378
+ const { DatabaseSync } = await import("node:sqlite");
1379
+ let inspection: Record<string, unknown>;
1380
+ try {
1381
+ const db = new DatabaseSync(path.join(workDir, "session.sqlite"), { readOnly: true });
1382
+ try {
1383
+ const row = db
1384
+ .prepare("SELECT id, conversation_id, kind, status, record FROM tasks WHERE id = ?")
1385
+ .get(frame.node.id) as
1386
+ | { id: number; conversation_id: number; kind: string; status: string; record: string }
1387
+ | undefined;
1388
+ const entryRows = db.prepare("SELECT id, record FROM entries ORDER BY id").all() as {
1389
+ id: number;
1390
+ record: string;
1391
+ }[];
1392
+ const entries = entryRows.map(
1393
+ (r) =>
1394
+ JSON.parse(r.record) as {
1395
+ kind?: string;
1396
+ model?: { role?: string; toolCallId?: string; content?: unknown }[];
1397
+ },
1398
+ );
1399
+ inspection = {
1400
+ row: row ?? null,
1401
+ record: row ? JSON.parse(row.record) : null,
1402
+ resultEntriesForCall: entries.filter((e) => e.kind === "pi.tool-result" && e.model?.[0]?.toolCallId === CALL_ID)
1403
+ .length,
1404
+ entryKinds: entries.map((e) => e.kind),
1405
+ };
1406
+ } finally {
1407
+ db.close();
1408
+ }
1409
+ } catch (error) {
1410
+ throw new Error(`GAP: the copied durable database could not be inspected read-only: ${(error as Error).message}`);
1411
+ }
1412
+ const checkpoint = checkpointVerdict(
1413
+ {
1414
+ row: inspection.row as { id?: unknown } | null,
1415
+ record: inspection.record as Parameters<typeof checkpointVerdict>[0]["record"],
1416
+ resultEntriesForCall: inspection.resultEntriesForCall as number,
1417
+ },
1418
+ { nodeId: frame.node.id, callId: CALL_ID, args: scriptedArgs },
1419
+ );
1420
+ ok(
1421
+ `[QK:PR1-RAW-CHECKPOINT] before reopening: the copied database's row and record ${frame.node.id} are this call's pi.tool task (input callId ${CALL_ID}), running, checkpoint execute / unsafe / the scripted arguments, with no result entry for it (files ${JSON.stringify(forensicFiles)})${checkpoint.ok ? "" : ` — refused: ${checkpoint.reasons.join("; ")}`}`,
1422
+ checkpoint.ok,
1423
+ );
1424
+ if (!checkpoint.ok) throw new Error("GAP: the raw committed checkpoint does not match; host2 is not started");
1425
+
1426
+ console.log("2. r1 host2 — continue the same session, no input; native recovery settles the call");
1427
+ const h2 = await launchHost(["-", "--recover", CALL_ID], "host2");
1428
+ const recovered = (await h2.next(REPORT_TIMEOUT_MS)) as unknown as Report & { recovered?: boolean };
1429
+ if (recovered.error !== undefined) throw new Error(`host2 failed: ${recovered.error}; stderr: ${h2.stderr().trim()}`);
1430
+ if (!observeBridges(h2.owned)) throw new Error(`GAP: host2 ${h2.owned.pid}'s children were not observed coherently`);
1431
+ const attachment = recovered.attachment as unknown as { gardenId: string; action?: string };
1432
+ const bornAttachment = born.attachment as { gardenId: string };
1433
+ const records = fs
1434
+ .readdirSync(dirs.sessions)
1435
+ .filter((n) => n.endsWith(".meta.json"))
1436
+ .map((n) => JSON.parse(fs.readFileSync(path.join(dirs.sessions, n), "utf8")) as Record<string, unknown>)
1437
+ .filter((r) => r.backend === "pi-durable");
1438
+ ok(
1439
+ `[QK:PR1-SAME-SESSION] host2 continued the same native session and garden: session ${String((recovered.session as unknown as { id: string }).id)} == ${session.id}, attach action ${String(attachment.action)}, garden ${attachment.gardenId}, one pi-durable record, a marker for host2's pid`,
1440
+ (recovered.session as unknown as { id: string; directory: string }).id === session.id &&
1441
+ (recovered.session as unknown as { directory: string }).directory === session.directory &&
1442
+ attachment.gardenId === bornAttachment.gardenId &&
1443
+ attachment.action === "attach" &&
1444
+ records.length === 1 &&
1445
+ fs.existsSync(path.join(dirs.senders, "pi-durable", `${h2.owned.pid}.json`)),
1446
+ );
1447
+ const results = recovered.entries.filter((e) => e.kind === "pi.tool-result" && e.toolCallId === CALL_ID);
1448
+ const calls = recovered.entries.filter((e) => e.kind === "pi.assistant").flatMap((e) => e.toolCalls);
1449
+ const diag = results[0]?.diagnostics?.[0];
1450
+ const lastAssistant = recovered.entries.filter((e) => e.kind === "pi.assistant").at(-1);
1451
+ ok(
1452
+ `[QK:PR1-RECOVERED-INTERRUPTED] the recovered view holds exactly one result for ${CALL_ID}: an error whose one diagnostic is interrupted with the native message, rendered by the harness — and exactly one tool call in the whole conversation`,
1453
+ results.length === 1 &&
1454
+ results[0]?.isError === true &&
1455
+ results[0]?.diagnostics?.length === 1 &&
1456
+ diag?.severity === "error" &&
1457
+ diag?.code === "interrupted" &&
1458
+ diag.message === INTERRUPTED_MESSAGE &&
1459
+ results[0]?.text === `<harness>\n[error] ${INTERRUPTED_MESSAGE}\n</harness>` &&
1460
+ calls.length === 1 &&
1461
+ calls[0]?.id === CALL_ID,
1462
+ );
1463
+ ok(
1464
+ `[QK:PR1-TURN-SETTLED] the recovered turn ends on the scripted text, idle, on the root conversation (last ${JSON.stringify(lastAssistant?.text)}, busy ${recovered.busy}, ${recovered.conversationIdAfter})`,
1465
+ lastAssistant?.text === FINAL_TEXT &&
1466
+ lastAssistant.toolCalls.length === 0 &&
1467
+ recovered.busy === false &&
1468
+ recovered.conversationIdAfter === 1,
1469
+ );
1470
+ const box = path.join(dirs.mailbox, targetGid);
1471
+ const msgs = fs.existsSync(box) ? fs.readdirSync(box).filter((f) => f.endsWith(".msg")) : [];
1472
+ ok(
1473
+ `[QK:PR1-NO-SIDE-EFFECT] side-effect check: the armed target's mailbox holds no .msg and no inbox.signal (${fs.existsSync(box) ? fs.readdirSync(box).join(", ") || "empty" : "no mailbox dir"}) — no-resend itself is an inference, not this check`,
1474
+ msgs.length === 0 && !fs.existsSync(path.join(box, "inbox.signal")),
1475
+ );
1476
+ console.log(` notices: ${recovered.notices.join(" | ") || "none"}`);
1477
+ r1Context = {
1478
+ listed,
1479
+ born,
1480
+ intent,
1481
+ recovered,
1482
+ targetGid,
1483
+ targetRecordPath: target.path,
1484
+ targetSenderMarker,
1485
+ targetReceiverMarker,
1486
+ box,
1487
+ host1Pid: h1.owned.pid,
1488
+ host2Pid: h2.owned.pid,
1489
+ pauseReceipt,
1490
+ killReceipt,
1491
+ forensic: { files: forensicFiles, dir: forensicDir },
1492
+ inspection,
1493
+ };
1494
+ h2.child.stdin?.write("close\n");
1495
+ const closed = await h2.next(CLOSE_TIMEOUT_MS);
1496
+ await Promise.race([h2.exited, sleep(CLOSE_TIMEOUT_MS)]);
1497
+ ok(
1498
+ `[QK:PR1-CLOSE] host2 closes and exits 0 (closed ${String(closed.closed)}, exit ${h2.exit().code})`,
1499
+ closed.closed === true && h2.exit().code === 0,
1500
+ );
1501
+ }
1502
+
1503
+ function finalWireR1(ctx: R1Context): void {
1504
+ console.log("3. wire, final snapshot (hosts exited, endpoint closed)");
1505
+ const first = wire[0]?.body ?? {};
1506
+ const offered = new Map(
1507
+ ((first.tools as { function?: { name?: string; parameters?: unknown } }[] | undefined) ?? []).map((t) => [
1508
+ t.function?.name ?? "",
1509
+ t.function?.parameters,
1510
+ ]),
1511
+ );
1512
+ ok(
1513
+ `[QK:PR1-WIRE-TOOLS-OFFERED] host1's model request offers entwurf_v2 with the bridge's own tools/list inputSchema`,
1514
+ ctx.listed.has("entwurf_v2") &&
1515
+ canonicalJson(offered.get("entwurf_v2")) === canonicalJson(ctx.listed.get("entwurf_v2")),
1516
+ );
1517
+ ok(
1518
+ `[QK:PR1-WIRE-TWO-REQUESTS] over the whole closed run the endpoint saw exactly two requests: host1's and host2's recovery (${wire.map((w) => `${w.method} ${w.url}`).join(" | ")})`,
1519
+ !server.listening &&
1520
+ wire.length === 2 &&
1521
+ wire.every((w) => w.method === "POST" && w.url === "/v1/chat/completions"),
1522
+ );
1523
+ const second =
1524
+ (wire[1]?.body?.messages as { role?: string; tool_call_id?: string; content?: unknown }[] | undefined) ?? [];
1525
+ const toolMessages = second.filter((m) => m.role === "tool");
1526
+ const viewText = ctx.recovered.entries.find((e) => e.kind === "pi.tool-result")?.text ?? "";
1527
+ ok(
1528
+ `[QK:PR1-WIRE-RECOVERY] the recovery request carries ${CALL_ID}'s interrupted result exactly as the recovered view holds it`,
1529
+ toolMessages.length === 1 &&
1530
+ toolMessages[0]?.tool_call_id === CALL_ID &&
1531
+ messageText(toolMessages[0]?.content) === viewText,
1532
+ );
1533
+ }
1534
+
1535
+ function exportR1Receipts(dir: string, ctx: R1Context | undefined): void {
1536
+ const files: Record<string, string> = {};
1537
+ const write = (name: string, bytes: string | Buffer) => {
1538
+ const file = path.join(dir, name);
1539
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1540
+ fs.writeFileSync(file, bytes);
1541
+ files[name] = sha256(bytes);
1542
+ };
1543
+ const copy = (name: string, from: string) => write(name, fs.readFileSync(from));
1544
+ write("wire.json", `${JSON.stringify(wire, null, 1)}\n`);
1545
+ if (ctx === undefined) {
1546
+ write(
1547
+ "correlation.json",
1548
+ `${JSON.stringify({ cell: CELL, completed: false, bridgeIo: ioSnapshots, files }, null, 1)}\n`,
1549
+ );
1550
+ return;
1551
+ }
1552
+ write("bridge-tools.json", `${JSON.stringify(Object.fromEntries(ctx.listed), null, 1)}\n`);
1553
+ write("host1-born.json", `${JSON.stringify(ctx.born, null, 1)}\n`);
1554
+ write(
1555
+ "host1-intent-frame.json",
1556
+ `${JSON.stringify({ label: "the native view frame host1 reported (task graph nodes + live slots + entries PROJECTION)", intent: ctx.intent }, null, 1)}\n`,
1557
+ );
1558
+ write(
1559
+ "host2-recovered-view.json",
1560
+ `${JSON.stringify({ label: "PROJECTION of host2's native view after recovery", report: ctx.recovered }, null, 1)}\n`,
1561
+ );
1562
+ for (const [name, digest] of Object.entries(ctx.forensic.files)) {
1563
+ if (digest !== null) copy(`forensic-sqlite/${name}`, path.join(ctx.forensic.dir, name));
1564
+ }
1565
+ write("sqlite-inspection.json", `${JSON.stringify(ctx.inspection, null, 1)}\n`);
1566
+ const boxNames = fs.existsSync(ctx.box) ? fs.readdirSync(ctx.box) : [];
1567
+ for (const name of boxNames) copy(`mailbox/${name}`, path.join(ctx.box, name));
1568
+ copy(`garden/target-record/${path.basename(ctx.targetRecordPath)}`, ctx.targetRecordPath);
1569
+ copy(`garden/target-sender-marker/${path.basename(ctx.targetSenderMarker)}`, ctx.targetSenderMarker);
1570
+ copy(`garden/target-receiver-marker/${path.basename(ctx.targetReceiverMarker)}`, ctx.targetReceiverMarker);
1571
+ for (const pid of [ctx.host1Pid, ctx.host2Pid]) {
1572
+ const marker = path.join(dirs.senders, "pi-durable", `${pid}.json`);
1573
+ if (fs.existsSync(marker)) copy(`garden/host-sender-marker/${pid}.json`, marker);
1574
+ }
1575
+ for (const name of fs.readdirSync(dirs.sessions).filter((n) => n.endsWith(".meta.json"))) {
1576
+ copy(`garden/records/${name}`, path.join(dirs.sessions, name));
1577
+ }
1578
+ write(
1579
+ "correlation.json",
1580
+ `${JSON.stringify(
1581
+ {
1582
+ cell: CELL,
1583
+ callId: CALL_ID,
1584
+ arguments: scriptedArgs,
1585
+ intentNode: (ctx.intent.frame as { node?: unknown } | undefined)?.node,
1586
+ intentSlot: (ctx.intent.frame as { slot?: unknown } | undefined)?.slot,
1587
+ pause: ctx.pauseReceipt,
1588
+ kill: ctx.killReceipt,
1589
+ bridgeIo: ioSnapshots,
1590
+ forensicSqlite: ctx.forensic.files,
1591
+ rawTaskRow: ctx.inspection.row,
1592
+ resultEntriesBeforeReopen: ctx.inspection.resultEntriesForCall,
1593
+ recoveredResult: ctx.recovered.entries.find((e) => e.kind === "pi.tool-result") ?? null,
1594
+ session: { host1: ctx.born.session, host2: ctx.recovered.session },
1595
+ garden: {
1596
+ host1: (ctx.born.attachment as { gardenId?: string }).gardenId,
1597
+ host2: ctx.recovered.attachment as unknown as { gardenId?: string; action?: string },
1598
+ },
1599
+ mailbox: { files: boxNames },
1600
+ wireRequests: wire.length,
1601
+ files,
1602
+ },
1603
+ null,
1604
+ 1,
1605
+ )}\n`,
1606
+ );
1607
+ }
1608
+
1609
+ let primary: unknown;
1610
+ try {
1611
+ await (R1 ? r1Cell() : cell());
1612
+ } catch (error) {
1613
+ primary = error;
1614
+ }
1615
+ const cleanupProblems = await reapAll();
1616
+ // The host is gone, so every client socket is ours to drop; the close is still bounded.
1617
+ server.closeAllConnections();
1618
+ const endpointClosed = await Promise.race([
1619
+ new Promise<boolean>((resolve) => server.close(() => resolve(true))),
1620
+ sleep(REAP_TIMEOUT_MS).then(() => false),
1621
+ ]);
1622
+ console.log("2. cleanup and operator guard");
1623
+ const alive = owned.filter((p) => procStat(p.pid)?.start === p.start);
1624
+ const reap = ownedReapVerdict({
1625
+ alive: alive.map((p) => p.pid),
1626
+ problems: cleanupProblems,
1627
+ endpointClosed,
1628
+ endpointListening: server.listening,
1629
+ observationNotes,
1630
+ });
1631
+ ok(
1632
+ `[QK:${QK}-OWNED-REAPED] every process this gate owns is gone, every owned host's children were observed, and the endpoint is closed (${owned.map((p) => `${p.what} ${p.pid}`).join(", ") || "none"}; ${reap.green ? "complete" : reap.reasons.join("; ")})`,
1633
+ reap.green,
1634
+ );
1635
+ if (R1) {
1636
+ if (r1Context !== undefined) finalWireR1(r1Context);
1637
+ } else if (context !== undefined) finalWire(context);
1638
+ if (receiptsInput !== undefined) {
1639
+ try {
1640
+ if (R1) exportR1Receipts(receiptsInput, r1Context);
1641
+ else exportReceipts(receiptsInput, context);
1642
+ ok(`[QK:${QK}-RECEIPTS-EXPORTED] the cell's raw bytes and correlation are in ${receiptsInput}`, true);
1643
+ } catch (error) {
1644
+ ok(`[QK:${QK}-RECEIPTS-EXPORTED] receipt export failed: ${(error as Error).message}`, false);
1645
+ }
1646
+ }
1647
+ const before = JSON.parse(operatorBefore) as { durableRecords: string[]; senders: boolean };
1648
+ ok(
1649
+ `[QK:${QK}-OPERATOR-UNTOUCHED] coarse pi-durable guard: the operator's own roots are unchanged by this run and hold no pi-durable record or sender dir (${operatorBefore})`,
1650
+ operatorPiDurableFacts() === operatorBefore && before.durableRecords.length === 0 && !before.senders,
1651
+ );
1652
+ if (alive.length > 0) console.log(`sandbox kept because owned processes are still alive: ${root}`);
1653
+ else if (process.env.ENTWURF_PI_DURABLE_KEEP === "1") console.log(`sandbox kept: ${root}`);
1654
+ else fs.rmSync(root, { recursive: true, force: true });
1655
+ if (primary !== undefined)
1656
+ console.error(`${LABEL}: FAILED — ${primary instanceof Error ? primary.message : String(primary)}`);
1657
+ for (const problem of cleanupProblems) console.error(`${LABEL}: cleanup — ${problem}`);
1658
+ console.log(
1659
+ `\n${LABEL}: ${passed} passed, ${failed} failed — S (scripted endpoint) + H (native ToolTask); V not run; checkout-only`,
1660
+ );
1661
+ process.exit(primary === undefined && failed === 0 && reap.green ? 0 : 1);