@junghanacs/entwurf 0.20.1 → 0.21.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 (96) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +4 -3
  3. package/CHANGELOG.md +195 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +202 -60
  6. package/README.md +49 -22
  7. package/VERIFY.md +37 -7
  8. package/docs/acp-backend-rail.md +0 -1
  9. package/docs/external-mcp-host.md +54 -33
  10. package/docs/setup-clean-host.md +110 -17
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +101 -109
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +326 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +50 -15
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  25. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  26. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  27. package/mcp/entwurf-bridge/src/index.ts +115 -111
  28. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  29. package/package.json +13 -4
  30. package/pi-extensions/entwurf-control.ts +71 -19
  31. package/pi-extensions/lib/codex-fresh-preflight.ts +363 -0
  32. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  33. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  34. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  35. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  36. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  37. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  38. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  39. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  40. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  41. package/pi-extensions/lib/mux-fresh-call.ts +64 -19
  42. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  43. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  44. package/pi-extensions/lib/native-push/register.ts +7 -9
  45. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  46. package/run.sh +218 -28
  47. package/scripts/check-agy-sender-identity.ts +1 -1
  48. package/scripts/check-codex-birth-hook.ts +264 -0
  49. package/scripts/check-codex-bridge-identity.ts +179 -0
  50. package/scripts/check-codex-native-push.ts +386 -0
  51. package/scripts/check-codex-sender-identity.ts +495 -0
  52. package/scripts/check-compaction-send-guard.ts +130 -0
  53. package/scripts/check-copilot-receive-arm.ts +4 -1
  54. package/scripts/check-entwurf-fact-provider.ts +38 -0
  55. package/scripts/check-entwurf-peers-surface.ts +13 -1
  56. package/scripts/check-entwurf-self-address.ts +15 -16
  57. package/scripts/check-entwurf-v2-contract.ts +4 -3
  58. package/scripts/check-entwurf-v2-decider.ts +7 -5
  59. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  60. package/scripts/check-entwurf-v2-production.ts +203 -11
  61. package/scripts/check-entwurf-v2-runner.ts +1 -1
  62. package/scripts/check-entwurf-v2-surface.ts +1 -1
  63. package/scripts/check-gate-qualification.ts +6 -3
  64. package/scripts/check-harness-admission-parity.ts +0 -1
  65. package/scripts/check-mux-launch-tmux.ts +47 -2
  66. package/scripts/check-native-push-adapter.ts +20 -16
  67. package/scripts/check-native-push-register.ts +5 -1
  68. package/scripts/check-release-gate-outcomes.ts +47 -1
  69. package/scripts/check-setup-qualification.sh +3 -1
  70. package/scripts/codex-birth-doctor.sh +276 -0
  71. package/scripts/codex-birth-install.sh +414 -0
  72. package/scripts/codex-birth-uninstall.sh +170 -0
  73. package/scripts/codex-mcp-config.py +435 -0
  74. package/scripts/codex-statusline-config.py +434 -0
  75. package/scripts/codex_toml_io.py +532 -0
  76. package/scripts/lib/codex-fresh-live-protocol.ts +113 -0
  77. package/scripts/lib/codex-fresh-source-receipts.ts +399 -0
  78. package/scripts/lib/launch-receipt-windows.ts +46 -0
  79. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  80. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  81. package/scripts/mutants/codex-native.json +838 -0
  82. package/scripts/mutants/compaction-send-guard.json +103 -0
  83. package/scripts/mutants/entwurf-peers.json +19 -0
  84. package/scripts/mutants/mux-fresh-call.json +19 -8
  85. package/scripts/mutants/omp-fresh.json +6 -4
  86. package/scripts/mutants/release-gate.json +13 -0
  87. package/scripts/mutants/v2-surface.json +53 -1
  88. package/scripts/raw-async-delivery/README.md +2 -1
  89. package/scripts/raw-codex-measure/README.md +114 -46
  90. package/scripts/smoke-agy-native-push-live.ts +3 -1
  91. package/scripts/smoke-codex-birth.sh +347 -0
  92. package/scripts/smoke-codex-config-state.sh +511 -0
  93. package/scripts/smoke-codex-fresh-live.ts +1186 -0
  94. package/scripts/smoke-codex-native-push-live.ts +75 -0
  95. package/scripts/smoke-setup-verdict.sh +123 -10
  96. package/scripts/tsconfig.json +1 -0
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * mux-fresh-call — open ONE visible sibling in the caller's own tmux server (its own session by
3
3
  * default, or one named existing session on that server since #105), hand it its first task in
