@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.
Files changed (111) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -3
  3. package/CHANGELOG.md +473 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +332 -60
  6. package/README.md +100 -22
  7. package/VERIFY.md +93 -7
  8. package/docs/acp-backend-rail.md +0 -1
  9. package/docs/external-mcp-host.md +64 -33
  10. package/docs/setup-clean-host.md +151 -17
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  29. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  30. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  31. package/mcp/entwurf-bridge/src/index.ts +154 -116
  32. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  33. package/package.json +13 -4
  34. package/pi-extensions/entwurf-control.ts +71 -19
  35. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  36. package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
  37. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  38. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  39. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  41. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  42. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  43. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  44. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  46. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  47. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  48. package/pi-extensions/lib/mux-fresh-call.ts +233 -29
  49. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  50. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  51. package/pi-extensions/lib/native-push/register.ts +7 -9
  52. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  53. package/run.sh +342 -28
  54. package/scripts/check-agy-sender-identity.ts +1 -1
  55. package/scripts/check-codex-app-server-launch.ts +445 -0
  56. package/scripts/check-codex-birth-hook.ts +264 -0
  57. package/scripts/check-codex-bridge-identity.ts +179 -0
  58. package/scripts/check-codex-native-push.ts +386 -0
  59. package/scripts/check-codex-sender-identity.ts +495 -0
  60. package/scripts/check-compaction-send-guard.ts +130 -0
  61. package/scripts/check-copilot-receive-arm.ts +4 -1
  62. package/scripts/check-entwurf-fact-provider.ts +38 -0
  63. package/scripts/check-entwurf-peers-surface.ts +13 -1
  64. package/scripts/check-entwurf-self-address.ts +15 -16
  65. package/scripts/check-entwurf-v2-contract.ts +4 -3
  66. package/scripts/check-entwurf-v2-decider.ts +7 -5
  67. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  68. package/scripts/check-entwurf-v2-production.ts +245 -12
  69. package/scripts/check-entwurf-v2-runner.ts +1 -1
  70. package/scripts/check-entwurf-v2-send.ts +26 -7
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +8 -3
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-mux-launch-tmux.ts +345 -4
  75. package/scripts/check-native-push-adapter.ts +20 -16
  76. package/scripts/check-native-push-register.ts +5 -1
  77. package/scripts/check-release-gate-outcomes.ts +47 -1
  78. package/scripts/check-setup-qualification.sh +3 -1
  79. package/scripts/codex-app-server-launch.sh +275 -0
  80. package/scripts/codex-birth-doctor.sh +276 -0
  81. package/scripts/codex-birth-install.sh +414 -0
  82. package/scripts/codex-birth-uninstall.sh +170 -0
  83. package/scripts/codex-mcp-config.py +435 -0
  84. package/scripts/codex-socket-path.ts +33 -0
  85. package/scripts/codex-statusline-config.py +434 -0
  86. package/scripts/codex-terminal-title-config.py +500 -0
  87. package/scripts/codex_toml_io.py +653 -0
  88. package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
  89. package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
  90. package/scripts/lib/launch-receipt-windows.ts +46 -0
  91. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  92. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  93. package/scripts/mutants/codex-app-server-launch.json +157 -0
  94. package/scripts/mutants/codex-caller-seat.json +336 -0
  95. package/scripts/mutants/codex-native.json +838 -0
  96. package/scripts/mutants/compaction-send-guard.json +103 -0
  97. package/scripts/mutants/entwurf-peers.json +19 -0
  98. package/scripts/mutants/mux-fresh-call.json +93 -10
  99. package/scripts/mutants/omp-fresh.json +6 -4
  100. package/scripts/mutants/release-gate.json +13 -0
  101. package/scripts/mutants/v2-surface.json +75 -1
  102. package/scripts/raw-async-delivery/README.md +2 -1
  103. package/scripts/raw-codex-measure/README.md +114 -46
  104. package/scripts/smoke-agy-native-push-live.ts +3 -1
  105. package/scripts/smoke-codex-birth.sh +347 -0
  106. package/scripts/smoke-codex-config-state.sh +700 -0
  107. package/scripts/smoke-codex-fresh-live.ts +1426 -0
  108. package/scripts/smoke-codex-native-push-live.ts +75 -0
  109. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  110. package/scripts/smoke-setup-verdict.sh +125 -10
  111. 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("buildTrustedMetaSenderEnvelope");
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
- "buildTrustedMetaSenderEnvelope does not fall back every non-native-push backend to self-fetch [QK:SELFADDR-NO-FALLBACK-SELF-FETCH]",
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
- "buildTrustedMetaSenderEnvelope derives self-fetch through resolveMailboxWakeModeCapability(identity), not a backend-name list [QK:SELFADDR-MAILBOX-WAKE-SEAM]",
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 somewhere else in the file
447
- // cannot make either row vacuously green.
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
- `${cls} names BOTH native hooks (SessionStart AND PreInvocation)`,
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("buildTrustedMetaSenderEnvelope");
469
- ok("buildTrustedMetaSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(metaBody));
470
- ok("buildTrustedMetaSenderEnvelope no longer hardcodes `replyable: true`", !/replyable:\s*true/.test(metaBody));
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
- "buildTrustedMetaSenderEnvelope derives active-receiver from the SHARED receiver composition (both markers)",
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
- "buildTrustedMetaSenderEnvelope no longer copies one match into both receiver facts",
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
- "buildTrustedMetaSenderEnvelope reads neither marker with the owner guard disabled",
489
+ "buildMetaSenderEnvelope reads neither marker with the owner guard disabled",
491
490
  !/verifyOwner:\s*false/.test(metaBody),
492
491
  );
493
492
  ok(
494
- "buildTrustedMetaSenderEnvelope keeps meta identity + derived replyable (inactive → not null)",
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 is native-push; pi/claude-code are NOT; the domain is DISJOINT
284
- // from the pi socket liveness domain (a backend is never in both rails).
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: NATIVE_PUSH_BACKENDS == ['antigravity']", [...NATIVE_PUSH_BACKENDS], ["antigravity"]);
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("codex"), preProbeAddressConflict: false },
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. (Indistinguishable here from the codex case at the
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("native-push ff+alive: plan route = probed route", d.plan.route.lsAddress === "127.0.0.1:5599");
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.lsAddress === "127.0.0.1:5600");
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.lsAddress === "127.0.0.1:5600");
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.lsAddress === "127.0.0.1:5601",
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.lsAddress === "127.0.0.1:5600",
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 { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
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: { lsAddress: string; conv: string; content: string }[];
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
- rpc?: "success" | "dead-throw";
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
- over.noSender
173
- ? undefined
174
- : { sessionId: "self", agentId: "pi/x", cwd: "/cwd", timestamp: "2026-06-13T00:00:00.000Z" },
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: "antigravity",
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
- spies.nativePushSend.push({ lsAddress: route.lsAddress, conv, content });
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]?.lsAddress === "127.0.0.1:5599");
287
- ok("A3: send carried the dispatch message", spies.nativePushSend[0]?.content === "hi agy");
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
  };