@junghanacs/entwurf 0.31.0 → 0.32.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 +3 -2
- package/CHANGELOG.md +82 -0
- package/README.md +46 -36
- package/VERIFY.md +35 -16
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +112 -95
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +8 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +153 -205
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +6 -4
- package/mcp/entwurf-bridge/src/index.ts +1 -1
- package/package.json +14 -17
- package/pi/pi-durable/bootstrap.mjs +142 -19
- package/pi/pi-durable/carrier/LICENSE +21 -0
- package/pi/pi-durable/carrier/harness-setup.js +123 -0
- package/pi/pi-durable/carrier/prompt.js +74 -0
- package/pi/pi-durable/carrier/runtime.js +402 -0
- package/pi/pi-durable/carrier/sessions.js +56 -0
- package/pi/pi-durable/carrier/subagent.js +55 -0
- package/pi/pi-durable/carrier/tui.js +655 -0
- package/pi/pi-durable/carrier-relocation.mjs +160 -0
- package/pi/pi-durable/carrier-resolver.mjs +38 -0
- package/pi/pi-durable/overlay/upstream-pin.json +103 -11
- package/pi-extensions/lib/fresh-call-composition.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +8 -10
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +3 -3
- package/pi-extensions/lib/pi-durable-runtime.ts +197 -263
- package/pi-extensions/meta-bridge-pi-durable.ts +11 -7
- package/run.sh +508 -235
- package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
- package/scripts/check-fresh-call-dispatch.ts +1 -1
- package/scripts/check-gate-qualification.ts +86 -34
- package/scripts/check-herdr-activation.ts +5 -4
- package/scripts/check-pi-durable-contact.ts +26 -93
- package/scripts/check-pi-durable-receive.ts +22 -59
- package/scripts/check-pi-durable-send.ts +235 -75
- package/scripts/emit-pi-durable-carrier.ts +355 -0
- package/scripts/lib/mutation-qualify.ts +21 -45
- package/scripts/lib/pi-durable-operator-guard.ts +89 -0
- package/scripts/lib/qualification-receipt.ts +7 -11
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/gate-qualification.json +13 -0
- package/scripts/mutants/mux-fresh-call.json +13 -13
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +442 -109
- package/scripts/mutants/pi-floor.json +24 -2
- package/scripts/pi-durable-contact-host.mjs +3 -3
- package/scripts/pi-durable-receive-host.mjs +3 -3
- package/scripts/pi-durable-send-host.mjs +76 -5
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +3 -3
- package/scripts/smoke-setup-verdict.sh +15 -15
- package/scripts/smoke-user-scope-citizen.sh +1 -1
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* reports. None imports the adapter's expected constants.
|
|
24
24
|
*
|
|
25
25
|
* Inputs as check-pi-durable-contact (missing → SKIP 97, never a pass):
|
|
26
|
-
*
|
|
26
|
+
* ENTWURF_PI_DURABLE_BRIDGE_ENTRY (the durable app is this checkout's verified carrier, #130)
|
|
27
27
|
* Optional:
|
|
28
28
|
* ENTWURF_PI_DURABLE_RECEIPTS a NEW absolute directory (refused if it exists) that receives the
|
|
29
29
|
* cell's raw bytes: wire.json, bridge-tools.json, native-view.json (a
|
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
* REQUIRED `intent`, and the app's own validation must settle it as
|
|
39
39
|
* `invalid_arguments`; `reject-undeliverable` — valid arguments, a
|
|
40
40
|
* target with no receiver marker, and the bridge must refuse by name
|
|
41
|
-
* (`mailbox-undeliverable`), surfacing natively as `tool_error
|
|
41
|
+
* (`mailbox-undeliverable`), surfacing natively as `tool_error`;
|
|
42
|
+
* `native-module` (#130 P2) — no send: the packaged `main` runs with
|
|
43
|
+
* the synthetic operator module test/fixtures/pi-durable-native-module/
|
|
44
|
+
* mock.mjs, and ONE scripted answer calls its tool, the native `bash`
|
|
45
|
+
* and the contact's `entwurf_self`.
|
|
42
46
|
* Unknown selector exits 2 before anything spawns.
|
|
43
47
|
*
|
|
44
48
|
* Both cells read the bridge child's kernel counters (/proc/<pid>/io `rchar`, `syscr`) at sampled
|
|
@@ -61,32 +65,39 @@ import * as path from "node:path";
|
|
|
61
65
|
import { createInterface } from "node:readline";
|
|
62
66
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
63
67
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
68
|
+
import { CARRIER_SCHEME } from "../pi/pi-durable/carrier-relocation.mjs";
|
|
64
69
|
import {
|
|
65
70
|
metaReceiverMarkerPath,
|
|
66
71
|
upsertMetaSession,
|
|
67
72
|
writeMetaReceiverMarker,
|
|
68
73
|
writeMetaSenderMarker,
|
|
69
74
|
} 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 { checkPiDurableRuntime, piDurableLayoutAt } from "../pi-extensions/lib/pi-durable-runtime.ts";
|
|
75
76
|
import { skipLive } from "./lib/live-skip.ts";
|
|
77
|
+
import {
|
|
78
|
+
describeOperatorSnapshot,
|
|
79
|
+
operatorPiDurableSnapshot,
|
|
80
|
+
operatorSnapshotUnchanged,
|
|
81
|
+
} from "./lib/pi-durable-operator-guard.ts";
|
|
76
82
|
import { checkpointVerdict, intentFrameVerdict, preCopyVerdict } from "./lib/pi-durable-r1.ts";
|
|
77
83
|
import { ownedReapVerdict } from "./lib/pi-durable-reap.ts";
|
|
78
84
|
|
|
79
85
|
const LABEL = "check-pi-durable-send";
|
|
80
86
|
const REPO = path.resolve(import.meta.dirname, "..");
|
|
81
|
-
const OVERLAY = path.join(REPO, "pi", "pi-durable", "overlay");
|
|
82
87
|
const HOST_DRIVER = path.join(REPO, "scripts", "pi-durable-send-host.mjs");
|
|
83
88
|
const REPORT_TIMEOUT_MS = 120_000;
|
|
84
89
|
const CLOSE_TIMEOUT_MS = 30_000;
|
|
85
90
|
const REAP_TIMEOUT_MS = 5_000;
|
|
86
91
|
const CELL = process.env.ENTWURF_PI_DURABLE_SEND_CELL?.trim() || "valid";
|
|
87
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
CELL !== "valid" &&
|
|
94
|
+
CELL !== "invalid-args" &&
|
|
95
|
+
CELL !== "reject-undeliverable" &&
|
|
96
|
+
CELL !== "r1-unsafe-recovery" &&
|
|
97
|
+
CELL !== "native-module"
|
|
98
|
+
) {
|
|
88
99
|
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")`,
|
|
100
|
+
`check-pi-durable-send: unknown ENTWURF_PI_DURABLE_SEND_CELL ${JSON.stringify(CELL)} (only "valid", "invalid-args", "reject-undeliverable", "r1-unsafe-recovery", "native-module")`,
|
|
90
101
|
);
|
|
91
102
|
process.exit(2);
|
|
92
103
|
}
|
|
@@ -106,9 +117,69 @@ const REJECT = CELL === "reject-undeliverable";
|
|
|
106
117
|
* input, and the call must settle as interrupted without the tool implementation running again.
|
|
107
118
|
*/
|
|
108
119
|
const R1 = CELL === "r1-unsafe-recovery";
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
/**
|
|
121
|
+
* native-module (#130 P2): the host runs the whole packaged `main` with ONE operator module — the
|
|
122
|
+
* synthetic fixture, node:* only — and the scripted first answer calls the module's tool, the native
|
|
123
|
+
* `bash` and the contact's `entwurf_self` in one assistant message. An observing resolve hook,
|
|
124
|
+
* preloaded after the carrier resolver and before the host driver, appends the first resolution of
|
|
125
|
+
* every `@earendil-works/*` and carrier-scheme (CARRIER_SCHEME) specifier to the same file the module's
|
|
126
|
+
* initialization appends `init` to: one file, so its line order IS the event order. The bridge's
|
|
127
|
+
* environment is not judged here — the beside QK PI-DURABLE-MODULE-BRIDGE-ENV-SPEC owns that spec.
|
|
128
|
+
*/
|
|
129
|
+
const NATIVE = CELL === "native-module";
|
|
130
|
+
const QK = INVALID ? "PDI" : REJECT ? "PDR" : R1 ? "PR1" : NATIVE ? "PDM" : "PDS";
|
|
131
|
+
const CALL_ID = INVALID
|
|
132
|
+
? "call_pdi_1"
|
|
133
|
+
: REJECT
|
|
134
|
+
? "call_pdr_1"
|
|
135
|
+
: R1
|
|
136
|
+
? "call_pr1_1"
|
|
137
|
+
: NATIVE
|
|
138
|
+
? "call_pdm_1"
|
|
139
|
+
: "call_pds_1";
|
|
140
|
+
const FINAL_TEXT = INVALID
|
|
141
|
+
? "invalid noted."
|
|
142
|
+
: REJECT
|
|
143
|
+
? "reject noted."
|
|
144
|
+
: R1
|
|
145
|
+
? "recovered."
|
|
146
|
+
: NATIVE
|
|
147
|
+
? "module noted."
|
|
148
|
+
: "sent.";
|
|
149
|
+
const MOCK_MODULE = path.join(REPO, "test", "fixtures", "pi-durable-native-module", "mock.mjs");
|
|
150
|
+
/** What the fixture's initialization sets — typed here, not imported from it. */
|
|
151
|
+
const MOCK_MARK = "mock-native-mark-0f5c";
|
|
152
|
+
const NATIVE_CALLS = [
|
|
153
|
+
{ id: CALL_ID, name: "mock_native_echo", arguments: { text: "pdm" } },
|
|
154
|
+
{ id: "call_pdm_2", name: "bash", arguments: { command: 'printf %s "$MOCK_NATIVE_MARK"' } },
|
|
155
|
+
{ id: "call_pdm_3", name: "entwurf_self", arguments: {} },
|
|
156
|
+
];
|
|
157
|
+
/** The contact's six tool names, typed here (this gate imports no adapter constant). */
|
|
158
|
+
const CONTACT_TOOLS = [
|
|
159
|
+
"entwurf_self",
|
|
160
|
+
"entwurf_peers",
|
|
161
|
+
"entwurf_v2",
|
|
162
|
+
"entwurf_inbox_read",
|
|
163
|
+
"entwurf_callback",
|
|
164
|
+
"entwurf_fresh_call",
|
|
165
|
+
];
|
|
166
|
+
const IMPORT_OBSERVER = `data:text/javascript,${encodeURIComponent(
|
|
167
|
+
[
|
|
168
|
+
'import { appendFileSync } from "node:fs";',
|
|
169
|
+
'import { registerHooks } from "node:module";',
|
|
170
|
+
"const file = process.env.MOCK_NATIVE_RECEIPT;",
|
|
171
|
+
"const seen = new Set();",
|
|
172
|
+
"registerHooks({",
|
|
173
|
+
"\tresolve(specifier, context, nextResolve) {",
|
|
174
|
+
`\t\tif (file && !seen.has(specifier) && (specifier.startsWith("@earendil-works/") || specifier.startsWith(${JSON.stringify(CARRIER_SCHEME)}))) {`,
|
|
175
|
+
"\t\t\tseen.add(specifier);",
|
|
176
|
+
'\t\t\tappendFileSync(file, "resolve " + specifier + "\\n");',
|
|
177
|
+
"\t\t}",
|
|
178
|
+
"\t\treturn nextResolve(specifier, context);",
|
|
179
|
+
"\t},",
|
|
180
|
+
"});",
|
|
181
|
+
].join("\n"),
|
|
182
|
+
)}`;
|
|
112
183
|
/** The committed-intent frame must arrive within this of `go` — a validity cutoff under the MCP
|
|
113
184
|
* SDK's 60s request timeout, never an elapsed-time success inference. */
|
|
114
185
|
const INTENT_DEADLINE_MS = 30_000;
|
|
@@ -116,17 +187,14 @@ const INTERRUPTED_MESSAGE = "Tool entwurf_v2 was interrupted and may have partia
|
|
|
116
187
|
/** One sampled idle window between two counter reads, before the input. Not a quiescence proof. */
|
|
117
188
|
const IDLE_SAMPLE_MS = 300;
|
|
118
189
|
|
|
119
|
-
const runtimeInput = process.env.ENTWURF_PI_DURABLE_RUNTIME?.trim();
|
|
120
190
|
const bridgeInput = process.env.ENTWURF_PI_DURABLE_BRIDGE_ENTRY?.trim();
|
|
121
|
-
if (!
|
|
191
|
+
if (!bridgeInput) {
|
|
122
192
|
skipLive(
|
|
123
193
|
LABEL,
|
|
124
|
-
"set
|
|
125
|
-
"
|
|
126
|
-
"this gate provisions neither",
|
|
194
|
+
"set ENTWURF_PI_DURABLE_BRIDGE_ENTRY=<bridge index.js emitted from this checkout into a private bundle>; " +
|
|
195
|
+
"this gate does not provision it",
|
|
127
196
|
);
|
|
128
197
|
}
|
|
129
|
-
const runtime = path.resolve(runtimeInput);
|
|
130
198
|
const bridgeEntry = path.resolve(bridgeInput);
|
|
131
199
|
const FAULT = process.env.ENTWURF_PI_DURABLE_SEND_FAULT?.trim() || undefined;
|
|
132
200
|
if (FAULT !== undefined && FAULT !== "malformed-report") {
|
|
@@ -175,13 +243,12 @@ function canonicalJson(value: unknown): string {
|
|
|
175
243
|
}
|
|
176
244
|
|
|
177
245
|
// ---------------------------------------------------------------------------
|
|
178
|
-
// 0. Inputs — the same
|
|
246
|
+
// 0. Inputs — the same verified carrier / bundle registry facts as G-contact.
|
|
179
247
|
// ---------------------------------------------------------------------------
|
|
180
|
-
// The
|
|
181
|
-
// preflight use (pi-extensions/lib/pi-durable-runtime.ts).
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const resolver = path.join(runtime, PI_DURABLE_RESOLVER_RELATIVE);
|
|
248
|
+
// The carrier and its SDK set are verified by the ONE verifier the managed launch, setup and the fresh
|
|
249
|
+
// preflight use (pi-extensions/lib/pi-durable-runtime.ts), from this checkout's layout.
|
|
250
|
+
const carrier = checkPiDurableRuntime(piDurableLayoutAt(REPO));
|
|
251
|
+
const resolver = carrier.ok ? carrier.resolver : "";
|
|
185
252
|
const bundleRegistry = path.join(
|
|
186
253
|
path.resolve(bridgeEntry, "..", "..", "..", ".."),
|
|
187
254
|
"pi-extensions",
|
|
@@ -189,16 +256,8 @@ const bundleRegistry = path.join(
|
|
|
189
256
|
);
|
|
190
257
|
console.log("0. inputs");
|
|
191
258
|
ok(
|
|
192
|
-
`[QK:${QK}-INPUTS]
|
|
193
|
-
|
|
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 &&
|
|
259
|
+
`[QK:${QK}-INPUTS] this checkout's pi-durable carrier and the pi SDK set it resolves are the pin (${carrier.ok ? "verified" : `${carrier.reason}: ${carrier.detail}`}), and the bridge bundle's registry matches this checkout`,
|
|
260
|
+
carrier.ok &&
|
|
202
261
|
fs.existsSync(bridgeEntry) &&
|
|
203
262
|
fs.existsSync(bundleRegistry) &&
|
|
204
263
|
fs.readFileSync(bundleRegistry).equals(fs.readFileSync(path.join(REPO, "pi", "entwurf-capabilities.json"))),
|
|
@@ -218,35 +277,11 @@ ok(
|
|
|
218
277
|
`[QK:${QK}-BUNDLE-OWNER-JOIN] the bridge bundle's compiled owner join admits pi-durable-host (${joinLine || "not found"})`,
|
|
219
278
|
joinLine.includes('"pi-durable-host"'),
|
|
220
279
|
);
|
|
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");
|
|
280
|
+
if (failed > 0) fatal("inputs are not the verified carrier, this checkout's registry and owner join — refusing to run");
|
|
223
281
|
|
|
224
282
|
// Coarse pi-durable-specific operator guard (as G-contact): no operator-byte claim beyond it.
|
|
225
283
|
const operatorAgent = path.join(os.homedir(), ".pi", "agent");
|
|
226
|
-
|
|
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();
|
|
284
|
+
const operatorBefore = operatorPiDurableSnapshot(operatorAgent);
|
|
250
285
|
|
|
251
286
|
// ---------------------------------------------------------------------------
|
|
252
287
|
// Sandbox: one private world — HOME/XDG/agent, the four garden roots and ENTWURF_DIR.
|
|
@@ -290,6 +325,7 @@ const env: Record<string, string> = {
|
|
|
290
325
|
const hostEnv: Record<string, string> = {
|
|
291
326
|
...env,
|
|
292
327
|
...(FAULT === undefined ? {} : { ENTWURF_PI_DURABLE_SEND_FAULT: FAULT }),
|
|
328
|
+
...(NATIVE ? { MOCK_NATIVE_RECEIPT: path.join(dirs.tmp, "native-module.receipt") } : {}),
|
|
293
329
|
};
|
|
294
330
|
|
|
295
331
|
// ---------------------------------------------------------------------------
|
|
@@ -346,18 +382,17 @@ const server = http.createServer((req, res) => {
|
|
|
346
382
|
wire.push({ method: req.method ?? "", url: req.url ?? "", body });
|
|
347
383
|
const n = wire.length;
|
|
348
384
|
if (req.method === "POST" && req.url === "/v1/chat/completions" && n === 1) {
|
|
385
|
+
const calls = NATIVE ? NATIVE_CALLS : [{ id: CALL_ID, name: "entwurf_v2", arguments: scriptedArgs }];
|
|
349
386
|
sse(res, [
|
|
350
387
|
chunk(
|
|
351
388
|
{
|
|
352
389
|
role: "assistant",
|
|
353
|
-
tool_calls:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
],
|
|
390
|
+
tool_calls: calls.map((call, index) => ({
|
|
391
|
+
index,
|
|
392
|
+
id: call.id,
|
|
393
|
+
type: "function",
|
|
394
|
+
function: { name: call.name, arguments: JSON.stringify(call.arguments) },
|
|
395
|
+
})),
|
|
361
396
|
},
|
|
362
397
|
null,
|
|
363
398
|
),
|
|
@@ -1137,8 +1172,9 @@ interface LaunchedHost {
|
|
|
1137
1172
|
stderr(): string;
|
|
1138
1173
|
}
|
|
1139
1174
|
|
|
1140
|
-
async function launchHost(args: string[], what: string): Promise<LaunchedHost> {
|
|
1141
|
-
const
|
|
1175
|
+
async function launchHost(args: string[], what: string, imports: string[] = []): Promise<LaunchedHost> {
|
|
1176
|
+
const preload = ["--import", resolver, ...imports.flatMap((url) => ["--import", url])];
|
|
1177
|
+
const child = spawn(process.execPath, [...preload, HOST_DRIVER, bridgeEntry, ...args], {
|
|
1142
1178
|
cwd: dirs.project,
|
|
1143
1179
|
env: hostEnv,
|
|
1144
1180
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1606,9 +1642,131 @@ function exportR1Receipts(dir: string, ctx: R1Context | undefined): void {
|
|
|
1606
1642
|
);
|
|
1607
1643
|
}
|
|
1608
1644
|
|
|
1645
|
+
interface NativeContext {
|
|
1646
|
+
report: Report & { native?: boolean };
|
|
1647
|
+
receipt: string[];
|
|
1648
|
+
hostGardenId: string | undefined;
|
|
1649
|
+
}
|
|
1650
|
+
let nativeContext: NativeContext | undefined;
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* #130 P2: one operator module through the packaged `main`, one native turn. The order oracle is the
|
|
1654
|
+
* receipt file (module `init` vs the observer's first SDK/carrier resolutions); the registration,
|
|
1655
|
+
* coexistence and child-environment oracles are the native view's three settled results and the
|
|
1656
|
+
* host's own record; the wire is judged after the run (finalWireNative).
|
|
1657
|
+
*/
|
|
1658
|
+
async function nativeModuleCell(): Promise<void> {
|
|
1659
|
+
console.log("1. native module: the packaged main with one operator module, then one native turn");
|
|
1660
|
+
const host = await launchHost(["use the native module", "--native-module", MOCK_MODULE], "host", [IMPORT_OBSERVER]);
|
|
1661
|
+
const report = (await host.next(REPORT_TIMEOUT_MS)) as unknown as NativeContext["report"];
|
|
1662
|
+
if (report.error !== undefined) throw new Error(`host: ${report.error}`);
|
|
1663
|
+
// The host is alive and holding: own its bridge child from /proc now, as the other cells do.
|
|
1664
|
+
if (!observeBridges(host.owned))
|
|
1665
|
+
throw new Error(`GAP: host ${host.owned.pid}'s children were not observed coherently`);
|
|
1666
|
+
const receipt = fs
|
|
1667
|
+
.readFileSync(hostEnv.MOCK_NATIVE_RECEIPT as string, "utf8")
|
|
1668
|
+
.split("\n")
|
|
1669
|
+
.filter(Boolean);
|
|
1670
|
+
const firstResolve = receipt.findIndex((line) => line.startsWith("resolve "));
|
|
1671
|
+
ok(
|
|
1672
|
+
`[QK:PDM-INIT-BEFORE-SDK] the module's initialization completed before the first @earendil-works / carrier-scheme resolution in the host, and the TUI's and the runtime's own imports came after it (${receipt.length} receipt lines; first: ${receipt.slice(0, 3).join(" | ")})`,
|
|
1673
|
+
receipt[0] === "init" &&
|
|
1674
|
+
receipt.filter((line) => line === "init").length === 1 &&
|
|
1675
|
+
firstResolve === 1 &&
|
|
1676
|
+
receipt.includes("resolve @earendil-works/pi-tui") &&
|
|
1677
|
+
receipt.includes(`resolve ${CARRIER_SCHEME}core/model-runtime.js`),
|
|
1678
|
+
);
|
|
1679
|
+
const records = fs
|
|
1680
|
+
.readdirSync(dirs.sessions)
|
|
1681
|
+
.filter((name) => name.endsWith(".meta.json"))
|
|
1682
|
+
.map(
|
|
1683
|
+
(name) =>
|
|
1684
|
+
JSON.parse(fs.readFileSync(path.join(dirs.sessions, name), "utf8")) as { backend?: string; gardenId?: string },
|
|
1685
|
+
);
|
|
1686
|
+
const hostRecords = records.filter((r) => r.backend === "pi-durable");
|
|
1687
|
+
const hostGardenId = hostRecords.length === 1 ? hostRecords[0]?.gardenId : undefined;
|
|
1688
|
+
const calls = report.entries.flatMap((e) => e.toolCalls);
|
|
1689
|
+
const resultOf = (id: string) => report.entries.find((e) => e.kind === "pi.tool-result" && e.toolCallId === id);
|
|
1690
|
+
ok(
|
|
1691
|
+
`[QK:PDM-NATIVE-CALLS] the native view records the scripted assistant message's three calls — the module's tool, the native bash and the contact's entwurf_self — each with a settled, non-error result (${calls.map((c) => `${c.id}:${c.name}`).join(", ")})`,
|
|
1692
|
+
calls.length === 3 &&
|
|
1693
|
+
NATIVE_CALLS.every((want) => calls.some((c) => c.id === want.id && c.name === want.name)) &&
|
|
1694
|
+
NATIVE_CALLS.every((want) => resultOf(want.id)?.isError === false),
|
|
1695
|
+
);
|
|
1696
|
+
ok(
|
|
1697
|
+
`[QK:PDM-MODULE-TOOL-RAN] the module's own tool ran natively and answered its text (got ${JSON.stringify(resultOf(CALL_ID)?.text)})`,
|
|
1698
|
+
resultOf(CALL_ID)?.text === "mock:pdm",
|
|
1699
|
+
);
|
|
1700
|
+
ok(
|
|
1701
|
+
`[QK:PDM-BASH-CHILD-ENV] the native bash child process saw the value the module's initialization set (got ${JSON.stringify(resultOf("call_pdm_2")?.text)})`,
|
|
1702
|
+
resultOf("call_pdm_2")?.text === MOCK_MARK,
|
|
1703
|
+
);
|
|
1704
|
+
ok(
|
|
1705
|
+
`[QK:PDM-CONTACT-COEXISTS] beside the module, the contact still answers natively: entwurf_self names the host's own record (${hostGardenId ?? `${hostRecords.length} pi-durable records`})`,
|
|
1706
|
+
hostGardenId !== undefined && (resultOf("call_pdm_3")?.text ?? "").includes(hostGardenId),
|
|
1707
|
+
);
|
|
1708
|
+
const lastAssistant = report.entries.filter((e) => e.kind === "pi.assistant").at(-1);
|
|
1709
|
+
ok(
|
|
1710
|
+
`[QK:PDM-TURN-SETTLED] the turn ends on the scripted text and the conversation is no longer busy (last: ${JSON.stringify(lastAssistant?.text)}, busy ${report.busy})`,
|
|
1711
|
+
lastAssistant?.text === FINAL_TEXT && report.busy === false,
|
|
1712
|
+
);
|
|
1713
|
+
console.log(` notices: ${report.notices.join(" | ") || "none"}`);
|
|
1714
|
+
nativeContext = { report, receipt, hostGardenId };
|
|
1715
|
+
host.child.stdin?.write("close\n");
|
|
1716
|
+
const closed = await host.next(CLOSE_TIMEOUT_MS);
|
|
1717
|
+
await Promise.race([host.exited, sleep(CLOSE_TIMEOUT_MS)]);
|
|
1718
|
+
ok(
|
|
1719
|
+
`[QK:PDM-CLOSE] the host closes and exits 0 (closed ${String(closed.closed)}, exit ${host.exit().code})`,
|
|
1720
|
+
closed.closed === true && host.exit().code === 0,
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/** The native-module cell's wire, judged on the closed run's final snapshot. */
|
|
1725
|
+
function finalWireNative(ctx: NativeContext): void {
|
|
1726
|
+
console.log("3. wire, final snapshot (host exited, endpoint closed)");
|
|
1727
|
+
const first = wire[0]?.body ?? {};
|
|
1728
|
+
const offered = ((first.tools as { function?: { name?: string } }[] | undefined) ?? []).map(
|
|
1729
|
+
(t) => t.function?.name ?? "",
|
|
1730
|
+
);
|
|
1731
|
+
ok(
|
|
1732
|
+
`[QK:PDM-WIRE-TOOLS-OFFERED] the app's first model request offers the module's tool, the native bash and all six contact tools together (offered: ${offered.join(", ")})`,
|
|
1733
|
+
["mock_native_echo", "bash", ...CONTACT_TOOLS].every((name) => offered.includes(name)),
|
|
1734
|
+
);
|
|
1735
|
+
ok(
|
|
1736
|
+
`[QK:PDM-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(" | ")})`,
|
|
1737
|
+
!server.listening &&
|
|
1738
|
+
wire.length === 2 &&
|
|
1739
|
+
wire.every((w) => w.method === "POST" && w.url === "/v1/chat/completions"),
|
|
1740
|
+
);
|
|
1741
|
+
const second =
|
|
1742
|
+
(wire[1]?.body?.messages as { role?: string; tool_call_id?: string; content?: unknown }[] | undefined) ?? [];
|
|
1743
|
+
const toolText = (id: string) => messageText(second.find((m) => m.role === "tool" && m.tool_call_id === id)?.content);
|
|
1744
|
+
ok(
|
|
1745
|
+
`[QK:PDM-RESULTS-REACH-MODEL] the second model request carries all three results as the native view recorded them (module ${JSON.stringify(toolText(CALL_ID))})`,
|
|
1746
|
+
NATIVE_CALLS.every(
|
|
1747
|
+
(call) =>
|
|
1748
|
+
toolText(call.id) !== "" &&
|
|
1749
|
+
toolText(call.id) ===
|
|
1750
|
+
ctx.report.entries.find((e) => e.kind === "pi.tool-result" && e.toolCallId === call.id)?.text,
|
|
1751
|
+
),
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** The native-module cell's raw bytes. */
|
|
1756
|
+
function exportNativeReceipts(dir: string, ctx: NativeContext | undefined): void {
|
|
1757
|
+
fs.writeFileSync(path.join(dir, "wire.json"), `${JSON.stringify(wire, null, 1)}\n`);
|
|
1758
|
+
if (ctx === undefined) return;
|
|
1759
|
+
fs.writeFileSync(path.join(dir, "native-view.json"), `${JSON.stringify(ctx.report, null, 1)}\n`);
|
|
1760
|
+
fs.writeFileSync(path.join(dir, "module-receipt.log"), `${ctx.receipt.join("\n")}\n`);
|
|
1761
|
+
fs.writeFileSync(
|
|
1762
|
+
path.join(dir, "correlation.json"),
|
|
1763
|
+
`${JSON.stringify({ cell: CELL, module: MOCK_MODULE, hostGardenId: ctx.hostGardenId, calls: NATIVE_CALLS, wireRequests: wire.length }, null, 1)}\n`,
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1609
1767
|
let primary: unknown;
|
|
1610
1768
|
try {
|
|
1611
|
-
await (R1 ? r1Cell() : cell());
|
|
1769
|
+
await (R1 ? r1Cell() : NATIVE ? nativeModuleCell() : cell());
|
|
1612
1770
|
} catch (error) {
|
|
1613
1771
|
primary = error;
|
|
1614
1772
|
}
|
|
@@ -1634,20 +1792,22 @@ ok(
|
|
|
1634
1792
|
);
|
|
1635
1793
|
if (R1) {
|
|
1636
1794
|
if (r1Context !== undefined) finalWireR1(r1Context);
|
|
1795
|
+
} else if (NATIVE) {
|
|
1796
|
+
if (nativeContext !== undefined) finalWireNative(nativeContext);
|
|
1637
1797
|
} else if (context !== undefined) finalWire(context);
|
|
1638
1798
|
if (receiptsInput !== undefined) {
|
|
1639
1799
|
try {
|
|
1640
1800
|
if (R1) exportR1Receipts(receiptsInput, r1Context);
|
|
1801
|
+
else if (NATIVE) exportNativeReceipts(receiptsInput, nativeContext);
|
|
1641
1802
|
else exportReceipts(receiptsInput, context);
|
|
1642
1803
|
ok(`[QK:${QK}-RECEIPTS-EXPORTED] the cell's raw bytes and correlation are in ${receiptsInput}`, true);
|
|
1643
1804
|
} catch (error) {
|
|
1644
1805
|
ok(`[QK:${QK}-RECEIPTS-EXPORTED] receipt export failed: ${(error as Error).message}`, false);
|
|
1645
1806
|
}
|
|
1646
1807
|
}
|
|
1647
|
-
const before = JSON.parse(operatorBefore) as { durableRecords: string[]; senders: boolean };
|
|
1648
1808
|
ok(
|
|
1649
|
-
`[QK:${QK}-OPERATOR-UNTOUCHED] coarse pi-durable guard: the operator's
|
|
1650
|
-
|
|
1809
|
+
`[QK:${QK}-OPERATOR-UNTOUCHED] coarse pi-durable registration guard: this run left the operator's pi-durable registrations exactly as it found them (${describeOperatorSnapshot(operatorBefore)} — same names, same bytes; existing state is not a red; no auth or session content read)`,
|
|
1810
|
+
operatorSnapshotUnchanged(operatorBefore, operatorPiDurableSnapshot(operatorAgent)),
|
|
1651
1811
|
);
|
|
1652
1812
|
if (alive.length > 0) console.log(`sandbox kept because owned processes are still alive: ${root}`);
|
|
1653
1813
|
else if (process.env.ENTWURF_PI_DURABLE_KEEP === "1") console.log(`sandbox kept: ${root}`);
|