4
- * the launch argv, and let it name itself back to the caller.
4
+ * the launch argv, and let it name itself back to the caller. Codex is the one measured topology
5
+ * exception: an omitted seat selects the existing `codex` home session (#95), never a TUI guess.
5
6
  *
6
7
  * ── Why this is a third module and not a parameter on the leaf ──
7
8
  *
@@ -78,6 +79,7 @@
78
79
 
79
80
  import { randomBytes } from "node:crypto";
80
81
  import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
82
+ import { CODEX_PREFLIGHT_HINT, type CodexPreflightRejectReason } from "./codex-fresh-preflight.ts";
81
83
  import {
82
84
  COPILOT_PREFLIGHT_HINT,
83
85
  type CopilotPreflightRejectReason,
@@ -100,15 +102,16 @@ import {
100
102
  runTmux,
101
103
  type WindowHandle,
102
104
  } from "./mux-placement.ts";
105
+ import { resolveCodexDefaultSocketPath } from "./native-push/codex-ws-client.ts";
103
106
  import { OMP_PREFLIGHT_HINT, type OmpPreflightRejectReason, ompFreshPreflight } from "./omp-fresh-preflight.ts";
104
107
  import { classifyTmuxSessionName, resolveTmuxSessionId, type TmuxSessionRejectReason } from "./resolve-tmux-session.ts";
105
108
 
106
109
  /** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
107
- * not a config entry. `copilot` was added by #82 RAIL 9 under the step 9 admission contract, and
108
- * `omp` by #87 Bundle C under the same one. The set is joined to the citizen backends by
109
- * `check-harness-admission-parity`: a harness that mints records but is missing HERE is not an
110
- * unwired convenience, it is a release blocker. */
111
- export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp"] as const;
110
+ * not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, and `codex`
111
+ * by #95 after its system birth and app-server rails were measured. The set is joined to the
112
+ * citizen backends by `check-harness-admission-parity`: a harness that mints records but is
113
+ * missing HERE is not an unwired convenience, it is a release blocker. */
114
+ export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex"] as const;
112
115
  export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
113
116
 
114
117
  /**
@@ -138,6 +141,7 @@ export const FRESH_CALL_RUNTIME: Record<FreshCallBackend, string> = {
138
141
  "claude-code": "claude",
139
142
  copilot: "entwurf",
140
143
  omp: "omp",
144
+ codex: "codex",
141
145
  };
142
146
 
143
147
  /**
@@ -166,6 +170,7 @@ export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
166
170
  "claude-code": "mcp__entwurf-bridge__entwurf_v2",
167
171
  copilot: "entwurf-bridge-entwurf_v2",
168
172
  omp: "mcp__entwurf_bridge_entwurf_v",
173
+ codex: "mcp__entwurf_bridge__entwurf_v2",
169
174
  };
170
175
 
171
176
  /** Mirrors the `entwurf_v2` message bound. This is an INTERFACE cap for symmetry with the
@@ -276,6 +281,7 @@ export function buildBackendArgs(
276
281
  backend: FreshCallBackend,
277
282
  composition: FreshCallComposition,
278
283
  model: string,
284
+ env: NodeJS.ProcessEnv = process.env,
279
285
  ): string[] {
280
286
  switch (backend) {
281
287
  case "pi":
@@ -286,11 +292,20 @@ export function buildBackendArgs(
286
292
  return ["copilot", "--interactive", composition.prompt, "--model", model, "--yolo"];
287
293
  case "omp":
288
294
  return [`--${OMP_BOOTSTRAP_FLAG}`, composition.bootstrapPayload, "--model", model, "--approval-mode", "yolo"];
295
+ case "codex":
296
+ return [
297
+ "--remote",
298
+ `unix://${resolveCodexDefaultSocketPath(env)}`,
299
+ "--model",
300
+ model,
301
+ "--dangerously-bypass-approvals-and-sandbox",
302
+ composition.prompt,
303
+ ];
289
304
  }
290
305
  }
291
306
 
292
307
  /**
293
- * What a launch has to say, in the two shapes the four backends need. Three of them are
308
+ * What a launch has to say, in the two shapes the five backends need. Four of them are
294
309
  * handed a first-turn PROMPT; omp is handed a bootstrap PAYLOAD its own installed extension
295
310
  * unpacks. Both are always built, because building one is cheap and a backend switch must
296
311
  * never be able to reach a field that was not composed.
@@ -375,6 +390,7 @@ export type FreshCallRejectReason =
375
390
  | TmuxSessionRejectReason
376
391
  | CopilotPreflightRejectReason
377
392
  | OmpPreflightRejectReason
393
+ | CodexPreflightRejectReason
378
394
  | "caller-identity-unavailable"
379
395
  | "model-empty"
380
396
  | "model-invalid"
@@ -389,6 +405,28 @@ export interface FreshCallPlacement {
389
405
  tmuxSession: string;
390
406
  }
391
407
 
408
+ /**
409
+ * Codex's explicit home topology (#95): the operator provides this one existing tmux session
410
+ * and seats the operator-owned app-server plus supported Codex TUIs there. This is a placement
411
+ * convention, not an address axis — `threadId` remains the native delivery address.
412
+ */
413
+ export const CODEX_HOME_TMUX_SESSION = "codex" as const;
414
+ export type FreshCallSeatSource = "requested" | "codex-home";
415
+ export interface FreshCallSeat {
416
+ tmuxSession: string;
417
+ source: FreshCallSeatSource;
418
+ }
419
+
420
+ /** An explicit seat always wins. Only omitted Codex placement gets the fixed home. */
421
+ export function selectFreshCallSeat(
422
+ backend: FreshCallBackend,
423
+ placement: FreshCallPlacement | undefined,
424
+ ): FreshCallSeat | null {
425
+ if (placement !== undefined) return { tmuxSession: placement.tmuxSession, source: "requested" };
426
+ if (backend === "codex") return { tmuxSession: CODEX_HOME_TMUX_SESSION, source: "codex-home" };
427
+ return null;
428
+ }
429
+
392
430
  /** Coordinates plus what was handed to tmux. Read `runtimePath` as "what we asked to start".
393
431
  * There is deliberately NO field here for the callback, the nonce's arrival, or the sibling's
394
432
  * garden id — see the module header. */
@@ -399,10 +437,11 @@ export interface FreshCallReceipt extends WindowHandle {
399
437
  * of fact as `runtimePath`: what tmux was asked for, never an observation of where the pane
400
438
  * landed. */
401
439
  cwd?: string;
402
- /** The REQUESTED session name — present only when the caller named a seat. The RESOLVED
403
- * target is the inherited `sessionId`, which is the session the window is actually in; this
404
- * field is the request that produced it, exactly as `cwd` is. */
440
+ /** The selected session name — either caller-requested or Codex's fixed home. The RESOLVED
441
+ * target is the inherited `sessionId`, which is the session the window is actually in. */
405
442
  tmuxSession?: string;
443
+ /** Why `tmuxSession` was selected. Absent exactly when no named seat was used. */
444
+ tmuxSessionSource?: FreshCallSeatSource;
406
445
  runtimePath: string;
407
446
  nonce: string;
408
447
  }
@@ -435,9 +474,9 @@ function defaultRandomHex(): string {
435
474
  * for. That is sufficient and not a compromise: every reader of the carrier trims and tests
436
475
  * truthiness (`index.ts:212-217`), so empty and absent are the same answer by construction.
437
476
  *
438
- * It is applied to all four backends because the leak is a property of tmux, not of a vendor. A
477
+ * It is applied to all five backends because the leak is a property of tmux, not of a vendor. A
439
478
  * scrub only on the backend whose measurement surfaced it would encode the claim that the other
440
- * three are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
479
+ * four are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
441
480
  * authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
442
481
  * exactly that after this argv has run. This is a fixed two-variable seam and deliberately NOT a
443
482
  * general env carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping
@@ -526,8 +565,10 @@ export function freshCall(
526
565
  // without tmux, so an unresolvable name is answered before anything else runs. Whether that
527
566
  // session EXISTS is a tmux question and is asked below, after the caller's own context is
528
567
  // proven — a name check that needed a live server would refuse for the wrong reason on a
529
- // host with no tmux at all.
530
- const seat = params.placement?.tmuxSession;
568
+ // host with no tmux at all. Codex alone selects its fixed home when the caller omitted a seat;
569
+ // an explicit seat remains an expert override and is reported as such.
570
+ const selectedSeat = selectFreshCallSeat(params.backend, params.placement);
571
+ const seat = selectedSeat?.tmuxSession;
531
572
  if (seat !== undefined) {
532
573
  const badSeat = classifyTmuxSessionName(seat);
533
574
  if (badSeat) return { ok: false, reason: badSeat };
@@ -555,7 +596,6 @@ export function freshCall(
555
596
  const missing = ompFreshPreflight(env);
556
597
  if (missing) return { ok: false, reason: missing };
557
598
  }
558
-
559
599
  const inspected = inspectPlacement(env);
560
600
  if (!inspected.ok) return { ok: false, reason: inspected.reason };
561
601
  const placement = inspected.placement;
@@ -582,7 +622,7 @@ export function freshCall(
582
622
  bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId: params.callerGardenId, nonce, task }),
583
623
  };
584
624
  const run = runTmux(
585
- buildFreshCallArgs(targetSessionId, runtimePath, buildBackendArgs(params.backend, composition, model), cwd),
625
+ buildFreshCallArgs(targetSessionId, runtimePath, buildBackendArgs(params.backend, composition, model, env), cwd),
586
626
  env,
587
627
  );
588
628
  assertTmuxOk("new-window", run);
@@ -610,7 +650,9 @@ export function freshCall(
610
650
  backend: params.backend,
611
651
  model,
612
652
  ...(cwd === undefined ? {} : { cwd }),
613
- ...(seat === undefined ? {} : { tmuxSession: seat }),
653
+ ...(selectedSeat === null
654
+ ? {}
655
+ : { tmuxSession: selectedSeat.tmuxSession, tmuxSessionSource: selectedSeat.source }),
614
656
  runtimePath,
615
657
  nonce,
616
658
  },
@@ -624,6 +666,7 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
624
666
  // the sentence an operator reads cannot drift away from the predicate that produced it.
625
667
  ...COPILOT_PREFLIGHT_HINT,
626
668
  ...OMP_PREFLIGHT_HINT,
669
+ ...CODEX_PREFLIGHT_HINT,
627
670
  "no-tmux-context": "this agent is not running inside tmux, so there is no session to open a sibling beside",
628
671
  "anchor-malformed": "TMUX_PANE is not a native pane id",
629
672
  "anchor-unresolved": "tmux resolved no pane for this agent's anchor",
@@ -640,7 +683,7 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
640
683
  "tmux-session-name-invalid":
641
684
  "the requested tmux session name is outside the shape this rail addresses (start with a letter or digit, then letters, digits, '_' or '-') — some other shapes tmux cannot resolve at all ('#' is expanded when the name is stored; '.' and ':' are its own pane/window separators inside a target; a name like '$0' loses to the session id '$0'), and the rest are declined to keep one narrow grammar, so rename the session or open one whose name fits",
642
685
  "tmux-session-missing":
643
- "no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again",
686
+ "no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again; Codex's omitted-placement home is the exact session name 'codex'",
644
687
  "model-empty": "model is empty after trimming; fresh calls require an explicit model",
645
688
  "model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
646
689
  "task-empty": "task is empty after trimming",
@@ -678,7 +721,9 @@ export function renderFreshCall(result: FreshCallResult): { text: string; isErro
678
721
  (r.cwd === undefined ? "" : ` cwd: ${r.cwd} (requested start directory — not an observation)\n`) +
679
722
  (r.tmuxSession === undefined
680
723
  ? ""
681
- : ` seat: ${r.tmuxSession} (requested tmux session, resolved to ${r.sessionId})\n`) +
724
+ : r.tmuxSessionSource === "codex-home"
725
+ ? ` seat: ${r.tmuxSession} (Codex home tmux session, resolved to ${r.sessionId})\n`
726
+ : ` seat: ${r.tmuxSession} (requested tmux session, resolved to ${r.sessionId})\n`) +
682
727
  ` window: ${r.windowId} (index ${r.windowIndex}) in session ${r.sessionId}\n` +
683
728
  ` pane: ${r.paneId} pid ${r.panePid}\n` +
684
729
  ` nonce: ${r.nonce}\n` +
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * native-push adapter rail — the transport LEAF by which a native-push backend
3
- * (antigravity, the first) is (1) probed for a LIVE conversation and (2) direct-injected
4
- * with a message. Mirrors the ACP backend-adapter rail (acp/backend-adapter.ts §ADAPTERS
3
+ * is (1) probed for a LIVE native target and (2) direct-injected with a message.
4
+ * Mirrors the ACP backend-adapter rail (acp/backend-adapter.ts §ADAPTERS
5
5
  * /resolveAcpBackendAdapter): one interface, one registry, a fail-fast resolver.
6
6
  *
7
7
  * Purity contract (봉인 3):
@@ -26,6 +26,7 @@ import * as os from "node:os";
26
26
  import * as path from "node:path";
27
27
 
28
28
  import type { NativePushBackend } from "../entwurf-v2-contract.ts";
29
+ import { codexNativePushAdapter } from "./codex-ws-client.ts";
29
30
 
30
31
  // ── runner seam (injectable process exec) ───────────────────────────────────
31
32
 
@@ -85,14 +86,13 @@ export const realNativePushRunner: NativePushRunner = {
85
86
  // ── route + probe result ────────────────────────────────────────────────────
86
87
 
87
88
  /**
88
- * A VOLATILE native-push route the live LS address (`127.0.0.1:PORT`) a probe found
89
- * serving the conversation. MUST NOT be stored (봉인 3): it is re-derived every dispatch
90
- * by a fresh probe (the LS port is per-process and shifts). Carried only within a single
91
- * `send` call, handed straight from a fresh `probe`.
89
+ * A VOLATILE native-push route. Antigravity carries its live LS address; Codex carries
90
+ * the one app-server UDS that proved the target thread loaded. Routes are never stored:
91
+ * every dispatch derives one from a fresh probe and hands it straight to `send`.
92
92
  */
93
- export interface NativePushRoute {
94
- readonly lsAddress: string;
95
- }
93
+ export type NativePushRoute =
94
+ | { readonly backend: "antigravity"; readonly lsAddress: string }
95
+ | { readonly backend: "codex"; readonly socketPath: string };
96
96
 
97
97
  /**
98
98
  * A probe outcome. `alive` carries the volatile route; `dead`/`indeterminate` carry a
@@ -108,17 +108,12 @@ export type NativePushProbeResult =
108
108
  export interface NativePushAdapter {
109
109
  /** Backend discriminator (a member of NATIVE_PUSH_BACKENDS). */
110
110
  readonly id: NativePushBackend;
111
- /**
112
- * Full-scan probe: find a LIVE route serving `nativeSessionId`, else report
113
- * dead/indeterminate. Scans EVERY host pid (never `head -1`) and re-discovers the
114
- * route on every call (no cache — volatile-route discipline, 봉인 3).
115
- */
111
+ /** Whether the executor may re-probe and retry once after an ambiguous send failure.
112
+ * Codex is false: `codex queue` may accept before its receipt fails, so retry can duplicate. */
113
+ readonly retriable: boolean;
114
+ /** Probe the backend's current native route for `nativeSessionId`. */
116
115
  probe(nativeSessionId: string): Promise<NativePushProbeResult>;
117
- /**
118
- * Direct-inject `content` into the conversation over `route`. Single attempt: throws
119
- * on failure (fail-loud). Does NOT probe and does NOT retry — the executor hand owns
120
- * re-probe/retry (봉인 3), so the adapter can never silently paper over a dead route.
121
- */
116
+ /** Direct-inject once through the just-probed route. Throws on failure. */
122
117
  send(route: NativePushRoute, nativeSessionId: string, content: string): Promise<void>;
123
118
  }
124
119
 
@@ -211,6 +206,7 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
211
206
 
212
207
  return {
213
208
  id: "antigravity",
209
+ retriable: true,
214
210
 
215
211
  async probe(nativeSessionId) {
216
212
  let pids: number[];
@@ -237,7 +233,7 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
237
233
  for (const pid of pids) {
238
234
  for (const lsAddress of portsByPid.get(pid) ?? []) {
239
235
  if (await servesConversation(lsAddress, nativeSessionId)) {
240
- return { status: "alive", route: { lsAddress } };
236
+ return { status: "alive", route: { backend: "antigravity", lsAddress } };
241
237
  }
242
238
  }
243
239
  }
@@ -251,12 +247,13 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
251
247
  },
252
248
 
253
249
  async send(route, nativeSessionId, content) {
250
+ if (route.backend !== "antigravity") {
251
+ throw new Error(`antigravity adapter received ${route.backend} route`);
252
+ }
254
253
  const r = await runner.exec([binary, "agentapi", "send-message", nativeSessionId, content], {
255
254
  env: { ANTIGRAVITY_LS_ADDRESS: route.lsAddress },
256
255
  timeoutMs: AGY_SEND_TIMEOUT_MS,
257
256
  });
258
- // A non-zero code — including a timeout kill (124) on a stalled route — THROWS
259
- // (fail-loud); the executor hand owns the 1-shot re-probe→re-send on that throw.
260
257
  if (r.code !== 0) {
261
258
  throw new Error(
262
259
  `native-push send failed (agentapi send-message exit ${r.code}) via ${route.lsAddress}: ${
@@ -268,12 +265,12 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
268
265
  };
269
266
  }
270
267
 
271
- /** The production antigravity adapter (real runner + env-resolved binary). */
268
+ /** Production adapters. */
272
269
  export const antigravityAdapter: NativePushAdapter = createAntigravityAdapter({ runner: realNativePushRunner });
273
270
 
274
271
  // ── registry + fail-fast resolver (mirror resolveAcpBackendAdapter) ──────────
275
272
 
276
- const ADAPTERS: readonly NativePushAdapter[] = [antigravityAdapter];
273
+ const ADAPTERS: readonly NativePushAdapter[] = [antigravityAdapter, codexNativePushAdapter];
277
274
 
278
275
  /**
279
276
  * Resolve the native-push adapter that owns backend `id`. Fail-fast, like