@junghanacs/entwurf 0.20.1 → 0.22.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 +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -31,7 +31,7 @@ import * as path from "node:path";
|
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
32
|
import { type EntwurfFactsResult, recordLessSocketMessage } from "../pi-extensions/lib/entwurf-fact-provider.ts";
|
|
33
33
|
import type { PeerFact } from "../pi-extensions/lib/entwurf-facts.ts";
|
|
34
|
-
import { renderEntwurfPeers } from "../pi-extensions/lib/entwurf-peers-render.ts";
|
|
34
|
+
import { ENTWURF_PEERS_RENDER_LIMIT, renderEntwurfPeers } from "../pi-extensions/lib/entwurf-peers-render.ts";
|
|
35
35
|
import type { FactLiveness } from "../pi-extensions/lib/entwurf-v2-contract.ts";
|
|
36
36
|
import type { MetaCitizenBackend } from "../pi-extensions/lib/meta-session.ts";
|
|
37
37
|
import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
|
|
@@ -199,6 +199,10 @@ function main(): void {
|
|
|
199
199
|
);
|
|
200
200
|
const bounded = renderEntwurfPeers({ facts: { peers: manyPeers, recordLessSockets: [] }, diagnostics: [] });
|
|
201
201
|
ok("bounded text omits older entries when peer list is large", bounded.text.includes("older entries omitted"));
|
|
202
|
+
ok(
|
|
203
|
+
"#112: human row budget remains exactly 32",
|
|
204
|
+
ENTWURF_PEERS_RENDER_LIMIT === 32 && bounded.text.includes("(8 older entries omitted; showing latest 32)"),
|
|
205
|
+
);
|
|
202
206
|
ok("bounded text shows latest entries", bounded.text.includes("20260612T000039-aaaaaa"));
|
|
203
207
|
ok("bounded text omits oldest entry", !bounded.text.includes("20260612T000000-aaaaaa backend="));
|
|
204
208
|
ok("bounded payload still carries every peer", bounded.payload.peers.length === 40);
|
|
@@ -255,6 +259,14 @@ function main(): void {
|
|
|
255
259
|
ok("wiring: bridge calls renderEntwurfPeers(", bridgeSrc.includes("renderEntwurfPeers("));
|
|
256
260
|
ok("wiring: native pi tool calls listEntwurfFacts(", nativeSrc.includes("listEntwurfFacts("));
|
|
257
261
|
ok("wiring: native pi tool calls renderEntwurfPeers(", nativeSrc.includes("renderEntwurfPeers("));
|
|
262
|
+
ok(
|
|
263
|
+
"#112 wiring: MCP observation uses the render limit",
|
|
264
|
+
bridgeSrc.includes("observationLimit: ENTWURF_PEERS_RENDER_LIMIT"),
|
|
265
|
+
);
|
|
266
|
+
ok(
|
|
267
|
+
"#112 wiring: native pi observation uses the render limit",
|
|
268
|
+
nativeSrc.includes("observationLimit: render.ENTWURF_PEERS_RENDER_LIMIT"),
|
|
269
|
+
);
|
|
258
270
|
ok(
|
|
259
271
|
"wiring: bridge does not paste full JSON payload into human text",
|
|
260
272
|
!bridgeSrc.includes("JSON.stringify(payload)"),
|
|
@@ -266,14 +266,14 @@ ok(
|
|
|
266
266
|
/existsSync\s*\(/.test(piBody) && /controlSocketPathIn\s*\(\s*ENTWURF_DIR\s*,/.test(piBody),
|
|
267
267
|
);
|
|
268
268
|
|
|
269
|
-
const metaBuilder = functionBody("
|
|
269
|
+
const metaBuilder = functionBody("buildMetaSenderEnvelope");
|
|
270
270
|
// FIRST of the two derivation claims so a binary-fallback mutant dies here, not at the seam pin.
|
|
271
271
|
ok(
|
|
272
|
-
"
|
|
272
|
+
"buildMetaSenderEnvelope does not fall back every non-native-push backend to self-fetch [QK:SELFADDR-NO-FALLBACK-SELF-FETCH]",
|
|
273
273
|
!/\?\s*"native-push"\s*:\s*"self-fetch"/.test(metaBuilder),
|
|
274
274
|
);
|
|
275
275
|
ok(
|
|
276
|
-
"
|
|
276
|
+
"buildMetaSenderEnvelope derives self-fetch through resolveMailboxWakeModeCapability(identity), not a backend-name list [QK:SELFADDR-MAILBOX-WAKE-SEAM]",
|
|
277
277
|
/resolveMailboxWakeModeCapability\s*\(\s*identity\s*\)/.test(metaBuilder),
|
|
278
278
|
);
|
|
279
279
|
|
|
@@ -443,14 +443,13 @@ function metaRailRenderIsHonest(region: string): { honest: boolean; reason: stri
|
|
|
443
443
|
throw new Error(`class ${name} body never closed`);
|
|
444
444
|
};
|
|
445
445
|
|
|
446
|
-
// Anchored per class (not a repo-wide grep) so a mention
|
|
447
|
-
//
|
|
446
|
+
// Anchored per class (not a repo-wide grep) so a mention elsewhere cannot make
|
|
447
|
+
// either row vacuously green. Native hook mechanisms vary by backend; Codex
|
|
448
|
+
// carries request-scoped identity instead of a shared process marker.
|
|
448
449
|
for (const cls of ["EntwurfEnvelopeWiringError", "EntwurfSenderIdentityError"]) {
|
|
449
450
|
const body = classBody(cls);
|
|
450
|
-
ok(
|
|
451
|
-
|
|
452
|
-
/SessionStart/.test(body) && /PreInvocation/.test(body),
|
|
453
|
-
);
|
|
451
|
+
ok(`${cls} names a native sender marker path`, /native (sender )?(claim|hook)|sender marker/.test(body));
|
|
452
|
+
ok(`${cls} names the managed Codex request identity path`, /managed Codex/.test(body));
|
|
454
453
|
ok(
|
|
455
454
|
`${cls} no longer presents SessionStart/Claude as THE marker writer`,
|
|
456
455
|
!/The native SessionStart hook writes that marker/.test(body) &&
|
|
@@ -465,33 +464,33 @@ function metaRailRenderIsHonest(region: string): { honest: boolean; reason: stri
|
|
|
465
464
|
// THIS session's own receiver inbox can actually wake (slice-2 presence marker), not a
|
|
466
465
|
// hardcoded true. An inactive receiver must still return the meta identity (replyable:false)
|
|
467
466
|
// — degrading to null would erase who-sent and fall through to external-mcp.
|
|
468
|
-
const metaBody = functionBody("
|
|
469
|
-
ok("
|
|
470
|
-
ok("
|
|
467
|
+
const metaBody = functionBody("buildMetaSenderEnvelope");
|
|
468
|
+
ok("buildMetaSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(metaBody));
|
|
469
|
+
ok("buildMetaSenderEnvelope no longer hardcodes `replyable: true`", !/replyable:\s*true/.test(metaBody));
|
|
471
470
|
ok(
|
|
472
471
|
// #101: the identity match alone is no longer the whole answer. `entwurf_self` composes
|
|
473
472
|
// the SAME `resolveMailboxReceiverFacts` the v2 dispatch seam uses — reading the receiver
|
|
474
473
|
// marker AND, where the watch owner is the sender-marker process, the join that says that
|
|
475
474
|
// owner is still serving this garden. A citizen's self-reported replyability and what
|
|
476
475
|
// dispatch decides about it come from one measurement, so they cannot disagree.
|
|
477
|
-
"
|
|
476
|
+
"buildMetaSenderEnvelope derives active-receiver from the SHARED receiver composition (both markers)",
|
|
478
477
|
/resolveMailboxReceiverFacts\s*\(/.test(metaBody) &&
|
|
479
478
|
/readMetaReceiverMarker\s*\(/.test(metaBody) &&
|
|
480
479
|
/readMetaSenderMarker\s*\(/.test(metaBody),
|
|
481
480
|
);
|
|
482
481
|
ok(
|
|
483
|
-
"
|
|
482
|
+
"buildMetaSenderEnvelope no longer copies one match into both receiver facts",
|
|
484
483
|
!/ownerAlive:\s*active/.test(metaBody) && !/watchArmed:\s*active/.test(metaBody),
|
|
485
484
|
);
|
|
486
485
|
ok(
|
|
487
486
|
// #101 (cross-review): both markers are read with their liveness guards ON. A reader that
|
|
488
487
|
// opted out would accept a dead session's leftover file as "which garden this pid serves
|
|
489
488
|
// now", and every fixture pid in a gate is live, so nothing dynamic here could tell.
|
|
490
|
-
"
|
|
489
|
+
"buildMetaSenderEnvelope reads neither marker with the owner guard disabled",
|
|
491
490
|
!/verifyOwner:\s*false/.test(metaBody),
|
|
492
491
|
);
|
|
493
492
|
ok(
|
|
494
|
-
"
|
|
493
|
+
"buildMetaSenderEnvelope keeps meta identity + derived replyable (inactive → not null)",
|
|
495
494
|
/origin:\s*"meta-session"/.test(metaBody) && /replyable:\s*self\.replyable/.test(metaBody),
|
|
496
495
|
);
|
|
497
496
|
|
|
@@ -280,8 +280,8 @@ ok(
|
|
|
280
280
|
// ── native-push (봉인 1/2/4): a THIRD table, disjoint from pi socket + mailbox ──
|
|
281
281
|
const NATIVE_PUSH_LIVENESSES = ["alive", "dead", "indeterminate"] as const;
|
|
282
282
|
|
|
283
|
-
// domain: antigravity
|
|
284
|
-
// from
|
|
283
|
+
// domain: antigravity and Codex are native-push; pi/claude-code are NOT; the domain
|
|
284
|
+
// is DISJOINT from pi socket liveness (a backend is never in both rails).
|
|
285
285
|
eq("native-push domain: antigravity supported", nativePushSupported("antigravity"), true);
|
|
286
286
|
eq("native-push domain: pi is NOT native-push (it is pi-socket)", nativePushSupported("pi"), false);
|
|
287
287
|
eq(
|
|
@@ -289,7 +289,8 @@ eq(
|
|
|
289
289
|
nativePushSupported("claude-code"),
|
|
290
290
|
false,
|
|
291
291
|
);
|
|
292
|
-
eq("native-push domain:
|
|
292
|
+
eq("native-push domain: Codex supported", nativePushSupported("codex"), true);
|
|
293
|
+
eq("native-push domain: NATIVE_PUSH_BACKENDS exact", [...NATIVE_PUSH_BACKENDS], ["antigravity", "codex"]);
|
|
293
294
|
ok(
|
|
294
295
|
"native-push ∩ pi-socket liveness domain = ∅ (separate rails)",
|
|
295
296
|
!NATIVE_PUSH_BACKENDS.some((b) => (LIVENESS_DOMAIN_BACKENDS as readonly string[]).includes(b)),
|
|
@@ -381,7 +381,7 @@ async function main(): Promise<void> {
|
|
|
381
381
|
// ── 8: unsupported + ff + seam says undeliverable → reject, no plan, no lock ─
|
|
382
382
|
{
|
|
383
383
|
const t = mkDeps({
|
|
384
|
-
resolution: { identity: identity("
|
|
384
|
+
resolution: { identity: identity("omp"), preProbeAddressConflict: false },
|
|
385
385
|
mailboxDeliverable: false,
|
|
386
386
|
});
|
|
387
387
|
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "mail" }, t.deps);
|
|
@@ -399,8 +399,7 @@ async function main(): Promise<void> {
|
|
|
399
399
|
// reject, NO plan, NO lock, NO inspect/probe. This is the v2 closure of the gap slice
|
|
400
400
|
// 2d-2 closed for v1: the decider no longer trusts wake-mode alone; the required seam's
|
|
401
401
|
// active-receiver verdict governs, so a reply to a dead claude-code is refused, not
|
|
402
|
-
// enqueued as mailbox garbage.
|
|
403
|
-
// receipt level — that is the point: the seam, not the backend, decides.) ────────────
|
|
402
|
+
// enqueued as mailbox garbage. ───────────────────────────────────────────────────
|
|
404
403
|
{
|
|
405
404
|
const t = mkDeps({
|
|
406
405
|
resolution: { identity: identity("claude-code"), preProbeAddressConflict: false },
|
|
@@ -581,7 +580,7 @@ async function main(): Promise<void> {
|
|
|
581
580
|
// ff × alive → execute native-push send; plan carries route/backend/nativeSessionId; lock null.
|
|
582
581
|
const t = mkDeps({
|
|
583
582
|
resolution: npResolution,
|
|
584
|
-
nativePush: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
|
|
583
|
+
nativePush: { status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" } },
|
|
585
584
|
});
|
|
586
585
|
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
|
|
587
586
|
ok("native-push ff+alive: execute", isExecute(d));
|
|
@@ -594,7 +593,10 @@ async function main(): Promise<void> {
|
|
|
594
593
|
t.mailboxCalls.length === 0,
|
|
595
594
|
);
|
|
596
595
|
if (isExecute(d) && d.plan.transport === "native-push") {
|
|
597
|
-
ok(
|
|
596
|
+
ok(
|
|
597
|
+
"native-push ff+alive: plan route = probed route",
|
|
598
|
+
d.plan.route.backend === "antigravity" && d.plan.route.lsAddress === "127.0.0.1:5599",
|
|
599
|
+
);
|
|
598
600
|
ok("native-push ff+alive: plan backend = antigravity", d.plan.backend === "antigravity");
|
|
599
601
|
ok("native-push ff+alive: plan nativeSessionId from identity", d.plan.nativeSessionId === `native-${GID}`);
|
|
600
602
|
ok(
|
|
@@ -35,7 +35,11 @@ function ok(label: string, cond: boolean): void {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const CONV = "conv-xyz";
|
|
38
|
-
const PLANTED: NativePushRoute = { lsAddress: "127.0.0.1:5600" };
|
|
38
|
+
const PLANTED: NativePushRoute = { backend: "antigravity", lsAddress: "127.0.0.1:5600" };
|
|
39
|
+
|
|
40
|
+
function agyAddress(route: NativePushRoute | undefined): string | undefined {
|
|
41
|
+
return route?.backend === "antigravity" ? route.lsAddress : undefined;
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
interface SendCall {
|
|
41
45
|
route: NativePushRoute;
|
|
@@ -48,6 +52,7 @@ interface FakeAdapterConfig {
|
|
|
48
52
|
probes: NativePushProbeResult[];
|
|
49
53
|
/** 0-based indices of send() calls that THROW. */
|
|
50
54
|
sendFailAt?: number[];
|
|
55
|
+
retriable?: boolean;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function makeFakeAdapter(config: FakeAdapterConfig): {
|
|
@@ -60,6 +65,7 @@ function makeFakeAdapter(config: FakeAdapterConfig): {
|
|
|
60
65
|
const sends: SendCall[] = [];
|
|
61
66
|
const adapter: NativePushAdapter = {
|
|
62
67
|
id: "antigravity",
|
|
68
|
+
retriable: config.retriable ?? true,
|
|
63
69
|
async probe() {
|
|
64
70
|
const p = config.probes[probeIdx] ?? { status: "dead", reason: "fake: probes exhausted" };
|
|
65
71
|
probeIdx++;
|
|
@@ -81,31 +87,53 @@ async function main(): Promise<void> {
|
|
|
81
87
|
const r = await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
|
|
82
88
|
ok("success: retried:false", r.success === true && r.retried === false);
|
|
83
89
|
ok("success: exactly ONE send", sends.length === 1);
|
|
84
|
-
ok("success: send used the PLANTED route (no re-probe)", sends[0]?.route
|
|
90
|
+
ok("success: send used the PLANTED route (no re-probe)", agyAddress(sends[0]?.route) === "127.0.0.1:5600");
|
|
85
91
|
ok("success: ZERO re-probe", probeCount() === 0);
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
// Codex's vendor queue may accept before its receipt fails: no re-probe, no replay.
|
|
95
|
+
{
|
|
96
|
+
const { adapter, sends, probeCount } = makeFakeAdapter({
|
|
97
|
+
probes: [{ status: "alive", route: { backend: "antigravity", lsAddress: "must-not-be-used" } }],
|
|
98
|
+
sendFailAt: [0, 1],
|
|
99
|
+
retriable: false,
|
|
100
|
+
});
|
|
101
|
+
await assert.rejects(() => deliverViaNativePush(adapter, PLANTED, CONV, "hi"), /fake send fail/);
|
|
102
|
+
ok("non-retriable failure makes exactly ONE send", sends.length === 1);
|
|
103
|
+
ok("non-retriable failure makes ZERO probes", probeCount() === 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
// ── fail → re-probe alive → re-send success: retried, fresh route ────────────
|
|
89
107
|
{
|
|
90
108
|
const { adapter, sends, probeCount } = makeFakeAdapter({
|
|
91
|
-
probes: [{ status: "alive", route: { lsAddress: "127.0.0.1:5601" } }],
|
|
109
|
+
probes: [{ status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5601" } }],
|
|
92
110
|
sendFailAt: [0],
|
|
93
111
|
});
|
|
94
112
|
const r = await deliverViaNativePush(adapter, PLANTED, CONV, "hi");
|
|
95
113
|
ok("retry: retried:true", r.success === true && r.retried === true);
|
|
96
114
|
ok("retry: TWO sends (initial + one retry)", sends.length === 2);
|
|
97
115
|
ok("retry: exactly ONE re-probe", probeCount() === 1);
|
|
98
|
-
ok("retry: 1st send used the planted route", sends[0]?.route
|
|
116
|
+
ok("retry: 1st send used the planted route", agyAddress(sends[0]?.route) === "127.0.0.1:5600");
|
|
99
117
|
ok(
|
|
100
118
|
"retry: 2nd send used the RE-DISCOVERED route (not the stale one)",
|
|
101
|
-
sends[1]?.route
|
|
119
|
+
agyAddress(sends[1]?.route) === "127.0.0.1:5601",
|
|
120
|
+
);
|
|
121
|
+
// The retry replays CONTENT, never re-derives it. The caller hands this leaf one already
|
|
122
|
+
// rendered string (the #95 sender envelope is rendered ONCE, upstream, with its own
|
|
123
|
+
// timestamp), so the only honest retry is the same bytes — a second render, or any
|
|
124
|
+
// per-attempt decoration here, would deliver a body that disagrees with the first
|
|
125
|
+
// attempt the target may already have accepted. Asserted at this leaf so the claim holds
|
|
126
|
+
// without coupling sender rendering into the adapter hand.
|
|
127
|
+
ok(
|
|
128
|
+
"[QK:NATIVE-PUSH-RETRY-BYTE-IDENTITY] the retry replays byte-identical content, never a re-derived body",
|
|
129
|
+
sends[0]?.content === sends[1]?.content,
|
|
102
130
|
);
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
// ── fail → re-probe alive → re-send FAIL: throws (fail-loud, no 3rd attempt) ──
|
|
106
134
|
{
|
|
107
135
|
const { adapter, sends } = makeFakeAdapter({
|
|
108
|
-
probes: [{ status: "alive", route: { lsAddress: "127.0.0.1:5601" } }],
|
|
136
|
+
probes: [{ status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5601" } }],
|
|
109
137
|
sendFailAt: [0, 1],
|
|
110
138
|
});
|
|
111
139
|
let threw = false;
|
|
@@ -182,7 +210,7 @@ async function main(): Promise<void> {
|
|
|
182
210
|
);
|
|
183
211
|
ok(
|
|
184
212
|
"makeNativePushSend: sent the plan message over the plan route",
|
|
185
|
-
sends[0]?.content === "payload" && sends[0]?.route
|
|
213
|
+
sends[0]?.content === "payload" && agyAddress(sends[0]?.route) === "127.0.0.1:5600",
|
|
186
214
|
);
|
|
187
215
|
ok("makeNativePushSend: lock IGNORED (lock-free — a bogus lock did not break delivery)", sends.length === 1);
|
|
188
216
|
}
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* (a pi-alive citizen → control-socket execute; acquireLock spy saw {dir: lockDir}).
|
|
10
10
|
* B. control `sendOverSocket` builds the RpcSendCommand (type/message/mode/wants_reply/
|
|
11
11
|
* sender) and maps response.success→outcome; the hand releases under `lockDir`.
|
|
12
|
+
* B2. #115 — the SAME closure carries `response.error` onto the RpcSendResult, so a
|
|
13
|
+
* completed-but-refused RPC reaches the caller as `rejected` WITH the receiver's
|
|
14
|
+
* named reason. Dropping that one field renders a reasonless reject, which is the
|
|
15
|
+
* field symptom; no other deterministic gate holds this wiring.
|
|
12
16
|
* D. the meta-mailbox hand enqueues onto the wired sessionsDir/mailboxDir.
|
|
13
17
|
* E. Q3 + Q5 — a dead control send re-resolves (claude-code citizen) to the mailbox and
|
|
14
18
|
* enqueues through the SAME sendViaMailbox instance (same enqueue spy) on the SAME dirs
|
|
@@ -27,8 +31,14 @@ import {
|
|
|
27
31
|
makeProductionEntwurfV2Deps,
|
|
28
32
|
type ProductionEntwurfV2Opts,
|
|
29
33
|
} from "../pi-extensions/lib/entwurf-v2-production.ts";
|
|
30
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
type EntwurfV2RunDeps,
|
|
36
|
+
type EntwurfV2RunResult,
|
|
37
|
+
executeDispatch,
|
|
38
|
+
runEntwurfV2,
|
|
39
|
+
} from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
31
40
|
import type { ControlSocketPlan, MetaMailboxPlan } from "../pi-extensions/lib/entwurf-v2-send.ts";
|
|
41
|
+
import { formatMetaMailboxBody, type MailboxSenderEnvelope } from "../pi-extensions/lib/meta-mailbox-body.ts";
|
|
32
42
|
import type { MetaIdentity, MetaReceiverMarker, MetaSenderMarker } from "../pi-extensions/lib/meta-session.ts";
|
|
33
43
|
import type { NativePushAdapter, NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
|
|
34
44
|
import type { TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
|
|
@@ -43,10 +53,21 @@ function ok(label: string, cond: boolean): void {
|
|
|
43
53
|
const GID = "20260613T100000-aaaaaa";
|
|
44
54
|
/** The garden a switched-away owner serves instead (#101). */
|
|
45
55
|
const OTHER_GID = "20260613T100000-bbbbbb";
|
|
56
|
+
/** The one trusted sender every rail in this gate renders from. */
|
|
57
|
+
const SENDER: MailboxSenderEnvelope = {
|
|
58
|
+
sessionId: "self",
|
|
59
|
+
agentId: "pi/x",
|
|
60
|
+
cwd: "/cwd",
|
|
61
|
+
timestamp: "2026-06-13T00:00:00.000Z",
|
|
62
|
+
};
|
|
46
63
|
const LOCK_DIR = "/fake/locks";
|
|
47
64
|
const SESSIONS_DIR = "/fake/sessions";
|
|
48
65
|
const MAILBOX_DIR = "/fake/mailbox";
|
|
49
66
|
const CONTROL_DIR = "/fake/ctl";
|
|
67
|
+
/** The named in-band refusal a real receiver answers with while it compacts (#115).
|
|
68
|
+
* It is a plain opaque string here on purpose: the production seam must carry WHATEVER
|
|
69
|
+
* reason the receiver named, not a vocabulary this gate recognises. */
|
|
70
|
+
const IN_BAND_REJECT_REASON = "compacting";
|
|
50
71
|
|
|
51
72
|
function identity(backend: MetaIdentity["backend"], gardenId = GID): MetaIdentity {
|
|
52
73
|
return {
|
|
@@ -132,7 +153,36 @@ interface Spies {
|
|
|
132
153
|
/** which backend the native-push adapter resolver was asked for (decide + execute). */
|
|
133
154
|
nativePushResolve: string[];
|
|
134
155
|
nativePushProbe: { conv: string }[];
|
|
135
|
-
nativePushSend: {
|
|
156
|
+
nativePushSend: { route: string; conv: string; content: string }[];
|
|
157
|
+
/** every `senderProvider()` call — ONE provider feeds all three rails, so a hand that
|
|
158
|
+
* resolves the envelope twice (or not at all) is visible here. */
|
|
159
|
+
senderCalls: number[];
|
|
160
|
+
/** ORDERED trace of the native-push hand's observable steps. Counts alone cannot see
|
|
161
|
+
* ORDER, and order is the whole safety claim: the envelope must be resolved BEFORE the
|
|
162
|
+
* adapter is touched, so a provider throw leaves the target's conversation untouched
|
|
163
|
+
* rather than half-delivered. `runInTwoPhases` marks where the execute phase starts. */
|
|
164
|
+
events: ("sender" | "resolve" | "probe" | "send")[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* `runEntwurfV2` split at its own seam so the EXECUTE phase has a MEASURED boundary. The decide
|
|
169
|
+
* phase legitimately resolves the adapter and probes (봉인 4: one adapter for both hands), so an
|
|
170
|
+
* order claim about the send hand has to start after decide returns — but the boundary must be
|
|
171
|
+
* the index recorded between the two calls, never something inferred from the event VALUES. An
|
|
172
|
+
* earlier version sliced after the last `probe`, which a regression could defeat: an executor
|
|
173
|
+
* that resolved and probed before the sender would make its own probe the "last" one and hide
|
|
174
|
+
* the illegal prefix it just produced. This records the length instead, so ANY extra execute-side
|
|
175
|
+
* resolve or probe ahead of the sender stays inside the slice and fails.
|
|
176
|
+
*/
|
|
177
|
+
async function runInTwoPhases(
|
|
178
|
+
input: DispatchInput,
|
|
179
|
+
deps: EntwurfV2RunDeps,
|
|
180
|
+
spies: Spies,
|
|
181
|
+
): Promise<{ result: EntwurfV2RunResult; executeEvents: string[] }> {
|
|
182
|
+
const decision = await deps.decide(input);
|
|
183
|
+
const executeStart = spies.events.length;
|
|
184
|
+
const result = await executeDispatch(decision, deps.executor);
|
|
185
|
+
return { result, executeEvents: spies.events.slice(executeStart) };
|
|
136
186
|
}
|
|
137
187
|
|
|
138
188
|
/** Build a factory whose every leaf IO is a spy. `over` lets a case shape the decision
|
|
@@ -142,10 +192,15 @@ function makeSpiedFactory(over: {
|
|
|
142
192
|
recordExists?: boolean;
|
|
143
193
|
inspectKind?: TargetSocketInspection["kind"];
|
|
144
194
|
probe?: "alive" | "dead" | "indeterminate";
|
|
145
|
-
|
|
195
|
+
/** `in-band-reject` (#115): the RPC COMPLETES and the receiver refuses in band
|
|
196
|
+
* (`{success:false,error:"compacting"}`) — distinct from `dead-throw`, which never
|
|
197
|
+
* reaches a response at all. It is the only arm that carries `response.error`. */
|
|
198
|
+
rpc?: "success" | "dead-throw" | "in-band-reject";
|
|
146
199
|
classifyDead?: boolean;
|
|
147
200
|
/** #50 C3 — a caller with no authoritative sender (senderProvider → undefined). */
|
|
148
201
|
noSender?: boolean;
|
|
202
|
+
/** #95 — the envelope source itself fails: the send must reject with NOTHING injected. */
|
|
203
|
+
senderThrows?: boolean;
|
|
149
204
|
/** the native-push adapter probe result (only reached on a native-push backend). */
|
|
150
205
|
nativePushProbe?: NativePushProbeResult;
|
|
151
206
|
/** SE-2 2d-3 — the target's receiver presence marker: "active" (matches identity,
|
|
@@ -166,12 +221,16 @@ function makeSpiedFactory(over: {
|
|
|
166
221
|
nativePushResolve: [],
|
|
167
222
|
nativePushProbe: [],
|
|
168
223
|
nativePushSend: [],
|
|
224
|
+
senderCalls: [],
|
|
225
|
+
events: [],
|
|
169
226
|
};
|
|
170
227
|
const opts: ProductionEntwurfV2Opts = {
|
|
171
|
-
senderProvider: () =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
228
|
+
senderProvider: () => {
|
|
229
|
+
spies.senderCalls.push(spies.senderCalls.length + 1);
|
|
230
|
+
spies.events.push("sender");
|
|
231
|
+
if (over.senderThrows) throw new Error("fake: sender envelope unavailable");
|
|
232
|
+
return over.noSender ? undefined : SENDER;
|
|
233
|
+
},
|
|
175
234
|
lockDir: LOCK_DIR,
|
|
176
235
|
sessionsDir: SESSIONS_DIR,
|
|
177
236
|
mailboxDir: MAILBOX_DIR,
|
|
@@ -219,6 +278,11 @@ function makeSpiedFactory(over: {
|
|
|
219
278
|
e.code = "ECONNREFUSED";
|
|
220
279
|
throw e;
|
|
221
280
|
}
|
|
281
|
+
if (over.rpc === "in-band-reject") {
|
|
282
|
+
return {
|
|
283
|
+
response: { type: "response", command: command.type, success: false, error: IN_BAND_REJECT_REASON },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
222
286
|
return { response: { type: "response", command: command.type, success: true } };
|
|
223
287
|
},
|
|
224
288
|
enqueue: (o) => {
|
|
@@ -231,14 +295,19 @@ function makeSpiedFactory(over: {
|
|
|
231
295
|
// two hands never resolve different adapters.
|
|
232
296
|
resolveNativePushAdapter: (backend: string): NativePushAdapter => {
|
|
233
297
|
spies.nativePushResolve.push(backend);
|
|
298
|
+
spies.events.push("resolve");
|
|
234
299
|
return {
|
|
235
|
-
id: "
|
|
300
|
+
id: backend as NativePushAdapter["id"],
|
|
301
|
+
retriable: backend === "antigravity",
|
|
236
302
|
async probe(conv) {
|
|
237
303
|
spies.nativePushProbe.push({ conv });
|
|
304
|
+
spies.events.push("probe");
|
|
238
305
|
return over.nativePushProbe ?? { status: "dead", reason: "fake: no native-push probe configured" };
|
|
239
306
|
},
|
|
240
307
|
async send(route, conv, content) {
|
|
241
|
-
|
|
308
|
+
const routeLabel = route.backend === "antigravity" ? route.lsAddress : route.socketPath;
|
|
309
|
+
spies.nativePushSend.push({ route: routeLabel, conv, content });
|
|
310
|
+
spies.events.push("send");
|
|
242
311
|
},
|
|
243
312
|
};
|
|
244
313
|
},
|
|
@@ -277,14 +346,14 @@ async function main(): Promise<void> {
|
|
|
277
346
|
{
|
|
278
347
|
const { deps, spies } = makeSpiedFactory({
|
|
279
348
|
backend: "antigravity",
|
|
280
|
-
nativePushProbe: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
|
|
349
|
+
nativePushProbe: { status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" } },
|
|
281
350
|
});
|
|
282
351
|
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi agy" }, deps);
|
|
283
352
|
ok("A3: antigravity ff → native-push delivered", result.kind === "executed" && result.transport === "native-push");
|
|
284
353
|
ok("A3: decider probed the native-push adapter once (decide side)", spies.nativePushProbe.length === 1);
|
|
285
354
|
ok("A3: executor sent via the native-push adapter once (execute side)", spies.nativePushSend.length === 1);
|
|
286
|
-
ok("A3: send used the DECIDER-probed route", spies.nativePushSend[0]?.
|
|
287
|
-
ok("A3: send carried the dispatch message", spies.nativePushSend[0]?.content
|
|
355
|
+
ok("A3: send used the DECIDER-probed route", spies.nativePushSend[0]?.route === "127.0.0.1:5599");
|
|
356
|
+
ok("A3: send carried the dispatch message", spies.nativePushSend[0]?.content.includes("hi agy") === true);
|
|
288
357
|
ok(
|
|
289
358
|
"A3: BOTH hands resolved the SAME adapter (for 'antigravity')",
|
|
290
359
|
spies.nativePushResolve.length === 2 && spies.nativePushResolve.every((b) => b === "antigravity"),
|
|
@@ -292,6 +361,145 @@ async function main(): Promise<void> {
|
|
|
292
361
|
ok("A3: native-push is LOCK-FREE (no acquire)", spies.acquire.length === 0);
|
|
293
362
|
ok("A3: native-push did NOT enqueue a mailbox (not the unsupported path)", spies.enqueue.length === 0);
|
|
294
363
|
}
|
|
364
|
+
{
|
|
365
|
+
const { deps, spies } = makeSpiedFactory({
|
|
366
|
+
backend: "codex",
|
|
367
|
+
nativePushProbe: {
|
|
368
|
+
status: "alive",
|
|
369
|
+
route: { backend: "codex", socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock" },
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi codex" }, deps);
|
|
373
|
+
ok(
|
|
374
|
+
"[QK:CODEX-NATIVE-PRODUCTION-WIRING] Codex ff → native-push delivered",
|
|
375
|
+
result.kind === "executed" && result.transport === "native-push",
|
|
376
|
+
);
|
|
377
|
+
ok(
|
|
378
|
+
"A3c: both hands resolve the Codex adapter",
|
|
379
|
+
spies.nativePushResolve.length === 2 && spies.nativePushResolve.every((b) => b === "codex"),
|
|
380
|
+
);
|
|
381
|
+
ok(
|
|
382
|
+
"A3c: planted Codex UDS reaches the send hand",
|
|
383
|
+
spies.nativePushSend[0]?.route.endsWith("/app-server-control.sock") === true,
|
|
384
|
+
);
|
|
385
|
+
ok(
|
|
386
|
+
"A3c: Codex native-push remains lock-free and mailbox-free",
|
|
387
|
+
spies.acquire.length === 0 && spies.enqueue.length === 0,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
// ── A3d (#95): the native-push rail injects the MAILBOX-SERIALIZED sender envelope ──
|
|
391
|
+
// Measured in the #95 A LIVE run: a nonce-only fresh-call callback direct-injected into a
|
|
392
|
+
// Codex thread arrived as bare text, so the receiving citizen could not name who called it
|
|
393
|
+
// and the callback was uncorrelatable. Direct injection has neither the control socket's RPC
|
|
394
|
+
// framing nor a mailbox file, so the envelope must ride IN the body — the same bytes
|
|
395
|
+
// `formatMetaMailboxBody` gives the mailbox rail. ONE rail: both native-push backends, no
|
|
396
|
+
// per-backend branch.
|
|
397
|
+
{
|
|
398
|
+
const route = { backend: "antigravity", lsAddress: "127.0.0.1:5599" } as const;
|
|
399
|
+
const { deps, spies } = makeSpiedFactory({
|
|
400
|
+
backend: "antigravity",
|
|
401
|
+
nativePushProbe: { status: "alive", route },
|
|
402
|
+
});
|
|
403
|
+
const { result, executeEvents } = await runInTwoPhases(
|
|
404
|
+
{ target: GID, intent: "fire-and-forget", message: "hi agy", wantsReply: true },
|
|
405
|
+
deps,
|
|
406
|
+
spies,
|
|
407
|
+
);
|
|
408
|
+
// WIRING FIRST, and it is its own claim: the factory must hand the native-push hand the
|
|
409
|
+
// SAME `opts.senderProvider` the mailbox hand gets. A render that is reachable but never
|
|
410
|
+
// fed falls back to raw silently, so "the provider was consulted exactly once on this
|
|
411
|
+
// rail" is asserted BEFORE the body is compared — that way a dropped wiring fails here
|
|
412
|
+
// at its own QK instead of borrowing the envelope claim's failure.
|
|
413
|
+
ok(
|
|
414
|
+
"[QK:NATIVE-PUSH-SENDER-PROVIDER-WIRING] the factory fed the native-push hand its one senderProvider, exactly once",
|
|
415
|
+
result.kind === "executed" && spies.senderCalls.length === 1,
|
|
416
|
+
);
|
|
417
|
+
ok(
|
|
418
|
+
"[QK:NATIVE-PUSH-SENDER-ENVELOPE] agy direct injection carries the mailbox-serialized trusted envelope",
|
|
419
|
+
spies.nativePushSend[0]?.content === formatMetaMailboxBody(SENDER, "hi agy", true),
|
|
420
|
+
);
|
|
421
|
+
ok(
|
|
422
|
+
"A3d: the enveloped body is NOT the raw plan message (an envelope was actually added)",
|
|
423
|
+
spies.nativePushSend[0]?.content !== "hi agy",
|
|
424
|
+
);
|
|
425
|
+
ok(
|
|
426
|
+
"A3d: wants_reply rides the envelope on this rail",
|
|
427
|
+
spies.nativePushSend[0]?.content.includes("wants reply: yes") === true,
|
|
428
|
+
);
|
|
429
|
+
// ORDER, not counts. The envelope must be resolved BEFORE the adapter is resolved or
|
|
430
|
+
// touched. Counts would be identical if the hand resolved the adapter first and the
|
|
431
|
+
// provider second — but then a provider throw would land after the adapter was already
|
|
432
|
+
// in hand, and the safety claim ("a failing envelope source injects nothing") would
|
|
433
|
+
// rest on luck rather than sequence.
|
|
434
|
+
ok(
|
|
435
|
+
"[QK:NATIVE-PUSH-SENDER-BEFORE-ADAPTER] the send hand resolves the envelope, THEN the adapter, THEN sends",
|
|
436
|
+
JSON.stringify(executeEvents) === JSON.stringify(["sender", "resolve", "send"]),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
{
|
|
440
|
+
// Same rail, other backend — codex renders identically. A backend branch would show here.
|
|
441
|
+
const route = {
|
|
442
|
+
backend: "codex",
|
|
443
|
+
socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock",
|
|
444
|
+
} as const;
|
|
445
|
+
const { deps, spies } = makeSpiedFactory({ backend: "codex", nativePushProbe: { status: "alive", route } });
|
|
446
|
+
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi codex" }, deps);
|
|
447
|
+
ok(
|
|
448
|
+
"A3d: codex direct injection carries the SAME mailbox-serialized envelope (one rail, no backend branch)",
|
|
449
|
+
result.kind === "executed" &&
|
|
450
|
+
spies.nativePushSend[0]?.content === formatMetaMailboxBody(SENDER, "hi codex", false),
|
|
451
|
+
);
|
|
452
|
+
ok("A3d: the codex body is NOT the raw plan message either", spies.nativePushSend[0]?.content !== "hi codex");
|
|
453
|
+
ok("A3d: codex resolved the envelope EXACTLY once", spies.senderCalls.length === 1);
|
|
454
|
+
}
|
|
455
|
+
{
|
|
456
|
+
// #50 C3 parity: no authoritative sender → the raw message is preserved, never a
|
|
457
|
+
// fabricated envelope. This is the same fallback the mailbox hand has.
|
|
458
|
+
const { deps, spies } = makeSpiedFactory({
|
|
459
|
+
backend: "codex",
|
|
460
|
+
noSender: true,
|
|
461
|
+
nativePushProbe: {
|
|
462
|
+
status: "alive",
|
|
463
|
+
route: { backend: "codex", socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock" },
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "raw only" }, deps);
|
|
467
|
+
ok(
|
|
468
|
+
"A3d: no authoritative sender → the RAW message is injected, no fabricated envelope",
|
|
469
|
+
result.kind === "executed" && spies.nativePushSend[0]?.content === "raw only",
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
{
|
|
473
|
+
// A throwing provider must reject the send with NOTHING injected — a half-delivered
|
|
474
|
+
// conversation is worse than a rejected dispatch. No new reject reason, no fallback.
|
|
475
|
+
const { deps, spies } = makeSpiedFactory({
|
|
476
|
+
backend: "codex",
|
|
477
|
+
senderThrows: true,
|
|
478
|
+
nativePushProbe: {
|
|
479
|
+
status: "alive",
|
|
480
|
+
route: { backend: "codex", socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock" },
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
const { result, executeEvents } = await runInTwoPhases(
|
|
484
|
+
{ target: GID, intent: "fire-and-forget", message: "never sent" },
|
|
485
|
+
deps,
|
|
486
|
+
spies,
|
|
487
|
+
);
|
|
488
|
+
ok(
|
|
489
|
+
"A3d: a throwing sender provider fails the dispatch EXACTLY as execution-failed",
|
|
490
|
+
result.kind === "execution-failed",
|
|
491
|
+
);
|
|
492
|
+
ok("A3d: a throwing sender provider called its provider exactly once", spies.senderCalls.length === 1);
|
|
493
|
+
ok("A3d: a throwing sender provider never resolved the adapter", executeEvents.includes("resolve") === false);
|
|
494
|
+
ok("A3d: a throwing sender provider injected NOTHING", spies.nativePushSend.length === 0);
|
|
495
|
+
// The whole execute phase is ONE event: the envelope source was asked, it failed, and the
|
|
496
|
+
// target's conversation was never reached. No partial delivery, no adapter in hand.
|
|
497
|
+
ok(
|
|
498
|
+
"A3d: the failed execute phase is exactly ['sender'] — nothing downstream ran",
|
|
499
|
+
JSON.stringify(executeEvents) === JSON.stringify(["sender"]),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
295
503
|
{
|
|
296
504
|
// antigravity + dead probe → reject native-push-target-dead, NO send.
|
|
297
505
|
const { deps, spies } = makeSpiedFactory({
|
|
@@ -339,6 +547,31 @@ async function main(): Promise<void> {
|
|
|
339
547
|
);
|
|
340
548
|
}
|
|
341
549
|
|
|
550
|
+
// ── B2: #115 — the production seam CARRIES the receiver's named in-band reason ─
|
|
551
|
+
// B above proves the success half of the map. This is the other half, and it is the
|
|
552
|
+
// half the field symptom lived in: a completed RPC answering `{success:false,
|
|
553
|
+
// error:"compacting"}`. The reason has to cross THREE production hops the fake cannot
|
|
554
|
+
// short-circuit — `response.error` → `RpcSendResult.error` (the factory's own
|
|
555
|
+
// `sendOverSocket` closure) → `driveSend`'s `inBandRejected` → the
|
|
556
|
+
// `executeControlSocketSend` result — so deleting the factory's `error:` wiring alone
|
|
557
|
+
// strands the reason and this cell goes red. `outcome` alone is NOT the assertion:
|
|
558
|
+
// a bare `rejected` is exactly what the defect rendered.
|
|
559
|
+
{
|
|
560
|
+
const { deps, spies } = makeSpiedFactory({ rpc: "in-band-reject" });
|
|
561
|
+
const res = await deps.executor.sendControl(CONTROL_PLAN, lockClaim());
|
|
562
|
+
ok("B2: in-band {success:false} → outcome 'rejected' (not failed, not sent)", res.outcome === "rejected");
|
|
563
|
+
ok(
|
|
564
|
+
"B2: [QK:V2PROD-INBAND-ERROR-WIRED] response.error reaches the send result verbatim",
|
|
565
|
+
res.rejectReason === IN_BAND_REJECT_REASON,
|
|
566
|
+
);
|
|
567
|
+
ok("B2: an in-band reject is a COMPLETED rpc — the socket hand ran once", spies.rpc.length === 1);
|
|
568
|
+
ok(
|
|
569
|
+
"B2: a rejected send still releases under the wired lockDir",
|
|
570
|
+
spies.release.length === 1 && spies.release[0].dir === LOCK_DIR,
|
|
571
|
+
);
|
|
572
|
+
ok("B2: an in-band reject never falls back to the mailbox", spies.enqueue.length === 0);
|
|
573
|
+
}
|
|
574
|
+
|
|
342
575
|
// ── C2: #50 C3 — the dormant rail carries the caller edge (<sender_info>) ──
|
|
343
576
|
// ── D: meta-mailbox hand enqueues onto the wired dirs ─────────────────────
|
|
344
577
|
{
|
|
@@ -101,7 +101,7 @@ const NATIVE_PUSH_PLAN: NativePushPlan = {
|
|
|
101
101
|
targetGardenId: GID,
|
|
102
102
|
backend: "antigravity",
|
|
103
103
|
nativeSessionId: "conv-xyz",
|
|
104
|
-
route: { lsAddress: "127.0.0.1:5599" },
|
|
104
|
+
route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" },
|
|
105
105
|
wantsReply: false,
|
|
106
106
|
message: "m",
|
|
107
107
|
};
|