@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.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +112 -0
- package/DELIVERY.md +1 -0
- package/README.md +47 -7
- package/VERIFY.md +17 -1
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +104 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
- package/mcp/entwurf-bridge/src/index.ts +7 -7
- package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
- package/package.json +13 -10
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/pi-durable/bootstrap.mjs +281 -0
- package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
- package/pi/pi-durable/overlay/upstream-pin.json +16 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
- package/pi-extensions/lib/fresh-call-composition.ts +42 -7
- package/pi-extensions/lib/meta-sender-identity.ts +7 -1
- package/pi-extensions/lib/meta-session.ts +28 -2
- package/pi-extensions/lib/mux-fresh-call.ts +34 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
- package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
- package/run.sh +200 -33
- package/scripts/check-fresh-call-dispatch.ts +18 -0
- package/scripts/check-gate-qualification.ts +2 -1
- package/scripts/check-pi-durable-contact.ts +834 -0
- package/scripts/check-pi-durable-receive.ts +1116 -0
- package/scripts/check-pi-durable-send.ts +1661 -0
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
- package/scripts/lib/pi-durable-r1.ts +100 -0
- package/scripts/lib/pi-durable-reap.ts +27 -0
- package/scripts/lib/pi-durable-receive.ts +137 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/copilot-birth.json +2 -2
- package/scripts/mutants/meta-hook-session-switch.json +4 -2
- package/scripts/mutants/mux-fresh-call.json +69 -3
- package/scripts/mutants/omp-birth.json +2 -2
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +390 -0
- package/scripts/mutants/pi-floor.json +2 -2
- package/scripts/pi-durable-contact-host.mjs +105 -0
- package/scripts/pi-durable-receive-host.mjs +127 -0
- package/scripts/pi-durable-send-host.mjs +213 -0
- package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
- package/scripts/smoke-setup-verdict.sh +21 -2
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-receive (#129 L-receive, FIRST CELL) — one mailbox message reaches an idle durable host and its
|
|
3
|
+
* model drains it. CHECKOUT-ONLY dev gate: an installed package refuses it in `run_ts` before it
|
|
4
|
+
* reads an input.
|
|
5
|
+
*
|
|
6
|
+
* EVIDENCE BOXES, kept apart and never substituted for one another:
|
|
7
|
+
* S local scripted model protocol — a 127.0.0.1 OpenAI-compatible endpoint THIS gate serves through
|
|
8
|
+
* the app's documented `models.json` compatible-endpoint support, dummy key, no vendor. Request
|
|
9
|
+
* #1 is answered with one `entwurf_inbox_read` call — but only after the gate has captured the
|
|
10
|
+
* enqueue it must explain (the HOLD below); request #2 with text.
|
|
11
|
+
* H actual native harness path — the host driver gives the app NO input
|
|
12
|
+
* (scripts/pi-durable-receive-host.mjs). The only thing that can start a run is the contact's
|
|
13
|
+
* doorbell admitting its notice to the ROOT conversation through the overlay's `submitToRoot`.
|
|
14
|
+
* V an actual vendor model turn — NOT run here.
|
|
15
|
+
*
|
|
16
|
+
* Sender: a SEEDED claude-code citizen whose record and sender marker are owned by a separate idle
|
|
17
|
+
* child of this gate, so no marker is keyed to the gate's own pid. The sending bridge is a child of
|
|
18
|
+
* this gate handed that marker through the explicit carrier `ENTWURF_META_SENDER_MARKER`
|
|
19
|
+
* (meta-sender-identity.ts: an explicit path is read with the live-owner guard and no ancestry scan),
|
|
20
|
+
* and its own `entwurf_self` is asked who it is before it sends — the resolver's answer is measured,
|
|
21
|
+
* not assumed.
|
|
22
|
+
*
|
|
23
|
+
* THE HOLD. The host's doorbell stamps the fresh `.msg` `.delivered` BEFORE it admits the notice, and
|
|
24
|
+
* the model's `entwurf_inbox_read` archives it to `.delivered.read` — so the enqueued body exists under
|
|
25
|
+
* its first name only until the ring, and under its second only until the drain. The endpoint therefore
|
|
26
|
+
* withholds response #1 at the actual request #1 until this gate has captured the ENQUEUE receipt, that
|
|
27
|
+
* file's `.delivered` bytes, the signal, the host's raw receiver marker and the notice; only then is
|
|
28
|
+
* the tool call released. A failed capture is a named GAP thrown BEFORE release: no sleep, no timed
|
|
29
|
+
* success, no fabricated tool result. `.delivered` is a wake attempt, not a read and not a native
|
|
30
|
+
* commit.
|
|
31
|
+
*
|
|
32
|
+
* SQLITE. After every owned process is gone (reap verdict green), the durable session's
|
|
33
|
+
* `session.sqlite`, `-wal`, `-shm` are copied untouched to a forensic set and, separately, to a work
|
|
34
|
+
* set that alone is opened read-only. The submission is selected by the doorbell's request id in its
|
|
35
|
+
* record — and the indexed `request_id` column, under upstream's JSON encoding, must agree — and its row
|
|
36
|
+
* id and record id must both be the id the receiver reported, on the root conversation, an input, done,
|
|
37
|
+
* whose placed entry is the notice. Without a green reap nothing is copied. Row, wire and archive side
|
|
38
|
+
* effects are separate QKs.
|
|
39
|
+
*
|
|
40
|
+
* Intended single idle cell: controller input 0, doorbell admission 1, S requests 2, V 0.
|
|
41
|
+
*
|
|
42
|
+
* Inputs as check-pi-durable-contact (missing → SKIP 97, never a pass):
|
|
43
|
+
* ENTWURF_PI_DURABLE_RUNTIME, ENTWURF_PI_DURABLE_BRIDGE_ENTRY (a bundle carrying the owner join)
|
|
44
|
+
* Optional:
|
|
45
|
+
* ENTWURF_PI_DURABLE_RECEIPTS a NEW absolute directory (refused if it exists) for the cell's raw bytes.
|
|
46
|
+
*
|
|
47
|
+
* Every wire assertion is judged on the FINAL snapshot, after the host has exited and the endpoint is
|
|
48
|
+
* closed, so a late request cannot slip past "exactly two".
|
|
49
|
+
*/
|
|
50
|
+
import { spawn } from "node:child_process";
|
|
51
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
52
|
+
import * as fs from "node:fs";
|
|
53
|
+
import * as http from "node:http";
|
|
54
|
+
import type { AddressInfo } from "node:net";
|
|
55
|
+
import * as os from "node:os";
|
|
56
|
+
import * as path from "node:path";
|
|
57
|
+
import { createInterface } from "node:readline";
|
|
58
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
59
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
60
|
+
import { metaReceiverMarkerPath, upsertMetaSession, writeMetaSenderMarker } from "../pi-extensions/lib/meta-session.ts";
|
|
61
|
+
import {
|
|
62
|
+
inspectPiDurableRuntime,
|
|
63
|
+
PI_DURABLE_RESOLVER_RELATIVE,
|
|
64
|
+
readPiDurablePin,
|
|
65
|
+
} from "../pi-extensions/lib/pi-durable-runtime.ts";
|
|
66
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
67
|
+
import { ownedReapVerdict } from "./lib/pi-durable-reap.ts";
|
|
68
|
+
import {
|
|
69
|
+
encodedRequestIdColumn,
|
|
70
|
+
enqueuedFile,
|
|
71
|
+
heldCaptureVerdict,
|
|
72
|
+
submissionVerdict,
|
|
73
|
+
} from "./lib/pi-durable-receive.ts";
|
|
74
|
+
|
|
75
|
+
const LABEL = "check-pi-durable-receive";
|
|
76
|
+
const QK = "PRX";
|
|
77
|
+
const REPO = path.resolve(import.meta.dirname, "..");
|
|
78
|
+
const OVERLAY = path.join(REPO, "pi", "pi-durable", "overlay");
|
|
79
|
+
const HOST_DRIVER = path.join(REPO, "scripts", "pi-durable-receive-host.mjs");
|
|
80
|
+
const REPORT_TIMEOUT_MS = 120_000;
|
|
81
|
+
/** Validity cutoff for request #1 after the ENQUEUE — a gap bound, never a success inference. */
|
|
82
|
+
const HOLD_WAIT_MS = 60_000;
|
|
83
|
+
const CLOSE_TIMEOUT_MS = 30_000;
|
|
84
|
+
const REAP_TIMEOUT_MS = 5_000;
|
|
85
|
+
const CALL_ID = "call_prx_1";
|
|
86
|
+
const FINAL_TEXT = "read.";
|
|
87
|
+
|
|
88
|
+
const runtimeInput = process.env.ENTWURF_PI_DURABLE_RUNTIME?.trim();
|
|
89
|
+
const bridgeInput = process.env.ENTWURF_PI_DURABLE_BRIDGE_ENTRY?.trim();
|
|
90
|
+
if (!runtimeInput || !bridgeInput) {
|
|
91
|
+
skipLive(
|
|
92
|
+
LABEL,
|
|
93
|
+
"set ENTWURF_PI_DURABLE_RUNTIME=<overlay checkout at the pin, patch applied, deps and model data installed> and " +
|
|
94
|
+
"ENTWURF_PI_DURABLE_BRIDGE_ENTRY=<bridge index.js emitted from this checkout into a private bundle>; " +
|
|
95
|
+
"this gate provisions neither",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const runtime = path.resolve(runtimeInput);
|
|
99
|
+
const bridgeEntry = path.resolve(bridgeInput);
|
|
100
|
+
const receiptsInput = process.env.ENTWURF_PI_DURABLE_RECEIPTS?.trim() || undefined;
|
|
101
|
+
if (receiptsInput !== undefined) {
|
|
102
|
+
if (!path.isAbsolute(receiptsInput)) {
|
|
103
|
+
console.error(`${LABEL}: ENTWURF_PI_DURABLE_RECEIPTS must be absolute (got ${JSON.stringify(receiptsInput)})`);
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(receiptsInput);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`${LABEL}: ENTWURF_PI_DURABLE_RECEIPTS must be a new directory: ${(error as Error).message}`);
|
|
110
|
+
process.exit(2);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let passed = 0;
|
|
115
|
+
let failed = 0;
|
|
116
|
+
function ok(label: string, cond: boolean): void {
|
|
117
|
+
if (cond) {
|
|
118
|
+
console.log(` ok ${label}`);
|
|
119
|
+
passed++;
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` FAIL ${label}`);
|
|
122
|
+
failed++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A prerequisite of the one cell: the SAME boolean is the QK and the guard, so a failed prerequisite
|
|
127
|
+
* stops here — before the send, the wait or the release — and spends no further request.
|
|
128
|
+
*/
|
|
129
|
+
function must(label: string, cond: boolean): void {
|
|
130
|
+
ok(label, cond);
|
|
131
|
+
if (!cond) throw new Error(`GAP: prerequisite failed — ${label}`);
|
|
132
|
+
}
|
|
133
|
+
function fatal(message: string): never {
|
|
134
|
+
console.error(`${LABEL}: ${message}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const sha256 = (data: string | Buffer): string => createHash("sha256").update(data).digest("hex");
|
|
138
|
+
function canonicalJson(value: unknown): string {
|
|
139
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
140
|
+
if (value !== null && typeof value === "object") {
|
|
141
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
142
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
143
|
+
}
|
|
144
|
+
return JSON.stringify(value);
|
|
145
|
+
}
|
|
146
|
+
function messageText(content: unknown): string {
|
|
147
|
+
if (typeof content === "string") return content;
|
|
148
|
+
if (Array.isArray(content)) {
|
|
149
|
+
return content
|
|
150
|
+
.flatMap((p) =>
|
|
151
|
+
p && typeof p === "object" && typeof (p as { text?: unknown }).text === "string"
|
|
152
|
+
? [(p as { text: string }).text]
|
|
153
|
+
: [],
|
|
154
|
+
)
|
|
155
|
+
.join("\n");
|
|
156
|
+
}
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// 0. Inputs — the pinned overlay, its model data, the bundle registry, and the bundle's owner join.
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// The pin and the runtime facts come from the ONE verifier the managed launch, setup and the fresh
|
|
164
|
+
// preflight use (pi-extensions/lib/pi-durable-runtime.ts).
|
|
165
|
+
const { pin } = readPiDurablePin(OVERLAY);
|
|
166
|
+
const facts = inspectPiDurableRuntime(runtime, OVERLAY);
|
|
167
|
+
const resolver = path.join(runtime, PI_DURABLE_RESOLVER_RELATIVE);
|
|
168
|
+
const bundleRoot = path.resolve(bridgeEntry, "..", "..", "..", "..");
|
|
169
|
+
const bundleRegistry = path.join(bundleRoot, "pi-extensions", "entwurf-capabilities.json");
|
|
170
|
+
const bundleDeliverability = path.join(bundleRoot, "pi-extensions", "lib", "entwurf-deliverability.js");
|
|
171
|
+
console.log("0. inputs");
|
|
172
|
+
ok(
|
|
173
|
+
`[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`,
|
|
174
|
+
facts.gitAnswered &&
|
|
175
|
+
facts.head === pin.commit &&
|
|
176
|
+
facts.indexMatchesHead &&
|
|
177
|
+
facts.diffEqualsPatch &&
|
|
178
|
+
facts.untracked === "" &&
|
|
179
|
+
facts.modelDataFiles === pin.modelData.files &&
|
|
180
|
+
facts.modelDataAggregate === pin.modelData.aggregateSha256 &&
|
|
181
|
+
facts.manifestSha256 === pin.modelData.manifestSha256 &&
|
|
182
|
+
facts.resolverPresent &&
|
|
183
|
+
fs.existsSync(bridgeEntry) &&
|
|
184
|
+
fs.existsSync(bundleRegistry) &&
|
|
185
|
+
fs.readFileSync(bundleRegistry).equals(fs.readFileSync(path.join(REPO, "pi", "entwurf-capabilities.json"))),
|
|
186
|
+
);
|
|
187
|
+
const joinLine = fs.existsSync(bundleDeliverability)
|
|
188
|
+
? (/SENDER_JOINED_RECEIVER_OWNER_KINDS\s*=\s*(\[[^\]]*\])/.exec(fs.readFileSync(bundleDeliverability, "utf8"))?.[1] ??
|
|
189
|
+
"")
|
|
190
|
+
: "";
|
|
191
|
+
ok(
|
|
192
|
+
`[QK:${QK}-BUNDLE-OWNER-JOIN] the bridge bundle's compiled owner join admits pi-durable-host (${joinLine || "not found"})`,
|
|
193
|
+
joinLine.includes('"pi-durable-host"'),
|
|
194
|
+
);
|
|
195
|
+
if (failed > 0) fatal("inputs are not the pinned overlay, its model data, this checkout's registry and owner join");
|
|
196
|
+
|
|
197
|
+
// Coarse pi-durable-specific operator guard (as G-contact): no operator-byte claim beyond it.
|
|
198
|
+
const operatorAgent = path.join(os.homedir(), ".pi", "agent");
|
|
199
|
+
function operatorPiDurableFacts(): string {
|
|
200
|
+
const backendOf = (file: string): unknown => {
|
|
201
|
+
try {
|
|
202
|
+
return (JSON.parse(fs.readFileSync(file, "utf8")) as { backend?: unknown }).backend;
|
|
203
|
+
} catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const list = (dir: string, suffix: string) =>
|
|
208
|
+
fs.existsSync(dir)
|
|
209
|
+
? fs
|
|
210
|
+
.readdirSync(dir)
|
|
211
|
+
.filter((n) => n.endsWith(suffix))
|
|
212
|
+
.filter((n) => backendOf(path.join(dir, n)) === "pi-durable")
|
|
213
|
+
: [];
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
durableRecords: list(path.join(operatorAgent, "meta-sessions"), ".meta.json"),
|
|
216
|
+
senders: fs.existsSync(path.join(operatorAgent, "meta-senders", "pi-durable")),
|
|
217
|
+
durableReceivers: list(path.join(operatorAgent, "meta-receivers"), ".json"),
|
|
218
|
+
durableSessions: fs.existsSync(path.join(operatorAgent, "experimental", "durable-sessions")),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const operatorBefore = operatorPiDurableFacts();
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Sandbox: one private world — HOME/XDG/agent, the four garden roots and ENTWURF_DIR.
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-durable-receive-"));
|
|
227
|
+
const dirs = {
|
|
228
|
+
home: path.join(root, "home"),
|
|
229
|
+
agent: path.join(root, "agent"),
|
|
230
|
+
tmp: path.join(root, "tmp"),
|
|
231
|
+
project: path.join(root, "project"),
|
|
232
|
+
senderProject: path.join(root, "sender-project"),
|
|
233
|
+
sessions: path.join(root, "garden", "meta-sessions"),
|
|
234
|
+
senders: path.join(root, "garden", "meta-senders"),
|
|
235
|
+
receivers: path.join(root, "garden", "meta-receivers"),
|
|
236
|
+
mailbox: path.join(root, "garden", "meta-mailbox"),
|
|
237
|
+
entwurf: path.join(root, "entwurf"),
|
|
238
|
+
xdgConfig: path.join(root, "xdg", "config"),
|
|
239
|
+
xdgCache: path.join(root, "xdg", "cache"),
|
|
240
|
+
xdgData: path.join(root, "xdg", "data"),
|
|
241
|
+
xdgState: path.join(root, "xdg", "state"),
|
|
242
|
+
};
|
|
243
|
+
for (const dir of Object.values(dirs)) fs.mkdirSync(dir, { recursive: true });
|
|
244
|
+
process.umask(0o022);
|
|
245
|
+
const env: Record<string, string> = {
|
|
246
|
+
PATH: process.env.PATH ?? "/run/current-system/sw/bin",
|
|
247
|
+
HOME: dirs.home,
|
|
248
|
+
TMPDIR: dirs.tmp,
|
|
249
|
+
XDG_CONFIG_HOME: dirs.xdgConfig,
|
|
250
|
+
XDG_CACHE_HOME: dirs.xdgCache,
|
|
251
|
+
XDG_DATA_HOME: dirs.xdgData,
|
|
252
|
+
XDG_STATE_HOME: dirs.xdgState,
|
|
253
|
+
PI_CODING_AGENT_DIR: dirs.agent,
|
|
254
|
+
ENTWURF_META_SESSIONS_DIR: dirs.sessions,
|
|
255
|
+
ENTWURF_META_SENDERS_DIR: dirs.senders,
|
|
256
|
+
ENTWURF_META_RECEIVERS_DIR: dirs.receivers,
|
|
257
|
+
ENTWURF_META_MAILBOX_DIR: dirs.mailbox,
|
|
258
|
+
ENTWURF_DIR: dirs.entwurf,
|
|
259
|
+
PI_OFFLINE: "1",
|
|
260
|
+
NOSYSBASHRC: "1",
|
|
261
|
+
TERM: "dumb",
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// S: the scripted endpoint, with the hold at request #1.
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
interface WireRequest {
|
|
268
|
+
method: string;
|
|
269
|
+
url: string;
|
|
270
|
+
body: Record<string, unknown> | null;
|
|
271
|
+
}
|
|
272
|
+
const wire: WireRequest[] = [];
|
|
273
|
+
let hostGid = "";
|
|
274
|
+
let request1Seen!: () => void;
|
|
275
|
+
const request1 = new Promise<void>((resolve) => {
|
|
276
|
+
request1Seen = resolve;
|
|
277
|
+
});
|
|
278
|
+
let releaseRequest1!: () => void;
|
|
279
|
+
const request1Released = new Promise<void>((resolve) => {
|
|
280
|
+
releaseRequest1 = resolve;
|
|
281
|
+
});
|
|
282
|
+
const sse = (res: http.ServerResponse, chunks: unknown[]) => {
|
|
283
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
|
|
284
|
+
for (const chunk of chunks) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
285
|
+
res.end("data: [DONE]\n\n");
|
|
286
|
+
};
|
|
287
|
+
const chunk = (delta: Record<string, unknown>, finish: string | null) => ({
|
|
288
|
+
id: "chatcmpl-prx",
|
|
289
|
+
object: "chat.completion.chunk",
|
|
290
|
+
created: 0,
|
|
291
|
+
model: "scripted",
|
|
292
|
+
choices: [{ index: 0, delta, finish_reason: finish }],
|
|
293
|
+
});
|
|
294
|
+
const usage = {
|
|
295
|
+
id: "chatcmpl-prx",
|
|
296
|
+
object: "chat.completion.chunk",
|
|
297
|
+
created: 0,
|
|
298
|
+
model: "scripted",
|
|
299
|
+
choices: [],
|
|
300
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
|
301
|
+
};
|
|
302
|
+
/** Request #2 must be the drained turn carrying a tool result for the scripted call. */
|
|
303
|
+
function carriesReadResult(body: Record<string, unknown> | null): boolean {
|
|
304
|
+
const messages = (body?.messages as { role?: string; tool_call_id?: string }[] | undefined) ?? [];
|
|
305
|
+
return messages.some((m) => m.role === "tool" && m.tool_call_id === CALL_ID);
|
|
306
|
+
}
|
|
307
|
+
const server = http.createServer((req, res) => {
|
|
308
|
+
let raw = "";
|
|
309
|
+
req.on("data", (c: Buffer) => {
|
|
310
|
+
raw += c.toString("utf8");
|
|
311
|
+
});
|
|
312
|
+
req.on("end", () => {
|
|
313
|
+
let body: Record<string, unknown> | null = null;
|
|
314
|
+
try {
|
|
315
|
+
body = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
|
|
316
|
+
} catch {
|
|
317
|
+
body = null;
|
|
318
|
+
}
|
|
319
|
+
wire.push({ method: req.method ?? "", url: req.url ?? "", body });
|
|
320
|
+
const n = wire.length;
|
|
321
|
+
if (req.method === "POST" && req.url === "/v1/chat/completions" && n === 1) {
|
|
322
|
+
request1Seen();
|
|
323
|
+
// Withheld until the gate has captured what this request must explain. A gate that throws
|
|
324
|
+
// instead never releases; the cleanup closes the socket.
|
|
325
|
+
void request1Released.then(() =>
|
|
326
|
+
sse(res, [
|
|
327
|
+
chunk(
|
|
328
|
+
{
|
|
329
|
+
role: "assistant",
|
|
330
|
+
tool_calls: [
|
|
331
|
+
{
|
|
332
|
+
index: 0,
|
|
333
|
+
id: CALL_ID,
|
|
334
|
+
type: "function",
|
|
335
|
+
function: { name: "entwurf_inbox_read", arguments: JSON.stringify({ gardenId: hostGid }) },
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
null,
|
|
340
|
+
),
|
|
341
|
+
chunk({}, "tool_calls"),
|
|
342
|
+
usage,
|
|
343
|
+
]),
|
|
344
|
+
);
|
|
345
|
+
} else if (req.method === "POST" && req.url === "/v1/chat/completions" && n === 2 && carriesReadResult(body)) {
|
|
346
|
+
sse(res, [chunk({ role: "assistant", content: FINAL_TEXT }, null), chunk({}, "stop"), usage]);
|
|
347
|
+
} else {
|
|
348
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
349
|
+
res.end(
|
|
350
|
+
JSON.stringify({ error: { message: `scripted endpoint: unexpected request #${n} ${req.method} ${req.url}` } }),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
356
|
+
const port = (server.address() as AddressInfo).port;
|
|
357
|
+
fs.writeFileSync(
|
|
358
|
+
path.join(dirs.agent, "models.json"),
|
|
359
|
+
`${JSON.stringify(
|
|
360
|
+
{
|
|
361
|
+
providers: {
|
|
362
|
+
loopback: {
|
|
363
|
+
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
364
|
+
api: "openai-completions",
|
|
365
|
+
apiKey: "prx-loopback-dummy",
|
|
366
|
+
models: [{ id: "scripted" }],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
null,
|
|
371
|
+
2,
|
|
372
|
+
)}\n`,
|
|
373
|
+
);
|
|
374
|
+
fs.writeFileSync(
|
|
375
|
+
path.join(dirs.agent, "settings.json"),
|
|
376
|
+
`${JSON.stringify({ defaultProvider: "loopback", defaultModel: "scripted" }, null, 2)}\n`,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// Processes this gate owns, and their bounded reaping (as G-send).
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
function procStat(pid: number): { ppid: number; start: string } | null {
|
|
383
|
+
try {
|
|
384
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
385
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
386
|
+
return { ppid: Number(fields[1]), start: fields[19] as string };
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function procArgv(pid: number): string[] {
|
|
392
|
+
try {
|
|
393
|
+
return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
|
|
394
|
+
} catch {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
399
|
+
interface Owned {
|
|
400
|
+
pid: number;
|
|
401
|
+
start: string;
|
|
402
|
+
what: string;
|
|
403
|
+
}
|
|
404
|
+
const owned: Owned[] = [];
|
|
405
|
+
function own(pid: number, what: string): Owned {
|
|
406
|
+
const existing = owned.find((p) => p.pid === pid && p.start === (procStat(pid)?.start ?? ""));
|
|
407
|
+
if (existing) return existing;
|
|
408
|
+
const entry = { pid, start: procStat(pid)?.start ?? "", what };
|
|
409
|
+
owned.push(entry);
|
|
410
|
+
return entry;
|
|
411
|
+
}
|
|
412
|
+
const childrenObserved = new Set<number>();
|
|
413
|
+
/** Own every bridge that /proc shows as a DIRECT child of a live owned host (PPID + entry argv). */
|
|
414
|
+
function observeBridges(host: Owned): void {
|
|
415
|
+
if (procStat(host.pid)?.start !== host.start) return;
|
|
416
|
+
for (const pid of fs
|
|
417
|
+
.readdirSync("/proc")
|
|
418
|
+
.filter((n) => /^\d+$/.test(n))
|
|
419
|
+
.map(Number)) {
|
|
420
|
+
if (procStat(pid)?.ppid === host.pid && procArgv(pid).includes(bridgeEntry)) own(pid, "host-bridge");
|
|
421
|
+
}
|
|
422
|
+
childrenObserved.add(host.pid);
|
|
423
|
+
}
|
|
424
|
+
function spawned(child: ReturnType<typeof spawn>, what: string): Promise<number> {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
child.once("spawn", () => (child.pid === undefined ? reject(new Error(`${what}: no pid`)) : resolve(child.pid)));
|
|
427
|
+
child.once("error", (error) => reject(new Error(`${what} did not start: ${error.message}`)));
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const observationNotes: string[] = [];
|
|
431
|
+
async function reapAll(): Promise<string[]> {
|
|
432
|
+
const problems: string[] = [];
|
|
433
|
+
for (const host of owned.filter((p) => p.what === "host")) {
|
|
434
|
+
if (procStat(host.pid)?.start === host.start) observeBridges(host);
|
|
435
|
+
else if (!childrenObserved.has(host.pid)) {
|
|
436
|
+
observationNotes.push(`host ${host.pid} exited before its children were observed`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const p of [...owned].reverse()) {
|
|
440
|
+
if (procStat(p.pid)?.start !== p.start) continue;
|
|
441
|
+
for (const signal of ["SIGTERM", "SIGKILL"] as const) {
|
|
442
|
+
try {
|
|
443
|
+
process.kill(p.pid, signal);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
problems.push(`${p.what} ${p.pid}: ${signal} failed (${(error as Error).message})`);
|
|
446
|
+
}
|
|
447
|
+
const deadline = Date.now() + REAP_TIMEOUT_MS;
|
|
448
|
+
while (Date.now() < deadline && procStat(p.pid)?.start === p.start) await sleep(50);
|
|
449
|
+
if (procStat(p.pid)?.start !== p.start) break;
|
|
450
|
+
}
|
|
451
|
+
if (procStat(p.pid)?.start === p.start) problems.push(`${p.what} ${p.pid} is still alive after SIGKILL`);
|
|
452
|
+
}
|
|
453
|
+
return problems;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// The cell
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
interface EntrySummary {
|
|
460
|
+
id: number;
|
|
461
|
+
kind: string;
|
|
462
|
+
role?: string;
|
|
463
|
+
toolCalls: { id: string; name: string; arguments: unknown }[];
|
|
464
|
+
toolCallId?: string;
|
|
465
|
+
toolName?: string;
|
|
466
|
+
isError?: boolean;
|
|
467
|
+
diagnostics?: { severity: string; code: string; message: string }[];
|
|
468
|
+
text: string;
|
|
469
|
+
}
|
|
470
|
+
interface Admission {
|
|
471
|
+
why: string;
|
|
472
|
+
requestId: string;
|
|
473
|
+
submissionId: unknown;
|
|
474
|
+
fresh: string[];
|
|
475
|
+
unread: number;
|
|
476
|
+
}
|
|
477
|
+
interface ReceiverReport {
|
|
478
|
+
status: string;
|
|
479
|
+
gardenId?: string;
|
|
480
|
+
markerPath?: string;
|
|
481
|
+
reason?: string;
|
|
482
|
+
detail?: string;
|
|
483
|
+
admissions?: Admission[];
|
|
484
|
+
}
|
|
485
|
+
interface Born {
|
|
486
|
+
born: true;
|
|
487
|
+
hostPid: number;
|
|
488
|
+
session: { id: string; directory: string; cwd: string };
|
|
489
|
+
conversationId: number;
|
|
490
|
+
attachment: { gardenId: string; bridgePid: number | null };
|
|
491
|
+
receiver: ReceiverReport;
|
|
492
|
+
}
|
|
493
|
+
interface Settled {
|
|
494
|
+
settled: string;
|
|
495
|
+
hostPid: number;
|
|
496
|
+
session: { id: string };
|
|
497
|
+
conversationId: number;
|
|
498
|
+
receiver: ReceiverReport;
|
|
499
|
+
entries: EntrySummary[];
|
|
500
|
+
busy: boolean;
|
|
501
|
+
notices: string[];
|
|
502
|
+
error?: string;
|
|
503
|
+
}
|
|
504
|
+
interface Held {
|
|
505
|
+
enqueued: string;
|
|
506
|
+
listing: string[];
|
|
507
|
+
deliveredBody: string;
|
|
508
|
+
signal: string;
|
|
509
|
+
receiverMarker: Record<string, unknown>;
|
|
510
|
+
receiverMarkerBytes: string;
|
|
511
|
+
request1UserText: string;
|
|
512
|
+
}
|
|
513
|
+
interface CellContext {
|
|
514
|
+
born: Born;
|
|
515
|
+
settled: Settled;
|
|
516
|
+
held: Held;
|
|
517
|
+
listed: Map<string, unknown>;
|
|
518
|
+
message: string;
|
|
519
|
+
senderGid: string;
|
|
520
|
+
senderRecordPath: string;
|
|
521
|
+
senderMarkerPath: string;
|
|
522
|
+
selfText: string;
|
|
523
|
+
receipt: string;
|
|
524
|
+
box: string;
|
|
525
|
+
}
|
|
526
|
+
let context: CellContext | undefined;
|
|
527
|
+
|
|
528
|
+
function lastUserText(body: Record<string, unknown> | null): string {
|
|
529
|
+
const messages = (body?.messages as { role?: string; content?: unknown }[] | undefined) ?? [];
|
|
530
|
+
return messageText(messages.filter((m) => m.role === "user").at(-1)?.content);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function cell(): Promise<void> {
|
|
534
|
+
// The sender: a separate idle child owns its record's sender marker.
|
|
535
|
+
const senderOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 600000)"], {
|
|
536
|
+
stdio: "ignore",
|
|
537
|
+
env,
|
|
538
|
+
cwd: dirs.senderProject,
|
|
539
|
+
});
|
|
540
|
+
own(await spawned(senderOwner, "the sender owner"), "sender-owner");
|
|
541
|
+
if (senderOwner.pid === undefined) throw new Error("the sender owner has no pid");
|
|
542
|
+
const sender = upsertMetaSession({
|
|
543
|
+
input: { backend: "claude-code", nativeSessionId: `prx-sender-${process.pid}`, cwd: dirs.senderProject },
|
|
544
|
+
dir: dirs.sessions,
|
|
545
|
+
});
|
|
546
|
+
const senderGid = sender.record.gardenId;
|
|
547
|
+
const senderMarkerPath = writeMetaSenderMarker({
|
|
548
|
+
backend: "claude-code",
|
|
549
|
+
gardenId: senderGid,
|
|
550
|
+
nativeSessionId: sender.record.nativeSessionId,
|
|
551
|
+
cwd: dirs.senderProject,
|
|
552
|
+
ownerPid: senderOwner.pid,
|
|
553
|
+
sendersDir: dirs.senders,
|
|
554
|
+
});
|
|
555
|
+
must(
|
|
556
|
+
`[QK:${QK}-SENDER-NOT-GATE] no sender marker is keyed to this gate's pid (${process.pid}); the seeded one is owned by the idle child ${senderOwner.pid}`,
|
|
557
|
+
!fs.existsSync(path.join(dirs.senders, "claude-code", `${process.pid}.json`)) &&
|
|
558
|
+
!fs.existsSync(path.join(dirs.senders, "pi-durable", `${process.pid}.json`)) &&
|
|
559
|
+
(JSON.parse(fs.readFileSync(senderMarkerPath, "utf8")) as { ownerPid?: unknown }).ownerPid === senderOwner.pid,
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
console.log("1. receive cell — doorbell admission → native turn → entwurf_inbox_read (S + H, V not run)");
|
|
563
|
+
const host = spawn(process.execPath, ["--import", resolver, HOST_DRIVER, bridgeEntry], {
|
|
564
|
+
cwd: dirs.project,
|
|
565
|
+
env,
|
|
566
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
567
|
+
});
|
|
568
|
+
let streamError = "";
|
|
569
|
+
host.stdin?.on("error", (e) => {
|
|
570
|
+
streamError += ` [host stdin error: ${e.message}]`;
|
|
571
|
+
});
|
|
572
|
+
const hostOwned = own(await spawned(host, "the host"), "host");
|
|
573
|
+
host.on("error", (e) => {
|
|
574
|
+
streamError += ` [host process error: ${e.message}]`;
|
|
575
|
+
});
|
|
576
|
+
let stderr = "";
|
|
577
|
+
host.stderr?.on("data", (c: Buffer) => {
|
|
578
|
+
stderr = (stderr + c.toString("utf8")).slice(-8192);
|
|
579
|
+
});
|
|
580
|
+
const lines: string[] = [];
|
|
581
|
+
const waiters: ((line: string | null) => void)[] = [];
|
|
582
|
+
createInterface({ input: host.stdout as NodeJS.ReadableStream })
|
|
583
|
+
.on("line", (line) => {
|
|
584
|
+
const w = waiters.shift();
|
|
585
|
+
if (w) w(line);
|
|
586
|
+
else lines.push(line);
|
|
587
|
+
})
|
|
588
|
+
.on("close", () => {
|
|
589
|
+
for (const w of waiters.splice(0)) w(null);
|
|
590
|
+
});
|
|
591
|
+
let exitCode: number | null | undefined;
|
|
592
|
+
const exited = new Promise<void>((resolve) =>
|
|
593
|
+
host.on("exit", (code) => {
|
|
594
|
+
exitCode = code;
|
|
595
|
+
resolve();
|
|
596
|
+
}),
|
|
597
|
+
);
|
|
598
|
+
const next = (ms: number) =>
|
|
599
|
+
new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
600
|
+
const settle = (line: string | null) => {
|
|
601
|
+
if (line === null)
|
|
602
|
+
return reject(new Error(`host ended without a line${streamError}; stderr: ${stderr.trim()}`));
|
|
603
|
+
try {
|
|
604
|
+
resolve(JSON.parse(line) as Record<string, unknown>);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
reject(new Error(`host sent a malformed line (${(error as Error).message}): ${line.slice(0, 200)}`));
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
const queued = lines.shift();
|
|
610
|
+
if (queued !== undefined) return settle(queued);
|
|
611
|
+
const timer = setTimeout(
|
|
612
|
+
() => reject(new Error(`gate timeout: no host line within ${ms}ms; stderr: ${stderr.trim()}`)),
|
|
613
|
+
ms,
|
|
614
|
+
);
|
|
615
|
+
waiters.push((line) => {
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
settle(line);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const bornLine = await next(REPORT_TIMEOUT_MS);
|
|
622
|
+
if (bornLine.error !== undefined) throw new Error(`host failed before the send: ${String(bornLine.error)}`);
|
|
623
|
+
if (bornLine.born !== true) throw new Error(`host sent no born line: ${JSON.stringify(bornLine).slice(0, 200)}`);
|
|
624
|
+
const born = bornLine as unknown as Born;
|
|
625
|
+
hostGid = born.attachment.gardenId;
|
|
626
|
+
observeBridges(hostOwned);
|
|
627
|
+
if (owned.filter((p) => p.what === "host-bridge").length !== 1) {
|
|
628
|
+
throw new Error("GAP: expected exactly one bridge child of the host");
|
|
629
|
+
}
|
|
630
|
+
const receiverFile = metaReceiverMarkerPath(hostGid, dirs.receivers);
|
|
631
|
+
const armed = fs.existsSync(receiverFile)
|
|
632
|
+
? (JSON.parse(fs.readFileSync(receiverFile, "utf8")) as Record<string, unknown>)
|
|
633
|
+
: {};
|
|
634
|
+
must(
|
|
635
|
+
`[QK:${QK}-RECEIVER-ARMED] before the send: the host's raw receiver marker names its garden and durable session, owned by the spawned host (pid + /proc start key) as pi-durable-host, and its own receiver() is armed with no admission yet`,
|
|
636
|
+
armed.gardenId === hostGid &&
|
|
637
|
+
armed.backend === "pi-durable" &&
|
|
638
|
+
armed.nativeSessionId === born.session.id &&
|
|
639
|
+
armed.ownerPid === hostOwned.pid &&
|
|
640
|
+
armed.ownerStartKey === `linux:${hostOwned.start}` &&
|
|
641
|
+
armed.ownerKind === "pi-durable-host" &&
|
|
642
|
+
born.receiver.status === "armed" &&
|
|
643
|
+
born.receiver.markerPath === receiverFile &&
|
|
644
|
+
(born.receiver.admissions ?? []).length === 0 &&
|
|
645
|
+
born.conversationId === 1,
|
|
646
|
+
);
|
|
647
|
+
must(`[QK:${QK}-NO-REQUEST-BEFORE-SEND] the idle host has made no model request (${wire.length})`, wire.length === 0);
|
|
648
|
+
|
|
649
|
+
// The sending bridge: a child of this gate, handed the seeded marker explicitly.
|
|
650
|
+
const transport = new StdioClientTransport({
|
|
651
|
+
command: process.execPath,
|
|
652
|
+
args: [bridgeEntry],
|
|
653
|
+
env: { ...env, ENTWURF_META_SENDER_MARKER: senderMarkerPath },
|
|
654
|
+
cwd: dirs.senderProject,
|
|
655
|
+
stderr: "pipe",
|
|
656
|
+
});
|
|
657
|
+
transport.stderr?.on("data", () => {});
|
|
658
|
+
const client = new Client({ name: LABEL, version: "1" });
|
|
659
|
+
try {
|
|
660
|
+
await client.connect(transport);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
// A failed connect may already have spawned the bridge, which is not owned yet: close the
|
|
663
|
+
// transport (it ends the child it spawned) before rethrowing, and keep the cause first.
|
|
664
|
+
try {
|
|
665
|
+
await transport.close();
|
|
666
|
+
} catch (closeError) {
|
|
667
|
+
throw new AggregateError(
|
|
668
|
+
[error, closeError],
|
|
669
|
+
`the sending bridge did not connect (${(error as Error).message}); closing it also failed`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
const sendingPid = transport.pid;
|
|
675
|
+
if (sendingPid === null) throw new Error("GAP: the sending bridge has no pid");
|
|
676
|
+
own(sendingPid, "sending-bridge");
|
|
677
|
+
const text = (r: Awaited<ReturnType<Client["callTool"]>>): string =>
|
|
678
|
+
(Array.isArray(r.content) ? r.content : [])
|
|
679
|
+
.flatMap((b) => (b.type === "text" && typeof b.text === "string" ? [b.text] : []))
|
|
680
|
+
.join("\n");
|
|
681
|
+
let selfText = "";
|
|
682
|
+
let receipt = "";
|
|
683
|
+
let listed = new Map<string, unknown>();
|
|
684
|
+
const message = `prx first cell ${randomBytes(6).toString("hex")}`;
|
|
685
|
+
try {
|
|
686
|
+
must(
|
|
687
|
+
`[QK:${QK}-SENDING-BRIDGE-OWNED] the sending bridge is this gate's direct child running the bundle (pid ${sendingPid})`,
|
|
688
|
+
procStat(sendingPid)?.ppid === process.pid && procArgv(sendingPid).includes(bridgeEntry),
|
|
689
|
+
);
|
|
690
|
+
listed = new Map((await client.listTools()).tools.map((t) => [t.name, t.inputSchema]));
|
|
691
|
+
const self = await client.callTool({ name: "entwurf_self", arguments: {} });
|
|
692
|
+
selfText = text(self);
|
|
693
|
+
const selfJson = (() => {
|
|
694
|
+
try {
|
|
695
|
+
return JSON.parse(selfText.trim().split("\n").at(-1) ?? "") as Record<string, unknown>;
|
|
696
|
+
} catch {
|
|
697
|
+
return {};
|
|
698
|
+
}
|
|
699
|
+
})();
|
|
700
|
+
must(
|
|
701
|
+
`[QK:${QK}-SENDER-RESOLVED] the sending bridge's own entwurf_self resolves the seeded claude-code citizen through the explicit marker (${String(selfJson.sessionId)}, ${String(selfJson.origin)}, isError ${String(self.isError === true)}); otherwise nothing is sent`,
|
|
702
|
+
self.isError !== true && selfJson.sessionId === senderGid && selfJson.origin === "meta-session",
|
|
703
|
+
);
|
|
704
|
+
const sent = await client.callTool({
|
|
705
|
+
name: "entwurf_v2",
|
|
706
|
+
arguments: { target: hostGid, intent: "fire-and-forget", message, wants_reply: false },
|
|
707
|
+
});
|
|
708
|
+
receipt = text(sent);
|
|
709
|
+
must(
|
|
710
|
+
`[QK:${QK}-TRANSPORT] the send is a meta-mailbox ENQUEUE into the host's garden (${JSON.stringify(receipt)}); otherwise nothing is awaited or released`,
|
|
711
|
+
sent.isError !== true && enqueuedFile(receipt) !== undefined,
|
|
712
|
+
);
|
|
713
|
+
} finally {
|
|
714
|
+
await client.close();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// THE HOLD: request #1 must arrive (cutoff, not success timing), then everything is captured
|
|
718
|
+
// before the tool call is released.
|
|
719
|
+
const arrived = await Promise.race([request1.then(() => true), sleep(HOLD_WAIT_MS).then(() => false)]);
|
|
720
|
+
if (!arrived) throw new Error(`GAP: no model request within ${HOLD_WAIT_MS}ms of the ENQUEUE; nothing released`);
|
|
721
|
+
const box = path.join(dirs.mailbox, hostGid);
|
|
722
|
+
const enqueued = enqueuedFile(receipt);
|
|
723
|
+
const listing = fs.existsSync(box) ? fs.readdirSync(box).sort() : [];
|
|
724
|
+
const deliveredPath = enqueued === undefined ? "" : path.join(box, `${enqueued}.delivered`);
|
|
725
|
+
const deliveredBody =
|
|
726
|
+
enqueued !== undefined && fs.existsSync(deliveredPath) ? fs.readFileSync(deliveredPath, "utf8") : null;
|
|
727
|
+
const signalPath = path.join(box, "inbox.signal");
|
|
728
|
+
const receiverMarkerBytes = fs.existsSync(receiverFile) ? fs.readFileSync(receiverFile, "utf8") : null;
|
|
729
|
+
const receiverMarker =
|
|
730
|
+
receiverMarkerBytes === null ? null : (JSON.parse(receiverMarkerBytes) as Record<string, unknown>);
|
|
731
|
+
const request1Body = wire[0]?.body ?? null;
|
|
732
|
+
const request1Tools = ((request1Body?.tools as { function?: { name?: string } }[] | undefined) ?? []).map(
|
|
733
|
+
(t) => t.function?.name ?? "",
|
|
734
|
+
);
|
|
735
|
+
const request1UserText = lastUserText(request1Body);
|
|
736
|
+
const hold = heldCaptureVerdict({
|
|
737
|
+
hostGid,
|
|
738
|
+
hostPid: hostOwned.pid,
|
|
739
|
+
hostStartKey: hostOwned.start,
|
|
740
|
+
enqueued,
|
|
741
|
+
listing,
|
|
742
|
+
deliveredBody,
|
|
743
|
+
sentMessage: message,
|
|
744
|
+
signalPresent: fs.existsSync(signalPath),
|
|
745
|
+
receiverMarker,
|
|
746
|
+
request1UserText,
|
|
747
|
+
request1Tools,
|
|
748
|
+
});
|
|
749
|
+
ok(
|
|
750
|
+
`[QK:${QK}-HELD-CAPTURE] at the withheld request #1: ${enqueued ?? "?"} is stamped .delivered with the sent bytes, the signal and the host-owned receiver marker are in place, and the request is the one-message notice naming the garden and entwurf_inbox_read without the body${hold.ok ? "" : ` — refused: ${hold.reasons.join("; ")}`}`,
|
|
751
|
+
hold.ok,
|
|
752
|
+
);
|
|
753
|
+
if (
|
|
754
|
+
!hold.ok ||
|
|
755
|
+
enqueued === undefined ||
|
|
756
|
+
deliveredBody === null ||
|
|
757
|
+
receiverMarker === null ||
|
|
758
|
+
receiverMarkerBytes === null
|
|
759
|
+
) {
|
|
760
|
+
throw new Error(`GAP: the hold capture failed (${hold.reasons.join("; ")}); request #1 is not released`);
|
|
761
|
+
}
|
|
762
|
+
const held: Held = {
|
|
763
|
+
enqueued,
|
|
764
|
+
listing,
|
|
765
|
+
deliveredBody,
|
|
766
|
+
signal: fs.readFileSync(signalPath, "utf8"),
|
|
767
|
+
receiverMarker,
|
|
768
|
+
receiverMarkerBytes,
|
|
769
|
+
request1UserText,
|
|
770
|
+
};
|
|
771
|
+
releaseRequest1();
|
|
772
|
+
|
|
773
|
+
const settledLine = await next(REPORT_TIMEOUT_MS);
|
|
774
|
+
if (settledLine.error !== undefined)
|
|
775
|
+
throw new Error(`host failed: ${String(settledLine.error)}; stderr: ${stderr.trim()}`);
|
|
776
|
+
const settled = settledLine as unknown as Settled;
|
|
777
|
+
const requestId = `entwurf-doorbell:${hostGid}:${enqueued}`;
|
|
778
|
+
const admissions = settled.receiver.admissions ?? [];
|
|
779
|
+
ok(
|
|
780
|
+
`[QK:${QK}-ROOT-ADMISSION-REPORTED] the receiver reports its latest admission — by signal, for ${enqueued}, with request id ${requestId} and a submission id — and it is still armed; exactly-once is witnessed by NATIVE-DOORBELL-ENTRY/ROW (${JSON.stringify(admissions)})`,
|
|
781
|
+
settled.settled === "answered" &&
|
|
782
|
+
settled.receiver.status === "armed" &&
|
|
783
|
+
admissions.length === 1 &&
|
|
784
|
+
admissions[0]?.why === "signal" &&
|
|
785
|
+
admissions[0]?.requestId === requestId &&
|
|
786
|
+
JSON.stringify(admissions[0]?.fresh) === JSON.stringify([enqueued]) &&
|
|
787
|
+
admissions[0]?.unread === 1 &&
|
|
788
|
+
Number.isSafeInteger(admissions[0]?.submissionId),
|
|
789
|
+
);
|
|
790
|
+
const users = settled.entries.filter((e) => e.kind === "pi.user");
|
|
791
|
+
const calls = settled.entries.filter((e) => e.kind === "pi.assistant").flatMap((e) => e.toolCalls);
|
|
792
|
+
const results = settled.entries.filter((e) => e.kind === "pi.tool-result");
|
|
793
|
+
const lastAssistant = settled.entries.filter((e) => e.kind === "pi.assistant").at(-1);
|
|
794
|
+
ok(
|
|
795
|
+
`[QK:${QK}-NATIVE-DOORBELL-ENTRY] the root conversation's one user entry is the notice the model received — no other input was given`,
|
|
796
|
+
users.length === 1 && users[0]?.text === request1UserText && settled.conversationId === 1,
|
|
797
|
+
);
|
|
798
|
+
ok(
|
|
799
|
+
`[QK:${QK}-NATIVE-DRAIN] the native view records one entwurf_inbox_read call for the host's garden and its non-error result carrying the delivered bytes and the read receipt`,
|
|
800
|
+
calls.length === 1 &&
|
|
801
|
+
calls[0]?.id === CALL_ID &&
|
|
802
|
+
calls[0]?.name === "entwurf_inbox_read" &&
|
|
803
|
+
canonicalJson(calls[0]?.arguments) === canonicalJson({ gardenId: hostGid }) &&
|
|
804
|
+
results.length === 1 &&
|
|
805
|
+
results[0]?.toolCallId === CALL_ID &&
|
|
806
|
+
results[0]?.isError === false &&
|
|
807
|
+
(results[0]?.text ?? "").includes(deliveredBody) &&
|
|
808
|
+
(results[0]?.text ?? "").includes("lastReadAt="),
|
|
809
|
+
);
|
|
810
|
+
ok(
|
|
811
|
+
`[QK:${QK}-NATIVE-TURN-SETTLED] the turn ends on the scripted text and the conversation is idle (last: ${JSON.stringify(lastAssistant?.text)}, busy ${settled.busy})`,
|
|
812
|
+
lastAssistant?.text === FINAL_TEXT && lastAssistant.toolCalls.length === 0 && settled.busy === false,
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
// --- raw archive, read here (a separate side effect from the native result and the row)
|
|
816
|
+
const after = fs.existsSync(box) ? fs.readdirSync(box).sort() : [];
|
|
817
|
+
const readPath = path.join(box, `${enqueued}.delivered.read`);
|
|
818
|
+
const state = fs.existsSync(path.join(box, "state.json"))
|
|
819
|
+
? (JSON.parse(fs.readFileSync(path.join(box, "state.json"), "utf8")) as { lastReadAt?: unknown })
|
|
820
|
+
: {};
|
|
821
|
+
ok(
|
|
822
|
+
`[QK:${QK}-ARCHIVE] the drained body is archived as ${enqueued}.delivered.read with the held bytes, nothing unread is left, and the mailbox state carries a read receipt (${after.join(", ")})`,
|
|
823
|
+
fs.existsSync(readPath) &&
|
|
824
|
+
fs.readFileSync(readPath, "utf8") === deliveredBody &&
|
|
825
|
+
!after.some((n) => n.endsWith(".msg") || n.endsWith(".msg.delivered")) &&
|
|
826
|
+
typeof state.lastReadAt === "string",
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
context = {
|
|
830
|
+
born,
|
|
831
|
+
settled,
|
|
832
|
+
held,
|
|
833
|
+
listed,
|
|
834
|
+
message,
|
|
835
|
+
senderGid,
|
|
836
|
+
senderRecordPath: sender.path,
|
|
837
|
+
senderMarkerPath,
|
|
838
|
+
selfText,
|
|
839
|
+
receipt,
|
|
840
|
+
box,
|
|
841
|
+
};
|
|
842
|
+
host.stdin?.write("close\n");
|
|
843
|
+
const closed = await next(CLOSE_TIMEOUT_MS);
|
|
844
|
+
await Promise.race([exited, sleep(CLOSE_TIMEOUT_MS)]);
|
|
845
|
+
ok(
|
|
846
|
+
`[QK:${QK}-CLOSE] the host closes and exits 0, and its close removed the receiver marker it owned (closed ${String(closed.closed)}, exit ${exitCode})`,
|
|
847
|
+
closed.closed === true && exitCode === 0 && !fs.existsSync(receiverFile),
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** The wire assertions, judged on the closed run's final snapshot. */
|
|
852
|
+
function finalWire(ctx: CellContext): void {
|
|
853
|
+
console.log("3. wire, final snapshot (host exited, endpoint closed)");
|
|
854
|
+
const first = wire[0]?.body ?? {};
|
|
855
|
+
const offered = new Map(
|
|
856
|
+
((first.tools as { function?: { name?: string; parameters?: unknown } }[] | undefined) ?? []).map((t) => [
|
|
857
|
+
t.function?.name ?? "",
|
|
858
|
+
t.function?.parameters,
|
|
859
|
+
]),
|
|
860
|
+
);
|
|
861
|
+
ok(
|
|
862
|
+
`[QK:${QK}-WIRE-INBOX-READ-SCHEMA-IS-BRIDGE] the entwurf_inbox_read parameters on the wire are the bridge's own tools/list inputSchema, as this gate listed it`,
|
|
863
|
+
ctx.listed.has("entwurf_inbox_read") &&
|
|
864
|
+
canonicalJson(offered.get("entwurf_inbox_read")) === canonicalJson(ctx.listed.get("entwurf_inbox_read")),
|
|
865
|
+
);
|
|
866
|
+
ok(
|
|
867
|
+
`[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(" | ")})`,
|
|
868
|
+
!server.listening &&
|
|
869
|
+
wire.length === 2 &&
|
|
870
|
+
wire.every((w) => w.method === "POST" && w.url === "/v1/chat/completions"),
|
|
871
|
+
);
|
|
872
|
+
const second =
|
|
873
|
+
(wire[1]?.body?.messages as { role?: string; tool_call_id?: string; content?: unknown }[] | undefined) ?? [];
|
|
874
|
+
const toolMessages = second.filter((m) => m.role === "tool");
|
|
875
|
+
const wireResult = messageText(toolMessages[0]?.content);
|
|
876
|
+
const viewText = ctx.settled.entries.find((e) => e.kind === "pi.tool-result")?.text ?? "";
|
|
877
|
+
ok(
|
|
878
|
+
`[QK:${QK}-WIRE-DRAIN] the second request carries ${CALL_ID}'s tool result: the delivered bytes, exactly as the native view recorded them`,
|
|
879
|
+
toolMessages.length === 1 &&
|
|
880
|
+
toolMessages[0]?.tool_call_id === CALL_ID &&
|
|
881
|
+
wireResult.includes(ctx.held.deliveredBody) &&
|
|
882
|
+
wireResult === viewText,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
interface SqliteInspection {
|
|
887
|
+
forensic: Record<string, string | null>;
|
|
888
|
+
forensicDir: string;
|
|
889
|
+
row: Record<string, unknown> | null;
|
|
890
|
+
record: Record<string, unknown> | null;
|
|
891
|
+
rowsForRequest: number;
|
|
892
|
+
rowsForColumn: number;
|
|
893
|
+
entry: Record<string, unknown> | null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** After a green reap only: forensic copy of the quiescent set, then a read-only look at a separate work copy. */
|
|
897
|
+
async function inspectSubmission(ctx: CellContext): Promise<SqliteInspection> {
|
|
898
|
+
const forensicDir = path.join(root, "forensic");
|
|
899
|
+
const workDir = path.join(root, "forensic-work");
|
|
900
|
+
fs.mkdirSync(forensicDir);
|
|
901
|
+
fs.mkdirSync(workDir);
|
|
902
|
+
const forensic: Record<string, string | null> = {};
|
|
903
|
+
for (const name of ["session.sqlite", "session.sqlite-wal", "session.sqlite-shm"]) {
|
|
904
|
+
const from = path.join(ctx.born.session.directory, name);
|
|
905
|
+
if (fs.existsSync(from)) {
|
|
906
|
+
fs.copyFileSync(from, path.join(forensicDir, name));
|
|
907
|
+
fs.copyFileSync(from, path.join(workDir, name));
|
|
908
|
+
forensic[name] = sha256(fs.readFileSync(path.join(forensicDir, name)));
|
|
909
|
+
} else forensic[name] = null;
|
|
910
|
+
}
|
|
911
|
+
const requestId = ctx.settled.receiver.admissions?.[0]?.requestId ?? "";
|
|
912
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
913
|
+
try {
|
|
914
|
+
const db = new DatabaseSync(path.join(workDir, "session.sqlite"), { readOnly: true });
|
|
915
|
+
try {
|
|
916
|
+
// Every row, selected by its parsed RECORD; the indexed column is counted separately under
|
|
917
|
+
// upstream's own encoding, so neither reading is trusted alone.
|
|
918
|
+
const all = db.prepare("SELECT id, conversation_id, request_id, status, record FROM submissions").all() as {
|
|
919
|
+
id: number;
|
|
920
|
+
conversation_id: number;
|
|
921
|
+
request_id: string | null;
|
|
922
|
+
status: string;
|
|
923
|
+
record: string;
|
|
924
|
+
}[];
|
|
925
|
+
const rows = all.filter((r) => (JSON.parse(r.record) as { requestId?: unknown }).requestId === requestId);
|
|
926
|
+
const rowsForColumn = all.filter((r) => r.request_id === encodedRequestIdColumn(requestId)).length;
|
|
927
|
+
const row = rows[0] ?? null;
|
|
928
|
+
const record = row === null ? null : (JSON.parse(row.record) as Record<string, unknown>);
|
|
929
|
+
const entryRow =
|
|
930
|
+
record !== null && typeof record.entry === "number"
|
|
931
|
+
? (db.prepare("SELECT id, record FROM entries WHERE id = ?").get(record.entry) as
|
|
932
|
+
| { id: number; record: string }
|
|
933
|
+
| undefined)
|
|
934
|
+
: undefined;
|
|
935
|
+
return {
|
|
936
|
+
forensic,
|
|
937
|
+
forensicDir,
|
|
938
|
+
row:
|
|
939
|
+
row === null
|
|
940
|
+
? null
|
|
941
|
+
: { id: row.id, conversation_id: row.conversation_id, request_id: row.request_id, status: row.status },
|
|
942
|
+
record,
|
|
943
|
+
rowsForRequest: rows.length,
|
|
944
|
+
rowsForColumn,
|
|
945
|
+
entry: entryRow === undefined ? null : (JSON.parse(entryRow.record) as Record<string, unknown>),
|
|
946
|
+
};
|
|
947
|
+
} finally {
|
|
948
|
+
db.close();
|
|
949
|
+
}
|
|
950
|
+
} catch (error) {
|
|
951
|
+
throw new Error(`GAP: the copied durable database could not be inspected read-only: ${(error as Error).message}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** Copy the cell's raw bytes into the receipts directory, with a correlation summary. */
|
|
956
|
+
function exportReceipts(dir: string, ctx: CellContext | undefined, sqlite: SqliteInspection | undefined): void {
|
|
957
|
+
const files: Record<string, string> = {};
|
|
958
|
+
const write = (name: string, bytes: string | Buffer) => {
|
|
959
|
+
const file = path.join(dir, name);
|
|
960
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
961
|
+
fs.writeFileSync(file, bytes);
|
|
962
|
+
files[name] = sha256(bytes);
|
|
963
|
+
};
|
|
964
|
+
const copy = (name: string, from: string) => write(name, fs.readFileSync(from));
|
|
965
|
+
write("wire.json", `${JSON.stringify(wire, null, 1)}\n`);
|
|
966
|
+
if (ctx === undefined) {
|
|
967
|
+
write("correlation.json", `${JSON.stringify({ completed: false, files }, null, 1)}\n`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
write("bridge-tools.json", `${JSON.stringify(Object.fromEntries(ctx.listed), null, 1)}\n`);
|
|
971
|
+
write(
|
|
972
|
+
"native-view.json",
|
|
973
|
+
`${JSON.stringify(
|
|
974
|
+
{
|
|
975
|
+
label:
|
|
976
|
+
"PROJECTION of the durable app's native view and the contact's receiver() by scripts/pi-durable-receive-host.mjs — not native internal state",
|
|
977
|
+
born: ctx.born,
|
|
978
|
+
settled: ctx.settled,
|
|
979
|
+
},
|
|
980
|
+
null,
|
|
981
|
+
1,
|
|
982
|
+
)}\n`,
|
|
983
|
+
);
|
|
984
|
+
write(
|
|
985
|
+
"held-capture.json",
|
|
986
|
+
`${JSON.stringify({ label: "captured while response #1 was withheld", ...ctx.held }, null, 1)}\n`,
|
|
987
|
+
);
|
|
988
|
+
write(`garden/host-receiver-marker-at-hold/${hostGid}.json`, ctx.held.receiverMarkerBytes);
|
|
989
|
+
for (const name of fs.existsSync(ctx.box) ? fs.readdirSync(ctx.box) : [])
|
|
990
|
+
copy(`mailbox/${name}`, path.join(ctx.box, name));
|
|
991
|
+
copy(`garden/sender-record/${path.basename(ctx.senderRecordPath)}`, ctx.senderRecordPath);
|
|
992
|
+
copy(`garden/sender-marker/${path.basename(ctx.senderMarkerPath)}`, ctx.senderMarkerPath);
|
|
993
|
+
const hostRecord = fs
|
|
994
|
+
.readdirSync(dirs.sessions)
|
|
995
|
+
.filter((n) => n.endsWith(".meta.json"))
|
|
996
|
+
.find(
|
|
997
|
+
(n) =>
|
|
998
|
+
(JSON.parse(fs.readFileSync(path.join(dirs.sessions, n), "utf8")) as { backend?: string }).backend ===
|
|
999
|
+
"pi-durable",
|
|
1000
|
+
);
|
|
1001
|
+
if (hostRecord === undefined) throw new Error("no pi-durable record to export");
|
|
1002
|
+
copy(`garden/host-record/${hostRecord}`, path.join(dirs.sessions, hostRecord));
|
|
1003
|
+
copy(
|
|
1004
|
+
`garden/host-sender-marker/${ctx.born.hostPid}.json`,
|
|
1005
|
+
path.join(dirs.senders, "pi-durable", `${ctx.born.hostPid}.json`),
|
|
1006
|
+
);
|
|
1007
|
+
if (sqlite !== undefined) {
|
|
1008
|
+
for (const [name, digest] of Object.entries(sqlite.forensic)) {
|
|
1009
|
+
if (digest !== null) copy(`forensic-sqlite/${name}`, path.join(sqlite.forensicDir, name));
|
|
1010
|
+
}
|
|
1011
|
+
const { forensicDir: _dir, ...inspection } = sqlite;
|
|
1012
|
+
write("sqlite-inspection.json", `${JSON.stringify(inspection, null, 1)}\n`);
|
|
1013
|
+
}
|
|
1014
|
+
write(
|
|
1015
|
+
"correlation.json",
|
|
1016
|
+
`${JSON.stringify(
|
|
1017
|
+
{
|
|
1018
|
+
completed: true,
|
|
1019
|
+
hostGardenId: hostGid,
|
|
1020
|
+
senderGardenId: ctx.senderGid,
|
|
1021
|
+
senderSelf: ctx.selfText,
|
|
1022
|
+
receipt: ctx.receipt,
|
|
1023
|
+
enqueued: ctx.held.enqueued,
|
|
1024
|
+
admission: ctx.settled.receiver.admissions?.[0] ?? null,
|
|
1025
|
+
wireRequests: wire.length,
|
|
1026
|
+
files,
|
|
1027
|
+
},
|
|
1028
|
+
null,
|
|
1029
|
+
1,
|
|
1030
|
+
)}\n`,
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
let primary: unknown;
|
|
1035
|
+
try {
|
|
1036
|
+
await cell();
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
primary = error;
|
|
1039
|
+
}
|
|
1040
|
+
const cleanupProblems = await reapAll();
|
|
1041
|
+
server.closeAllConnections();
|
|
1042
|
+
const endpointClosed = await Promise.race([
|
|
1043
|
+
new Promise<boolean>((resolve) => server.close(() => resolve(true))),
|
|
1044
|
+
sleep(REAP_TIMEOUT_MS).then(() => false),
|
|
1045
|
+
]);
|
|
1046
|
+
console.log("2. cleanup, database and operator guard");
|
|
1047
|
+
const alive = owned.filter((p) => procStat(p.pid)?.start === p.start);
|
|
1048
|
+
const reap = ownedReapVerdict({
|
|
1049
|
+
alive: alive.map((p) => p.pid),
|
|
1050
|
+
problems: cleanupProblems,
|
|
1051
|
+
endpointClosed,
|
|
1052
|
+
endpointListening: server.listening,
|
|
1053
|
+
observationNotes,
|
|
1054
|
+
});
|
|
1055
|
+
ok(
|
|
1056
|
+
`[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("; ")})`,
|
|
1057
|
+
reap.green,
|
|
1058
|
+
);
|
|
1059
|
+
let sqlite: SqliteInspection | undefined;
|
|
1060
|
+
if (context !== undefined) {
|
|
1061
|
+
if (!reap.green) {
|
|
1062
|
+
ok(`[QK:${QK}-ROW] GAP: owned processes were not all gone; the live database is not copied`, false);
|
|
1063
|
+
} else {
|
|
1064
|
+
try {
|
|
1065
|
+
sqlite = await inspectSubmission(context);
|
|
1066
|
+
const admission = context.settled.receiver.admissions?.[0];
|
|
1067
|
+
const row = submissionVerdict(
|
|
1068
|
+
{
|
|
1069
|
+
row: sqlite.row,
|
|
1070
|
+
record: sqlite.record,
|
|
1071
|
+
rowsForRequest: sqlite.rowsForRequest,
|
|
1072
|
+
rowsForColumn: sqlite.rowsForColumn,
|
|
1073
|
+
entry: sqlite.entry as { kind?: unknown; model?: { role?: unknown; content?: unknown }[] } | null,
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
submissionId: admission?.submissionId,
|
|
1077
|
+
requestId: admission?.requestId ?? "",
|
|
1078
|
+
notice: context.held.request1UserText,
|
|
1079
|
+
},
|
|
1080
|
+
);
|
|
1081
|
+
ok(
|
|
1082
|
+
`[QK:${QK}-ROW] the copied database holds one input submission for the doorbell's request id on the root conversation, with the reported id, done, its placed entry the notice (files ${JSON.stringify(sqlite.forensic)})${row.ok ? "" : ` — refused: ${row.reasons.join("; ")}`}`,
|
|
1083
|
+
row.ok,
|
|
1084
|
+
);
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
ok(`[QK:${QK}-ROW] ${(error as Error).message}`, false);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
finalWire(context);
|
|
1090
|
+
}
|
|
1091
|
+
if (receiptsInput !== undefined) {
|
|
1092
|
+
try {
|
|
1093
|
+
exportReceipts(receiptsInput, context, sqlite);
|
|
1094
|
+
ok(`[QK:${QK}-RECEIPTS-EXPORTED] the cell's raw bytes and correlation are in ${receiptsInput}`, true);
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
ok(`[QK:${QK}-RECEIPTS-EXPORTED] receipt export failed: ${(error as Error).message}`, false);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const before = JSON.parse(operatorBefore) as { durableRecords: string[]; senders: boolean; durableReceivers: string[] };
|
|
1100
|
+
ok(
|
|
1101
|
+
`[QK:${QK}-OPERATOR-UNTOUCHED] coarse pi-durable guard: the operator's own roots are unchanged by this run and hold no pi-durable record, sender dir or receiver marker (${operatorBefore})`,
|
|
1102
|
+
operatorPiDurableFacts() === operatorBefore &&
|
|
1103
|
+
before.durableRecords.length === 0 &&
|
|
1104
|
+
!before.senders &&
|
|
1105
|
+
before.durableReceivers.length === 0,
|
|
1106
|
+
);
|
|
1107
|
+
if (alive.length > 0) console.log(`sandbox kept because owned processes are still alive: ${root}`);
|
|
1108
|
+
else if (process.env.ENTWURF_PI_DURABLE_KEEP === "1") console.log(`sandbox kept: ${root}`);
|
|
1109
|
+
else fs.rmSync(root, { recursive: true, force: true });
|
|
1110
|
+
if (primary !== undefined)
|
|
1111
|
+
console.error(`${LABEL}: FAILED — ${primary instanceof Error ? primary.message : String(primary)}`);
|
|
1112
|
+
for (const problem of cleanupProblems) console.error(`${LABEL}: cleanup — ${problem}`);
|
|
1113
|
+
console.log(
|
|
1114
|
+
`\n${LABEL}: ${passed} passed, ${failed} failed — S (scripted endpoint) + H (doorbell admission); V not run; checkout-only`,
|
|
1115
|
+
);
|
|
1116
|
+
process.exit(primary === undefined && failed === 0 && reap.green ? 0 : 1);
|