@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
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. ack success → outcome `sent`, release ×1, deadFallback NOT called.
|
|
7
7
|
* 2. in-band reject (success:false) → outcome `rejected`, release ×1, NO fallback
|
|
8
|
-
* (deadFallback + mailbox NOT called — the receiver was reached and refused);
|
|
9
|
-
*
|
|
8
|
+
* (deadFallback + mailbox NOT called — the receiver was reached and refused); a
|
|
9
|
+
* supplied receiver error is carried verbatim as `rejectReason` (N3 boundary).
|
|
10
10
|
* 3. dead → re-resolve(control-socket) → success → `fallback-sent`, release ×1,
|
|
11
11
|
* deadFallback called EXACTLY once and UNDER the still-held lock (before release).
|
|
12
12
|
* 4. dead → re-resolve reject → `rejected`, release ×1, and the resolver's reason is
|
|
@@ -193,16 +193,24 @@ async function main(): Promise<void> {
|
|
|
193
193
|
|
|
194
194
|
// ── 2: in-band reject → rejected, release once, NO fallback ───────────────
|
|
195
195
|
{
|
|
196
|
-
const { result, trace } = await run({ firstSend: { result: { success: false, error: "
|
|
196
|
+
const { result, trace } = await run({ firstSend: { result: { success: false, error: "compacting" } } });
|
|
197
197
|
ok("in-band reject → rejected", result.outcome === "rejected");
|
|
198
198
|
ok("in-band reject → release ×1", trace.releases.length === 1);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
ok(
|
|
200
|
+
"[QK:V2SEND-INBAND-REJECT-REASON] in-band control reject carries its receiver error",
|
|
201
|
+
result.rejectReason === "compacting",
|
|
202
|
+
);
|
|
202
203
|
ok(
|
|
203
204
|
"in-band reject → no deadFallback, no mailbox",
|
|
204
205
|
trace.deadFallbackCalls === 0 && trace.mailboxSends.length === 0,
|
|
205
206
|
);
|
|
207
|
+
const unnamed = await run({ firstSend: { result: { success: false } } });
|
|
208
|
+
ok("in-band reject without an error does not invent rejectReason", unnamed.result.rejectReason === undefined);
|
|
209
|
+
const empty = await run({ firstSend: { result: { success: false, error: "" } } });
|
|
210
|
+
ok(
|
|
211
|
+
"in-band reject with an empty error does not expose an empty rejectReason",
|
|
212
|
+
empty.result.rejectReason === undefined,
|
|
213
|
+
);
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
// ── 3: dead → re-resolve(control) success → fallback-sent, fallback before release
|
|
@@ -223,6 +231,16 @@ async function main(): Promise<void> {
|
|
|
223
231
|
"dead → deadFallback UNDER held lock (before release)",
|
|
224
232
|
trace.order.indexOf("deadFallback") < trace.order.indexOf("releaseLock"),
|
|
225
233
|
);
|
|
234
|
+
|
|
235
|
+
const refused = await run({
|
|
236
|
+
firstSend: { throwCode: "ECONNREFUSED" },
|
|
237
|
+
deadFallback: { kind: "execute", plan: RERESOLVED_CONTROL_PLAN },
|
|
238
|
+
fallbackSend: { result: { success: false, error: "retry-refused" } },
|
|
239
|
+
});
|
|
240
|
+
ok(
|
|
241
|
+
"dead → re-resolve(control) in-band reject carries the retry receiver error",
|
|
242
|
+
refused.result.outcome === "rejected" && refused.result.rejectReason === "retry-refused",
|
|
243
|
+
);
|
|
226
244
|
}
|
|
227
245
|
|
|
228
246
|
// ── 4: dead → re-resolve reject → rejected ────────────────────────────────
|
|
@@ -296,9 +314,10 @@ async function main(): Promise<void> {
|
|
|
296
314
|
const refused = await run({
|
|
297
315
|
firstSend: { throwCode: "ENOENT" },
|
|
298
316
|
deadFallback: { kind: "execute", plan: MAILBOX_PLAN },
|
|
299
|
-
fallbackSend: { result: { success: false } },
|
|
317
|
+
fallbackSend: { result: { success: false, error: "mailbox-refused" } },
|
|
300
318
|
});
|
|
301
319
|
ok("dead → mailbox enqueue success:false → rejected", refused.result.outcome === "rejected");
|
|
320
|
+
ok("dead → mailbox enqueue reject carries its receiver error", refused.result.rejectReason === "mailbox-refused");
|
|
302
321
|
// No file was written, so there is nothing to name — never echo a dep's stray path.
|
|
303
322
|
ok("dead → rejected enqueue carries NO messagePath", refused.result.messagePath === undefined);
|
|
304
323
|
}
|
|
@@ -729,7 +729,7 @@ async function main(): Promise<void> {
|
|
|
729
729
|
v2Start !== -1 && after > v2Start,
|
|
730
730
|
);
|
|
731
731
|
const v2Block = src.slice(v2Start, after);
|
|
732
|
-
ok("5: MCP — v2 handler builds buildSendSenderEnvelope()", /buildSendSenderEnvelope\(
|
|
732
|
+
ok("5: MCP — v2 handler builds buildSendSenderEnvelope(context)", /buildSendSenderEnvelope\s*\(/.test(v2Block));
|
|
733
733
|
ok("5: MCP — v2 handler passes senderProvider: () => sender", /senderProvider:\s*\(\)\s*=>\s*sender/.test(v2Block));
|
|
734
734
|
ok(
|
|
735
735
|
"5: MCP — v2 handler calls runAndRenderEntwurfV2FromSurface",
|
|
@@ -832,9 +832,14 @@ let manifestCount: number;
|
|
|
832
832
|
"bridge-boot-resume": 3,
|
|
833
833
|
"bridge-command-boot": 9,
|
|
834
834
|
"capability-cache": 3,
|
|
835
|
+
"codex-app-server-launch": 9,
|
|
836
|
+
"codex-caller-seat": 28,
|
|
837
|
+
"codex-native": 68,
|
|
838
|
+
"compaction-send-guard": 7,
|
|
835
839
|
"copilot-birth": 19,
|
|
836
840
|
"copilot-launch": 14,
|
|
837
841
|
"copilot-receive": 20,
|
|
842
|
+
"entwurf-peers": 1,
|
|
838
843
|
"fresh-cut": 3,
|
|
839
844
|
"gate-qualification": 2,
|
|
840
845
|
"meta-facts": 4,
|
|
@@ -842,7 +847,7 @@ let manifestCount: number;
|
|
|
842
847
|
"meta-identity": 4,
|
|
843
848
|
"meta-retire": 3,
|
|
844
849
|
"mux-boundary": 16,
|
|
845
|
-
"mux-fresh-call":
|
|
850
|
+
"mux-fresh-call": 51,
|
|
846
851
|
"mux-launcher-fence": 7,
|
|
847
852
|
"mux-parent-artifact": 3,
|
|
848
853
|
"pack-install": 2,
|
|
@@ -852,13 +857,13 @@ let manifestCount: number;
|
|
|
852
857
|
"omp-fresh": 24,
|
|
853
858
|
"omp-receive": 11,
|
|
854
859
|
"probe-ordering": 1,
|
|
855
|
-
"release-gate":
|
|
860
|
+
"release-gate": 19,
|
|
856
861
|
"resume-args": 6,
|
|
857
862
|
"resume-launch-identity": 6,
|
|
858
863
|
"self-address": 5,
|
|
859
864
|
"setup-verdict": 14,
|
|
860
865
|
"source-install": 2,
|
|
861
|
-
"v2-surface":
|
|
866
|
+
"v2-surface": 13,
|
|
862
867
|
"v2-visible-resume": 17,
|
|
863
868
|
};
|
|
864
869
|
const laneTally: Record<string, number> = {};
|
|
@@ -81,7 +81,6 @@ const PRE_82_LEGACY: Partial<Record<MetaCitizenBackend, [file: string, sentence:
|
|
|
81
81
|
"DELIVERY.md",
|
|
82
82
|
"Admitted before the #82 step 9 contract and not re-evaluated under it, so it is legacy citizen evidence, not a step-9 supported harness",
|
|
83
83
|
],
|
|
84
|
-
codex: ["DELIVERY.md", "No owned native-citizen install/invocation lane."],
|
|
85
84
|
};
|
|
86
85
|
|
|
87
86
|
const fresh = new Set<string>(FRESH_CALL_BACKENDS);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and required to be byte-identical, and `TMUX`/`TMUX_PANE` INHERITED from a fixture pane's
|
|
9
9
|
* own `/proc/<pid>/environ` rather than assembled.
|
|
10
10
|
*
|
|
11
|
-
* The
|
|
11
|
+
* The runtimes under test are FIXTURE scripts, not the operator's real `pi`/`codex`. T1-a's claim is
|
|
12
12
|
* "the window opens in the right place and the named runtime is what starts in it" — a claim a
|
|
13
13
|
* long-lived stand-in proves exactly, without an interactive agent, a model call, or a session
|
|
14
14
|
* record. `resolvePiRuntime` (which finds the official `pi` on PATH) is proven by the
|
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
* - the #105 seat, through the real `freshCall`: an absent seat refuses and creates nothing,
|
|
28
28
|
* an existing one puts the window in THAT session with a receipt naming the resolved
|
|
29
29
|
* target, and the resulting handle closes through `closeWindow` from outside that session
|
|
30
|
+
* - an omitted seat stays caller-local for EVERY backend (#95 D1 retired the Codex home)
|
|
31
|
+
* - #95 lane B's caller-seat leaf against REAL pane titles: 0 / 1 / 2 matching panes, and the
|
|
32
|
+
* activity-adjacent title a ` | `-segment rule would miss
|
|
33
|
+
* - #95 lane B's COMPOSITION: a Codex caller's omitted-placement fresh call lands in the
|
|
34
|
+
* session its own titled pane is in, an explicit seat still overrides it, and an
|
|
35
|
+
* unresolvable anchor refuses with the server byte-identical
|
|
36
|
+
* - #95 lane C: WHERE a sibling starts — a caller's record directory places the pane, an
|
|
37
|
+
* explicit request still wins, an omitted one inherits THIS process's directory rather
|
|
38
|
+
* than the target session's `session_path`, and codex carries that same directory to its
|
|
39
|
+
* thread as `-C`, read off the runtime's own recorded argv
|
|
30
40
|
*/
|
|
31
41
|
|
|
32
42
|
import assert from "node:assert/strict";
|
|
@@ -34,7 +44,7 @@ import { spawnSync } from "node:child_process";
|
|
|
34
44
|
import fs from "node:fs";
|
|
35
45
|
import path from "node:path";
|
|
36
46
|
import { buildLaunchArgs, LaunchPreconditionError, launchPi } from "../pi-extensions/lib/mux-launch.ts";
|
|
37
|
-
import { inspectPlacement } from "../pi-extensions/lib/mux-placement.ts";
|
|
47
|
+
import { inspectPlacement, runTmux } from "../pi-extensions/lib/mux-placement.ts";
|
|
38
48
|
import { skipLive } from "./lib/live-skip.ts";
|
|
39
49
|
|
|
40
50
|
const LABEL = "check-mux-launch-tmux";
|
|
@@ -118,8 +128,14 @@ async function main(): Promise<void> {
|
|
|
118
128
|
const runtimeDir = fs.mkdtempSync(path.join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "entwurf-mux-rt-"));
|
|
119
129
|
// A long-lived stand-in for the official runtime. `exec` so no shell survives: the pane's
|
|
120
130
|
// process must be the runtime itself for the pane_pid claim to mean anything.
|
|
131
|
+
// Each stand-in also DUMPS its own argv beside itself before it sleeps. That file is the
|
|
132
|
+
// only oracle in this repo for "what actually reached the runtime", independent of the
|
|
133
|
+
// builder that composed it — #95 lane C needs it because codex's thread directory rides a
|
|
134
|
+
// VENDOR flag whose absence is silent (the thread simply opens in the app-server's repo).
|
|
121
135
|
const runtime = path.join(runtimeDir, "pi");
|
|
122
|
-
|
|
136
|
+
const DUMP_ARGV = '#!/bin/sh\nprintf \'%s\\n\' "$@" > "$0.argv"\nexec sleep 900\n';
|
|
137
|
+
fs.writeFileSync(runtime, DUMP_ARGV, { mode: 0o755 });
|
|
138
|
+
fs.writeFileSync(path.join(runtimeDir, "codex"), DUMP_ARGV, { mode: 0o755 });
|
|
123
139
|
|
|
124
140
|
try {
|
|
125
141
|
assert.equal(fx("-f", "/dev/null", "new-session", "-d", "-s", SESSION).status, 0, "fixture new-session");
|
|
@@ -353,7 +369,7 @@ async function main(): Promise<void> {
|
|
|
353
369
|
fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").join(" ") === callerWindowsBefore,
|
|
354
370
|
);
|
|
355
371
|
ok(
|
|
356
|
-
"seat: an omitted seat still lands in the caller's own session and the receipt names none",
|
|
372
|
+
"seat: an omitted Pi seat still lands in the caller's own session and the receipt names none",
|
|
357
373
|
(() => {
|
|
358
374
|
const own = call();
|
|
359
375
|
if (!own.ok) return false;
|
|
@@ -370,8 +386,333 @@ async function main(): Promise<void> {
|
|
|
370
386
|
closeWindow(receipt, inherited) === "closed" &&
|
|
371
387
|
!fxLines("list-windows", "-a", "-F", "#{window_id}").includes(receipt.windowId),
|
|
372
388
|
);
|
|
389
|
+
|
|
390
|
+
// (iv) #95 D1 (GLG, 2026-09-16): an omitted seat is caller-local for EVERY backend,
|
|
391
|
+
// Codex included. Until then an omitted-placement Codex selected a fixed existing
|
|
392
|
+
// session named `codex` — a room the operator had to keep because nothing could find
|
|
393
|
+
// the pane a Codex caller was sitting in. The caller-pane anchor below IS that
|
|
394
|
+
// mapping, so this cell now asserts the ABSENCE of the old default: with a session
|
|
395
|
+
// literally named `codex` present on this server, an omitted-placement Codex call
|
|
396
|
+
// must still land in the caller's own session and name no seat at all.
|
|
397
|
+
assert.equal(fx("new-session", "-d", "-s", "codex").status, 0, "fixture session named codex");
|
|
398
|
+
const decoyHomeId = fxLines("list-windows", "-t", "=codex", "-F", "#{session_id}")[0];
|
|
399
|
+
const omittedCodex = freshCall(
|
|
400
|
+
{
|
|
401
|
+
backend: "codex",
|
|
402
|
+
model: "fixture-model",
|
|
403
|
+
task: "fixture task",
|
|
404
|
+
callerGardenId: "20260101T000000-fixture",
|
|
405
|
+
},
|
|
406
|
+
inherited,
|
|
407
|
+
);
|
|
408
|
+
assert.ok(
|
|
409
|
+
omittedCodex.ok,
|
|
410
|
+
`the omitted-placement Codex call must succeed: ${omittedCodex.ok ? "" : omittedCodex.reason}`,
|
|
411
|
+
);
|
|
412
|
+
ok(
|
|
413
|
+
"no fixed home: an omitted Codex seat is caller-local and names NO seat, even with a session called `codex` right there",
|
|
414
|
+
omittedCodex.receipt.sessionId === placement.sessionId &&
|
|
415
|
+
omittedCodex.receipt.sessionId !== decoyHomeId &&
|
|
416
|
+
omittedCodex.receipt.tmuxSession === undefined &&
|
|
417
|
+
omittedCodex.receipt.tmuxSessionSource === undefined &&
|
|
418
|
+
fxLines("list-windows", "-t", decoyHomeId, "-F", "#{window_id}").length === 1,
|
|
419
|
+
);
|
|
420
|
+
process.kill(Number(omittedCodex.receipt.panePid), "SIGKILL");
|
|
373
421
|
fx("kill-session", "-t", seatId);
|
|
422
|
+
fx("kill-session", "-t", decoyHomeId);
|
|
374
423
|
ok("seat: the fixture is back to one session", sessionCount() === 1);
|
|
424
|
+
|
|
425
|
+
// (v) #95 lane B: the caller-seat leaf, against REAL pane titles on this server.
|
|
426
|
+
// The deterministic gate can pin the anchor and the counting, but "what tmux
|
|
427
|
+
// actually reports as `#{pane_title}`, and which `$session` that pane is in" had
|
|
428
|
+
// no oracle independent of the leaf's own parse. This cell is that oracle. The
|
|
429
|
+
// titles are set with `select-pane -T` rather than by a Codex process: the claim
|
|
430
|
+
// under test is the LOOKUP, and a real TUI would add a model turn, a record and a
|
|
431
|
+
// vendor version to a gate that must stay hermetic.
|
|
432
|
+
{
|
|
433
|
+
const { resolveCodexCallerSeat } = await import("../pi-extensions/lib/codex-caller-seat.ts");
|
|
434
|
+
const thread = "01a0a7f9-ed9c-7aa2-a4dd-b1a39c0d4e11";
|
|
435
|
+
const anchor = `${thread.slice(0, 29)}...`;
|
|
436
|
+
const seatRun = (args: string[]) => runTmux(args, inherited);
|
|
437
|
+
const titleOf = (pane: string): string =>
|
|
438
|
+
fxLines("list-panes", "-a", "-F", "#{pane_id}|#{pane_title}").find((row) => row.startsWith(`${pane}|`)) ?? "";
|
|
439
|
+
|
|
440
|
+
// 0 matches: every pane on this server carries the default title, which is the
|
|
441
|
+
// same shape a server with `allow-set-title off` reports for a live Codex.
|
|
442
|
+
ok(
|
|
443
|
+
"caller seat: no pane naming the thread refuses as unresolved — no fallback session",
|
|
444
|
+
(() => {
|
|
445
|
+
const r = resolveCodexCallerSeat(thread, seatRun);
|
|
446
|
+
return !r.ok && r.reason === "codex-caller-seat-unresolved";
|
|
447
|
+
})(),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// 1 match, in a session that is NOT the caller's: the leaf must report THAT
|
|
451
|
+
// session, which is the whole point of the anchor.
|
|
452
|
+
assert.equal(fx("new-session", "-d", "-s", `${SESSION}-tui`).status, 0, "fixture TUI session");
|
|
453
|
+
const tuiSessionId = fxLines("list-windows", "-t", `=${SESSION}-tui`, "-F", "#{session_id}")[0];
|
|
454
|
+
const tuiPane = fxLines("list-panes", "-t", `=${SESSION}-tui`, "-F", "#{pane_id}")[0];
|
|
455
|
+
assert.equal(fx("select-pane", "-t", tuiPane, "-T", `entwurf | ${anchor}`).status, 0, "fixture TUI title");
|
|
456
|
+
ok(
|
|
457
|
+
"caller seat: tmux really reports the OSC-shaped title we are matching against",
|
|
458
|
+
titleOf(tuiPane) === `${tuiPane}|entwurf | ${anchor}`,
|
|
459
|
+
);
|
|
460
|
+
ok(
|
|
461
|
+
"caller seat: the one matching pane resolves to ITS session, not the caller's",
|
|
462
|
+
(() => {
|
|
463
|
+
const r = resolveCodexCallerSeat(thread, seatRun);
|
|
464
|
+
return (
|
|
465
|
+
r.ok &&
|
|
466
|
+
r.seat.paneId === tuiPane &&
|
|
467
|
+
r.seat.sessionId === tuiSessionId &&
|
|
468
|
+
r.seat.sessionId !== placement.sessionId &&
|
|
469
|
+
r.seat.source === "codex-title-anchor"
|
|
470
|
+
);
|
|
471
|
+
})(),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// The ACTIVITY-adjacent shape. `title_setup.rs:183-193` joins the activity item
|
|
475
|
+
// to its neighbour with a plain space, so an operator list ending in `activity`
|
|
476
|
+
// renders the thread id INSIDE one ` | ` segment. A segment-equality rule reads
|
|
477
|
+
// this pane as no match at all; the token rule resolves it.
|
|
478
|
+
assert.equal(fx("select-pane", "-t", tuiPane, "-T", `entwurf | Working ${anchor}`).status, 0, "activity title");
|
|
479
|
+
ok(
|
|
480
|
+
"caller seat: an activity-adjacent title still resolves — the id is a TOKEN, not a whole segment",
|
|
481
|
+
(() => {
|
|
482
|
+
const r = resolveCodexCallerSeat(thread, seatRun);
|
|
483
|
+
return r.ok && r.seat.paneId === tuiPane && r.seat.sessionId === tuiSessionId;
|
|
484
|
+
})(),
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// 2 matches: a stale duplicate in another session. Picking either would seat a
|
|
488
|
+
// sibling by guess, so nothing is chosen.
|
|
489
|
+
assert.equal(fx("split-window", "-d", "-t", tuiPane).status, 0, "fixture duplicate pane");
|
|
490
|
+
const duplicate = fxLines("list-panes", "-t", `=${SESSION}-tui`, "-F", "#{pane_id}").find(
|
|
491
|
+
(id) => id !== tuiPane,
|
|
492
|
+
);
|
|
493
|
+
assert.ok(duplicate, "the fixture duplicate pane must exist");
|
|
494
|
+
assert.equal(fx("select-pane", "-t", duplicate, "-T", `tmp | ${anchor}`).status, 0, "duplicate title");
|
|
495
|
+
ok(
|
|
496
|
+
"caller seat: two panes naming one thread refuse as ambiguous — never the first match",
|
|
497
|
+
(() => {
|
|
498
|
+
const r = resolveCodexCallerSeat(thread, seatRun);
|
|
499
|
+
return !r.ok && r.reason === "codex-caller-seat-ambiguous";
|
|
500
|
+
})(),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// ── the COMPOSITION, through the real `freshCall` (#95 lane B C3) ─────────
|
|
504
|
+
// The leaf's decision is proven above; what has no oracle outside production is
|
|
505
|
+
// whether the omitted-placement path for a CODEX CALLER actually reaches that
|
|
506
|
+
// decision and turns it into the `-t` target. These cells run the real
|
|
507
|
+
// composition with the same hermetic runtime the rest of this gate uses, and
|
|
508
|
+
// read the answer from the server's own inventory rather than the receipt alone.
|
|
509
|
+
// Back to EXACTLY one matching pane: the duplicate above must stop naming the
|
|
510
|
+
// thread, or the composition below would (correctly) refuse as ambiguous.
|
|
511
|
+
assert.equal(fx("select-pane", "-t", duplicate, "-T", "plain-shell").status, 0, "clear duplicate title");
|
|
512
|
+
assert.equal(fx("select-pane", "-t", tuiPane, "-T", `entwurf | ${anchor}`).status, 0, "restore TUI title");
|
|
513
|
+
const anchoredCall = (over?: { tmuxSession: string }) =>
|
|
514
|
+
freshCall(
|
|
515
|
+
{
|
|
516
|
+
backend: "pi",
|
|
517
|
+
model: "fixture/model",
|
|
518
|
+
task: "fixture task",
|
|
519
|
+
placement: over,
|
|
520
|
+
callerGardenId: "20260101T000000-fixture",
|
|
521
|
+
callerNativeSessionId: thread,
|
|
522
|
+
},
|
|
523
|
+
inherited,
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const anchored = anchoredCall();
|
|
527
|
+
assert.ok(anchored.ok, `the anchored fresh call must succeed: ${anchored.ok ? "" : anchored.reason}`);
|
|
528
|
+
ok(
|
|
529
|
+
"caller seat composition: the window lands in the session the CALLER's pane is in, not the caller session",
|
|
530
|
+
anchored.receipt.sessionId === tuiSessionId &&
|
|
531
|
+
anchored.receipt.sessionId !== placement.sessionId &&
|
|
532
|
+
fxLines("list-windows", "-t", tuiSessionId, "-F", "#{window_id}").includes(anchored.receipt.windowId),
|
|
533
|
+
);
|
|
534
|
+
ok(
|
|
535
|
+
"caller seat composition: the receipt names the SOURCE and no session name — the pane was observed, not requested",
|
|
536
|
+
anchored.receipt.tmuxSessionSource === "codex-title-anchor" && anchored.receipt.tmuxSession === undefined,
|
|
537
|
+
);
|
|
538
|
+
ok(
|
|
539
|
+
"caller seat composition: that window closes by its own handle",
|
|
540
|
+
closeWindow(anchored.receipt, inherited) === "closed" &&
|
|
541
|
+
!fxLines("list-windows", "-a", "-F", "#{window_id}").includes(anchored.receipt.windowId),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// Rule 1 over rule 2, against a real server: an explicit seat is never rewritten
|
|
545
|
+
// by the anchor, even when the anchor would have resolved.
|
|
546
|
+
assert.equal(fx("new-session", "-d", "-s", `${SESSION}-over`).status, 0, "fixture override session");
|
|
547
|
+
const overrideId = fxLines("list-windows", "-t", `=${SESSION}-over`, "-F", "#{session_id}")[0];
|
|
548
|
+
const overridden = anchoredCall({ tmuxSession: `${SESSION}-over` });
|
|
549
|
+
assert.ok(overridden.ok, `the overridden fresh call must succeed: ${overridden.ok ? "" : overridden.reason}`);
|
|
550
|
+
ok(
|
|
551
|
+
"caller seat composition: an explicit seat still wins over a resolvable anchor",
|
|
552
|
+
overridden.receipt.sessionId === overrideId &&
|
|
553
|
+
overridden.receipt.sessionId !== tuiSessionId &&
|
|
554
|
+
overridden.receipt.tmuxSession === `${SESSION}-over` &&
|
|
555
|
+
overridden.receipt.tmuxSessionSource === "requested",
|
|
556
|
+
);
|
|
557
|
+
closeWindow(overridden.receipt, inherited);
|
|
558
|
+
fx("kill-session", "-t", overrideId);
|
|
559
|
+
|
|
560
|
+
// An unresolvable anchor is a refusal with NOTHING created anywhere — the D2
|
|
561
|
+
// contract, read from the server rather than from the return value alone.
|
|
562
|
+
const beforeUnresolved = inventory();
|
|
563
|
+
const unresolved = freshCall(
|
|
564
|
+
{
|
|
565
|
+
backend: "pi",
|
|
566
|
+
model: "fixture/model",
|
|
567
|
+
task: "fixture task",
|
|
568
|
+
callerGardenId: "20260101T000000-fixture",
|
|
569
|
+
callerNativeSessionId: "01a0ffff-ffff-7fff-bfff-ffffffffffff",
|
|
570
|
+
},
|
|
571
|
+
inherited,
|
|
572
|
+
);
|
|
573
|
+
ok(
|
|
574
|
+
"caller seat composition: an unresolvable anchor refuses and creates NOTHING — no window, no session, no fallback",
|
|
575
|
+
!unresolved.ok && unresolved.reason === "codex-caller-seat-unresolved" && inventory() === beforeUnresolved,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
fx("kill-session", "-t", tuiSessionId);
|
|
579
|
+
ok("caller seat: the fixture is back to one session", sessionCount() === 1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// (vi) #95 lane C: WHERE the sibling starts, read off the pane and off the runtime's
|
|
583
|
+
// own argv. The deterministic gate can pin which value the composition selects; what
|
|
584
|
+
// only tmux and the runtime can answer is whether that value actually placed the
|
|
585
|
+
// pane, and whether the vendor token carrying it survived to the process. A codex
|
|
586
|
+
// thread's directory is the sharper half: with no `-C` the TUI attaches to the
|
|
587
|
+
// operator's app-server and the THREAD opens in the app-server's repo while the pane
|
|
588
|
+
// sits somewhere else entirely — a divergence no receipt in this repo would show.
|
|
589
|
+
{
|
|
590
|
+
const callerRepo = path.join(runtimeDir, "caller-repo");
|
|
591
|
+
const requestedRepo = path.join(runtimeDir, "requested-repo");
|
|
592
|
+
const sessionRepo = path.join(runtimeDir, "session-repo");
|
|
593
|
+
for (const dir of [callerRepo, requestedRepo, sessionRepo]) fs.mkdirSync(dir);
|
|
594
|
+
const paneCwd = (paneId: string): string =>
|
|
595
|
+
fxLines("display-message", "-p", "-t", paneId, "#{pane_current_path}")[0];
|
|
596
|
+
const argvAt = (backend: "pi" | "codex"): string => `${path.join(runtimeDir, backend)}.argv`;
|
|
597
|
+
/** `[측정]` a pane's `#{pane_current_path}` is read from the child's `/proc` at
|
|
598
|
+
* QUERY time, and tmux chdirs in that child AFTER forking it — so a read taken
|
|
599
|
+
* straight after the launch receipt can still answer the SERVER's directory and
|
|
600
|
+
* make a correct `-c` look ignored. The runtime's own argv dump is the settle
|
|
601
|
+
* signal: once it exists the fixture is running and has been chdir'ed. Bounded,
|
|
602
|
+
* never unbounded — a runtime that never starts is a failed assertion, not a hang.
|
|
603
|
+
* Each launch clears the previous file first, so a stale dump can never be read
|
|
604
|
+
* as this launch's. */
|
|
605
|
+
const clearArgv = (backend: "pi" | "codex"): void => fs.rmSync(argvAt(backend), { force: true });
|
|
606
|
+
const awaitRuntime = (backend: "pi" | "codex"): void => {
|
|
607
|
+
const end = Date.now() + 5000;
|
|
608
|
+
while (!fs.existsSync(argvAt(backend)) && Date.now() < end) spawnSync("sleep", ["0.05"]);
|
|
609
|
+
assert.ok(fs.existsSync(argvAt(backend)), `the ${backend} fixture started and recorded its argv`);
|
|
610
|
+
};
|
|
611
|
+
const runtimeArgv = (backend: "pi" | "codex"): string[] => {
|
|
612
|
+
awaitRuntime(backend);
|
|
613
|
+
const argv = fs.readFileSync(argvAt(backend), "utf8").split("\n").slice(0, -1);
|
|
614
|
+
clearArgv(backend);
|
|
615
|
+
return argv;
|
|
616
|
+
};
|
|
617
|
+
const freshWith = (over: { backend?: "pi" | "codex"; cwd?: string; callerCwd?: string }) =>
|
|
618
|
+
freshCall(
|
|
619
|
+
{
|
|
620
|
+
backend: over.backend ?? "pi",
|
|
621
|
+
model: "fixture/model",
|
|
622
|
+
task: "fixture task",
|
|
623
|
+
cwd: over.cwd,
|
|
624
|
+
callerCwd: over.callerCwd,
|
|
625
|
+
callerGardenId: "20260101T000000-fixture",
|
|
626
|
+
},
|
|
627
|
+
inherited,
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
clearArgv("pi");
|
|
631
|
+
const fromRecord = freshWith({ callerCwd: callerRepo });
|
|
632
|
+
assert.ok(fromRecord.ok, `the caller-cwd fresh call must succeed: ${fromRecord.ok ? "" : fromRecord.reason}`);
|
|
633
|
+
awaitRuntime("pi");
|
|
634
|
+
ok(
|
|
635
|
+
"caller cwd: a caller that HAS a record directory opens the pane THERE, not in this process's directory",
|
|
636
|
+
paneCwd(fromRecord.receipt.paneId) === callerRepo &&
|
|
637
|
+
callerRepo !== process.cwd() &&
|
|
638
|
+
fromRecord.receipt.cwd === callerRepo &&
|
|
639
|
+
fromRecord.receipt.cwdSource === "codex-caller-record",
|
|
640
|
+
);
|
|
641
|
+
ok(
|
|
642
|
+
"caller cwd: the runtime argv carries no tmux carrier — the directory reached the PANE through `-c`",
|
|
643
|
+
!runtimeArgv("pi").includes("-c"),
|
|
644
|
+
);
|
|
645
|
+
closeWindow(fromRecord.receipt, inherited);
|
|
646
|
+
|
|
647
|
+
clearArgv("pi");
|
|
648
|
+
const requested = freshWith({ cwd: requestedRepo, callerCwd: callerRepo });
|
|
649
|
+
assert.ok(requested.ok, `the requested-cwd fresh call must succeed: ${requested.ok ? "" : requested.reason}`);
|
|
650
|
+
awaitRuntime("pi");
|
|
651
|
+
ok(
|
|
652
|
+
"caller cwd: an explicit request still wins over the caller's record directory, and the receipt says which rule chose it",
|
|
653
|
+
paneCwd(requested.receipt.paneId) === requestedRepo &&
|
|
654
|
+
requested.receipt.cwd === requestedRepo &&
|
|
655
|
+
requested.receipt.cwdSource === "requested",
|
|
656
|
+
);
|
|
657
|
+
closeWindow(requested.receipt, inherited);
|
|
658
|
+
runtimeArgv("pi");
|
|
659
|
+
|
|
660
|
+
// The inherited case, and the measurement `-C`'s default rests on: with no `-c`
|
|
661
|
+
// the pane takes the directory of the process that ran `new-window` — NOT the
|
|
662
|
+
// target session's `session_path`, which is why a seat in another directory is
|
|
663
|
+
// what makes this readable at all.
|
|
664
|
+
assert.equal(
|
|
665
|
+
fx("new-session", "-d", "-s", `${SESSION}-cwd`, "-c", sessionRepo).status,
|
|
666
|
+
0,
|
|
667
|
+
"fixture session rooted elsewhere",
|
|
668
|
+
);
|
|
669
|
+
const seatCwdId = fxLines("list-windows", "-t", `=${SESSION}-cwd`, "-F", "#{session_id}")[0];
|
|
670
|
+
clearArgv("pi");
|
|
671
|
+
const inherit = freshCall(
|
|
672
|
+
{
|
|
673
|
+
backend: "pi",
|
|
674
|
+
model: "fixture/model",
|
|
675
|
+
task: "fixture task",
|
|
676
|
+
placement: { tmuxSession: `${SESSION}-cwd` },
|
|
677
|
+
callerGardenId: "20260101T000000-fixture",
|
|
678
|
+
},
|
|
679
|
+
inherited,
|
|
680
|
+
);
|
|
681
|
+
assert.ok(inherit.ok, `the inherited-cwd fresh call must succeed: ${inherit.ok ? "" : inherit.reason}`);
|
|
682
|
+
awaitRuntime("pi");
|
|
683
|
+
ok(
|
|
684
|
+
"caller cwd: with NO directory named, the pane inherits this process's — not the target session's session_path — and the receipt invents nothing",
|
|
685
|
+
paneCwd(inherit.receipt.paneId) === process.cwd() &&
|
|
686
|
+
process.cwd() !== sessionRepo &&
|
|
687
|
+
inherit.receipt.sessionId === seatCwdId &&
|
|
688
|
+
inherit.receipt.cwd === undefined &&
|
|
689
|
+
inherit.receipt.cwdSource === undefined,
|
|
690
|
+
);
|
|
691
|
+
closeWindow(inherit.receipt, inherited);
|
|
692
|
+
runtimeArgv("pi");
|
|
693
|
+
fx("kill-session", "-t", seatCwdId);
|
|
694
|
+
|
|
695
|
+
// The codex half: the SAME value the pane got, carried to the vendor as `-C`,
|
|
696
|
+
// read off the process's own argv rather than off the builder that wrote it.
|
|
697
|
+
clearArgv("codex");
|
|
698
|
+
const codexThread = freshWith({ backend: "codex", callerCwd: callerRepo });
|
|
699
|
+
assert.ok(codexThread.ok, `the codex fresh call must succeed: ${codexThread.ok ? "" : codexThread.reason}`);
|
|
700
|
+
const codexArgv = runtimeArgv("codex");
|
|
701
|
+
const dashC = codexArgv.indexOf("-C");
|
|
702
|
+
ok(
|
|
703
|
+
"codex thread cwd: the vendor argv carries `-C <dir>` with the SAME directory the pane landed in — one value, two carriers",
|
|
704
|
+
dashC >= 0 &&
|
|
705
|
+
codexArgv[dashC + 1] === callerRepo &&
|
|
706
|
+
codexArgv.filter((a) => a === "-C").length === 1 &&
|
|
707
|
+
paneCwd(codexThread.receipt.paneId) === callerRepo,
|
|
708
|
+
);
|
|
709
|
+
ok(
|
|
710
|
+
"codex thread cwd: the flag rides the SAME argv as the remote attachment — an attached thread with no override takes the app-server's directory",
|
|
711
|
+
codexArgv.includes("--remote") && dashC > codexArgv.indexOf("--remote"),
|
|
712
|
+
);
|
|
713
|
+
closeWindow(codexThread.receipt, inherited);
|
|
714
|
+
ok("caller cwd: the fixture is back to one session", sessionCount() === 1);
|
|
715
|
+
}
|
|
375
716
|
}
|
|
376
717
|
} finally {
|
|
377
718
|
fx("kill-server");
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
AGY_SEND_TIMEOUT_MS,
|
|
30
30
|
antigravityAdapter,
|
|
31
31
|
createAntigravityAdapter,
|
|
32
|
+
type NativePushRoute,
|
|
32
33
|
type NativePushRunner,
|
|
33
34
|
resolveNativePushAdapter,
|
|
34
35
|
} from "../pi-extensions/lib/native-push/adapter.ts";
|
|
@@ -45,6 +46,11 @@ function eq(label: string, actual: unknown, expected: unknown): void {
|
|
|
45
46
|
passed++;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function agyAddress(route: NativePushRoute): string {
|
|
50
|
+
assert.equal(route.backend, "antigravity", "Antigravity adapter returned its own route kind");
|
|
51
|
+
return route.backend === "antigravity" ? route.lsAddress : "(wrong backend)";
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
const FAKE_BINARY = "/fake/bin/agy";
|
|
49
55
|
const CONV = "conv-abc-123";
|
|
50
56
|
|
|
@@ -129,7 +135,7 @@ function makeFakeRunner(config: FakeConfig): { runner: NativePushRunner; calls:
|
|
|
129
135
|
ok("full-scan: probe finds the route on the SECOND pid (not head -1)", result.status === "alive");
|
|
130
136
|
eq(
|
|
131
137
|
"full-scan: route is pid 456's port (5002)",
|
|
132
|
-
result.status === "alive" ? result.route
|
|
138
|
+
result.status === "alive" ? agyAddress(result.route) : "(dead)",
|
|
133
139
|
"127.0.0.1:5002",
|
|
134
140
|
);
|
|
135
141
|
// proof it did NOT stop at pid 123: it probed BOTH 5001 (miss) and 5002 (hit).
|
|
@@ -219,7 +225,11 @@ for (const [code, what] of [
|
|
|
219
225
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
220
226
|
|
|
221
227
|
const r1 = await adapter.probe(CONV);
|
|
222
|
-
eq(
|
|
228
|
+
eq(
|
|
229
|
+
"volatile-route: probe #1 route = 5002",
|
|
230
|
+
r1.status === "alive" ? agyAddress(r1.route) : "(dead)",
|
|
231
|
+
"127.0.0.1:5002",
|
|
232
|
+
);
|
|
223
233
|
const pgrepAfter1 = calls.filter((c) => c.argv[0] === "pgrep").length;
|
|
224
234
|
|
|
225
235
|
// the LS port shifts (per-process, volatile) — a fresh probe must RE-DISCOVER it.
|
|
@@ -227,7 +237,7 @@ for (const [code, what] of [
|
|
|
227
237
|
const r2 = await adapter.probe(CONV);
|
|
228
238
|
eq(
|
|
229
239
|
"volatile-route: probe #2 RE-DISCOVERS the changed route = 5003 (no cache)",
|
|
230
|
-
r2.status === "alive" ? r2.route
|
|
240
|
+
r2.status === "alive" ? agyAddress(r2.route) : "(dead)",
|
|
231
241
|
"127.0.0.1:5003",
|
|
232
242
|
);
|
|
233
243
|
ok(
|
|
@@ -241,7 +251,7 @@ for (const [code, what] of [
|
|
|
241
251
|
{
|
|
242
252
|
const { runner, calls } = makeFakeRunner({ pids: [123], ss: ssLine(5002, 123), serving: () => true });
|
|
243
253
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
244
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hello world");
|
|
254
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hello world");
|
|
245
255
|
const sendCall = calls.find((c) => c.argv.includes("send-message"));
|
|
246
256
|
eq("send: argv === [binary, agentapi, send-message, conv, content]", sendCall?.argv, [
|
|
247
257
|
FAKE_BINARY,
|
|
@@ -262,7 +272,7 @@ for (const [code, what] of [
|
|
|
262
272
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
263
273
|
let threw = false;
|
|
264
274
|
try {
|
|
265
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
275
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
266
276
|
} catch (err) {
|
|
267
277
|
threw = true;
|
|
268
278
|
ok("send-fail: error names the non-zero exit", /native-push send failed/.test((err as Error).message));
|
|
@@ -286,7 +296,7 @@ for (const [code, what] of [
|
|
|
286
296
|
const { runner, calls } = makeFakeRunner({ pids: [123], ss: ssLine(5002, 123), serving: () => true });
|
|
287
297
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
288
298
|
await adapter.probe(CONV);
|
|
289
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
299
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
290
300
|
const metaCall = calls.find((c) => c.argv.includes("get-conversation-metadata"));
|
|
291
301
|
const sendCall = calls.find((c) => c.argv.includes("send-message"));
|
|
292
302
|
eq("timeout: get-conversation-metadata carries the metadata timeout", metaCall?.timeoutMs, AGY_METADATA_TIMEOUT_MS);
|
|
@@ -317,7 +327,7 @@ for (const [code, what] of [
|
|
|
317
327
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
318
328
|
let threw = false;
|
|
319
329
|
try {
|
|
320
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
330
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
321
331
|
} catch {
|
|
322
332
|
threw = true;
|
|
323
333
|
}
|
|
@@ -327,15 +337,9 @@ for (const [code, what] of [
|
|
|
327
337
|
// ── resolver fail-fast (mirror resolveAcpBackendAdapter) ─────────────────────
|
|
328
338
|
eq("resolve: antigravity → the antigravity adapter", resolveNativePushAdapter("antigravity").id, "antigravity");
|
|
329
339
|
eq("adapter id: antigravityAdapter.id === antigravity", antigravityAdapter.id, "antigravity");
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
resolveNativePushAdapter("codex");
|
|
334
|
-
} catch {
|
|
335
|
-
threw = true;
|
|
336
|
-
}
|
|
337
|
-
ok("resolve: unknown backend (codex) → throw (no silent default)", threw);
|
|
338
|
-
}
|
|
340
|
+
eq("resolve: codex → the Codex adapter", resolveNativePushAdapter("codex").id, "codex");
|
|
341
|
+
eq("retry policy: Antigravity may retry", resolveNativePushAdapter("antigravity").retriable, true);
|
|
342
|
+
eq("retry policy: Codex never retries", resolveNativePushAdapter("codex").retriable, false);
|
|
339
343
|
{
|
|
340
344
|
let threw = false;
|
|
341
345
|
try {
|
|
@@ -39,6 +39,7 @@ const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
|
|
|
39
39
|
function fakeAdapter(probe: NativePushProbeResult): NativePushAdapter {
|
|
40
40
|
return {
|
|
41
41
|
id: "antigravity",
|
|
42
|
+
retriable: true,
|
|
42
43
|
async probe() {
|
|
43
44
|
return probe;
|
|
44
45
|
},
|
|
@@ -48,7 +49,10 @@ function fakeAdapter(probe: NativePushProbeResult): NativePushAdapter {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const ALIVE: NativePushProbeResult = {
|
|
52
|
+
const ALIVE: NativePushProbeResult = {
|
|
53
|
+
status: "alive",
|
|
54
|
+
route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" },
|
|
55
|
+
};
|
|
52
56
|
|
|
53
57
|
async function main(): Promise<void> {
|
|
54
58
|
const dir = mkdtempSync(path.join(os.tmpdir(), "entwurf-register-"));
|