@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
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as net from "node:net";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { renderFreshCall } from "../pi-extensions/lib/mux-fresh-call.ts";
|
|
9
|
+
import {
|
|
10
|
+
type CodexExecRunner,
|
|
11
|
+
type CodexProtocolOpener,
|
|
12
|
+
type CodexRpcProtocol,
|
|
13
|
+
checkCodexSocketFile,
|
|
14
|
+
createCodexNativePushAdapter,
|
|
15
|
+
probeCodexThread,
|
|
16
|
+
readCodexThread,
|
|
17
|
+
realCodexProtocolOpener,
|
|
18
|
+
resolveCodexDefaultSocketPath,
|
|
19
|
+
sendViaCodexQueue,
|
|
20
|
+
setCodexThreadName,
|
|
21
|
+
} from "../pi-extensions/lib/native-push/codex-ws-client.ts";
|
|
22
|
+
import { launchReceiptWindows } from "./lib/launch-receipt-windows.ts";
|
|
23
|
+
|
|
24
|
+
class FakeProtocol implements CodexRpcProtocol {
|
|
25
|
+
readonly calls: Array<{ method: string; params: Record<string, unknown> }> = [];
|
|
26
|
+
closed = false;
|
|
27
|
+
|
|
28
|
+
private readonly replies: unknown[];
|
|
29
|
+
|
|
30
|
+
constructor(replies: unknown[]) {
|
|
31
|
+
this.replies = replies;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async request(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
35
|
+
this.calls.push({ method, params });
|
|
36
|
+
return this.replies.shift();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
notify(method: string, params: Record<string, unknown> = {}): void {
|
|
40
|
+
this.calls.push({ method, params });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
close(): void {
|
|
44
|
+
this.closed = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function opener(protocol: FakeProtocol): CodexProtocolOpener {
|
|
49
|
+
return { open: async () => protocol };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const NEGATIVE_FRAMES = {
|
|
53
|
+
masked: Buffer.from([0x81, 0x80]),
|
|
54
|
+
oversize: Buffer.from([0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01]),
|
|
55
|
+
"json-null": Buffer.from([0x81, 0x04, 0x6e, 0x75, 0x6c, 0x6c]),
|
|
56
|
+
} as const;
|
|
57
|
+
type NegativeFrameCase = keyof typeof NEGATIVE_FRAMES;
|
|
58
|
+
|
|
59
|
+
const NEGATIVE_ERRORS: Record<NegativeFrameCase, RegExp> = {
|
|
60
|
+
masked: /masked WebSocket frame/,
|
|
61
|
+
oversize: /WebSocket frame exceeds 1 MiB/,
|
|
62
|
+
"json-null": /non-object JSON message/,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
async function exerciseNegativeFrame(name: NegativeFrameCase): Promise<void> {
|
|
66
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), `entwurf-codex-ws-${name}-`));
|
|
67
|
+
const socketPath = path.join(scratch, "app-server.sock");
|
|
68
|
+
let peer: net.Socket | undefined;
|
|
69
|
+
const server = net.createServer((socket) => {
|
|
70
|
+
peer = socket;
|
|
71
|
+
socket.on("error", () => {
|
|
72
|
+
// The client deliberately destroys this connection on the malformed frame.
|
|
73
|
+
});
|
|
74
|
+
let handshake = Buffer.alloc(0);
|
|
75
|
+
let requestBytes = Buffer.alloc(0);
|
|
76
|
+
let upgraded = false;
|
|
77
|
+
let replied = false;
|
|
78
|
+
socket.on("data", (incoming) => {
|
|
79
|
+
let chunk = incoming;
|
|
80
|
+
if (!upgraded) {
|
|
81
|
+
handshake = Buffer.concat([handshake, chunk]);
|
|
82
|
+
const boundary = handshake.indexOf("\r\n\r\n");
|
|
83
|
+
if (boundary < 0) return;
|
|
84
|
+
const request = handshake.subarray(0, boundary).toString("utf8");
|
|
85
|
+
const key = request.match(/\r\nSec-WebSocket-Key:\s*([^\r\n]+)/i)?.[1]?.trim();
|
|
86
|
+
if (!key) {
|
|
87
|
+
socket.destroy(new Error("test client omitted Sec-WebSocket-Key"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const accept = crypto.createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
91
|
+
chunk = handshake.subarray(boundary + 4);
|
|
92
|
+
handshake = Buffer.alloc(0);
|
|
93
|
+
upgraded = true;
|
|
94
|
+
socket.write(
|
|
95
|
+
`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
requestBytes = Buffer.concat([requestBytes, chunk]);
|
|
99
|
+
if (replied || requestBytes.length < 2) return;
|
|
100
|
+
if ((requestBytes[1]! & 0x80) === 0) {
|
|
101
|
+
socket.destroy(new Error("test client sent an unmasked WebSocket frame"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
replied = true;
|
|
105
|
+
socket.write(NEGATIVE_FRAMES[name]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
let protocol: CodexRpcProtocol | undefined;
|
|
109
|
+
try {
|
|
110
|
+
await new Promise<void>((resolve, reject) => {
|
|
111
|
+
server.once("error", reject);
|
|
112
|
+
server.listen(socketPath, () => {
|
|
113
|
+
server.off("error", reject);
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
protocol = await realCodexProtocolOpener.open(socketPath);
|
|
118
|
+
await assert.rejects(
|
|
119
|
+
() => protocol!.request("negative/check", {}),
|
|
120
|
+
NEGATIVE_ERRORS[name],
|
|
121
|
+
`${name} must reject the pending request through the real UDS handshake and frame parser`,
|
|
122
|
+
);
|
|
123
|
+
} finally {
|
|
124
|
+
protocol?.close();
|
|
125
|
+
peer?.destroy();
|
|
126
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
127
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const childCase = process.argv[2] === "--ws-negative-child" ? process.argv[3] : undefined;
|
|
132
|
+
if (childCase !== undefined) {
|
|
133
|
+
assert.ok(childCase in NEGATIVE_FRAMES, `unknown negative frame case: ${childCase}`);
|
|
134
|
+
await exerciseNegativeFrame(childCase as NegativeFrameCase);
|
|
135
|
+
fs.writeSync(1, `codex WebSocket negative child rejected ${childCase}\n`);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function checkNegativeFrameBounded(name: NegativeFrameCase): Promise<void> {
|
|
140
|
+
const child = spawn(process.execPath, [...process.execArgv, process.argv[1]!, "--ws-negative-child", name], {
|
|
141
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
142
|
+
});
|
|
143
|
+
let stdout = "";
|
|
144
|
+
let stderr = "";
|
|
145
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
146
|
+
stdout += chunk.toString();
|
|
147
|
+
});
|
|
148
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
149
|
+
stderr += chunk.toString();
|
|
150
|
+
});
|
|
151
|
+
let timedOut = false;
|
|
152
|
+
const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {
|
|
153
|
+
let spawnError: Error | undefined;
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
timedOut = true;
|
|
156
|
+
child.kill("SIGKILL");
|
|
157
|
+
}, 2_000);
|
|
158
|
+
child.once("error", (error) => {
|
|
159
|
+
spawnError = error;
|
|
160
|
+
});
|
|
161
|
+
child.once("close", (code, signal) => {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
resolve({ code, signal, error: spawnError });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
assert.equal(timedOut, false, `[QK:CODEX-NATIVE-WS-TERMINAL] ${name} held the parser child past its 2s bound`);
|
|
167
|
+
assert.equal(result.code, 0, `${name} child failed (${result.error?.message ?? result.signal ?? stderr.trim()})`);
|
|
168
|
+
assert.match(stdout, new RegExp(`rejected ${name}\\s*$`), `${name} did not reach its bounded rejection`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await Promise.all((Object.keys(NEGATIVE_FRAMES) as NegativeFrameCase[]).map(checkNegativeFrameBounded));
|
|
172
|
+
|
|
173
|
+
assert.equal(
|
|
174
|
+
resolveCodexDefaultSocketPath({ HOME: "/home/operator" }),
|
|
175
|
+
"/home/operator/.codex/app-server-control/app-server-control.sock",
|
|
176
|
+
"[QK:CODEX-NATIVE-FIXED-SOCKET] HOME resolves only the vendor default socket",
|
|
177
|
+
);
|
|
178
|
+
assert.equal(
|
|
179
|
+
resolveCodexDefaultSocketPath({ HOME: "/ignored", CODEX_HOME: "/codex-home" }),
|
|
180
|
+
"/codex-home/app-server-control/app-server-control.sock",
|
|
181
|
+
"[QK:CODEX-NATIVE-FIXED-SOCKET] CODEX_HOME owns the vendor default socket root",
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-codex-native-"));
|
|
185
|
+
try {
|
|
186
|
+
const missing = checkCodexSocketFile(path.join(temp, "missing.sock"));
|
|
187
|
+
assert.deepEqual(missing.ok ? null : missing.status, "dead", "missing socket is a dead app-server");
|
|
188
|
+
const regular = path.join(temp, "regular");
|
|
189
|
+
fs.writeFileSync(regular, "not a socket");
|
|
190
|
+
const wrongType = checkCodexSocketFile(regular);
|
|
191
|
+
assert.deepEqual(wrongType.ok ? null : wrongType.status, "indeterminate", "wrong path type never reads dead");
|
|
192
|
+
const link = path.join(temp, "link");
|
|
193
|
+
fs.symlinkSync(regular, link);
|
|
194
|
+
assert.match(JSON.stringify(checkCodexSocketFile(link)), /symlink/, "symlinked socket is refused by name");
|
|
195
|
+
} finally {
|
|
196
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const firstPage = new FakeProtocol([
|
|
200
|
+
{},
|
|
201
|
+
{ data: ["other"], nextCursor: "next" },
|
|
202
|
+
{ data: ["target"], nextCursor: null },
|
|
203
|
+
]);
|
|
204
|
+
const alive = await probeCodexThread("target", {
|
|
205
|
+
env: { CODEX_HOME: "/fixed" },
|
|
206
|
+
checkSocket: () => ({ ok: true }),
|
|
207
|
+
openProtocol: opener(firstPage),
|
|
208
|
+
});
|
|
209
|
+
assert.deepEqual(
|
|
210
|
+
alive,
|
|
211
|
+
{ status: "alive", route: { backend: "codex", socketPath: "/fixed/app-server-control/app-server-control.sock" } },
|
|
212
|
+
"[QK:CODEX-NATIVE-LOADED-PROBE] full loaded-thread pagination proves the volatile route",
|
|
213
|
+
);
|
|
214
|
+
assert.equal(firstPage.closed, true, "successful probe closes its connection");
|
|
215
|
+
assert.deepEqual(
|
|
216
|
+
firstPage.calls.map((call) => call.method),
|
|
217
|
+
["initialize", "initialized", "thread/loaded/list", "thread/loaded/list"],
|
|
218
|
+
);
|
|
219
|
+
assert.deepEqual(firstPage.calls[3]?.params, { cursor: "next" }, "pagination carries the vendor cursor");
|
|
220
|
+
|
|
221
|
+
const absentProtocol = new FakeProtocol([{}, { data: [], nextCursor: null }]);
|
|
222
|
+
const absent = await probeCodexThread("target", {
|
|
223
|
+
env: { CODEX_HOME: "/fixed" },
|
|
224
|
+
checkSocket: () => ({ ok: true }),
|
|
225
|
+
openProtocol: opener(absentProtocol),
|
|
226
|
+
});
|
|
227
|
+
assert.equal(absent.status, "dead", "a clean loaded inventory without the target is not delivery-capable");
|
|
228
|
+
assert.equal(absentProtocol.closed, true, "negative probe closes its connection");
|
|
229
|
+
|
|
230
|
+
const malformedProtocol = new FakeProtocol([{}, { data: [{ id: "wrong-shape" }], nextCursor: null }]);
|
|
231
|
+
const malformed = await probeCodexThread("target", {
|
|
232
|
+
env: { CODEX_HOME: "/fixed" },
|
|
233
|
+
checkSocket: () => ({ ok: true }),
|
|
234
|
+
openProtocol: opener(malformedProtocol),
|
|
235
|
+
});
|
|
236
|
+
assert.equal(malformed.status, "indeterminate", "a malformed inventory is unknown, never dead");
|
|
237
|
+
assert.equal(malformedProtocol.closed, true, "failed protocol probe closes its connection");
|
|
238
|
+
|
|
239
|
+
let queueArgv: readonly string[] = [];
|
|
240
|
+
const queueRunner: CodexExecRunner = {
|
|
241
|
+
async exec(argv) {
|
|
242
|
+
queueArgv = argv;
|
|
243
|
+
return { code: 0, stdout: "Queued message 01a-message for thread target.\n", stderr: "" };
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
const body = "quotes ' \" and\nnewlines stay one argv value";
|
|
247
|
+
await sendViaCodexQueue({ backend: "codex", socketPath: "/fixed/socket" }, "target", body, queueRunner);
|
|
248
|
+
assert.deepEqual(
|
|
249
|
+
queueArgv,
|
|
250
|
+
["codex", "queue", "--remote", "unix:///fixed/socket", "--thread", "target", "--message", body],
|
|
251
|
+
"[QK:CODEX-NATIVE-VENDOR-QUEUE] send is one argv-safe vendor queue invocation",
|
|
252
|
+
);
|
|
253
|
+
await assert.rejects(
|
|
254
|
+
() =>
|
|
255
|
+
sendViaCodexQueue({ backend: "codex", socketPath: "/fixed/socket" }, "target", "body", {
|
|
256
|
+
async exec() {
|
|
257
|
+
return { code: 0, stdout: "Queued message id for thread another.\n", stderr: "" };
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
/invalid acknowledgement/,
|
|
261
|
+
"an acknowledgement for another thread is not delivery evidence",
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const titleProtocol = new FakeProtocol([{}, {}]);
|
|
265
|
+
await setCodexThreadName("/fixed/socket", "target", "garden-id", opener(titleProtocol));
|
|
266
|
+
assert.deepEqual(
|
|
267
|
+
titleProtocol.calls.at(-1),
|
|
268
|
+
{ method: "thread/name/set", params: { threadId: "target", name: "garden-id" } },
|
|
269
|
+
"[QK:CODEX-NATIVE-VISIBLE-NAME] the vendor thread title receives the garden id",
|
|
270
|
+
);
|
|
271
|
+
assert.equal(titleProtocol.closed, true, "title update closes its connection");
|
|
272
|
+
|
|
273
|
+
// The READ half of the visible title. `setCodexThreadName` above proves birth ARMS it;
|
|
274
|
+
// only reading it back through the app-server proves the operator would see it, and that is
|
|
275
|
+
// what the first-admission LIVE acceptance asserts against the garden id. The dialect is
|
|
276
|
+
// held here so the metered LIVE run is not the first place a param name is checked.
|
|
277
|
+
const readProtocol = new FakeProtocol([{}, { thread: { id: "target", name: "20260911T064717-bc47f9" } }]);
|
|
278
|
+
const readThread = await readCodexThread("/fixed/socket", "target", false, opener(readProtocol));
|
|
279
|
+
assert.deepEqual(
|
|
280
|
+
readProtocol.calls.at(-1),
|
|
281
|
+
{ method: "thread/read", params: { threadId: "target", includeTurns: false } },
|
|
282
|
+
"[QK:CODEX-THREAD-READ-DIALECT] the visible title is read back with the vendor's own thread/read params",
|
|
283
|
+
);
|
|
284
|
+
assert.equal(readThread.name, "20260911T064717-bc47f9", "the read view carries the live thread title");
|
|
285
|
+
assert.equal(readProtocol.closed, true, "the title read closes its connection");
|
|
286
|
+
const turnsProtocol = new FakeProtocol([{}, { thread: { id: "target", name: null, turns: [] } }]);
|
|
287
|
+
await readCodexThread("/fixed/socket", "target", true, opener(turnsProtocol));
|
|
288
|
+
assert.deepEqual(
|
|
289
|
+
turnsProtocol.calls.at(-1),
|
|
290
|
+
{ method: "thread/read", params: { threadId: "target", includeTurns: true } },
|
|
291
|
+
"includeTurns travels as the vendor spells it — the turn items are the orphan-window recovery authority",
|
|
292
|
+
);
|
|
293
|
+
for (const malformed of [null, {}, { thread: null }, { thread: [] }]) {
|
|
294
|
+
let refused = false;
|
|
295
|
+
try {
|
|
296
|
+
await readCodexThread("/fixed/socket", "target", false, opener(new FakeProtocol([{}, malformed])));
|
|
297
|
+
} catch {
|
|
298
|
+
refused = true;
|
|
299
|
+
}
|
|
300
|
+
assert.equal(
|
|
301
|
+
refused,
|
|
302
|
+
true,
|
|
303
|
+
`[QK:CODEX-THREAD-READ-REFUSES-MALFORMED] a thread/read reply without a thread object is refused, never read as an empty title: ${JSON.stringify(malformed)}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ORPHAN RECOVERY, proven against the REAL renderer rather than a hand-typed sample. When a
|
|
308
|
+
// travelling receipt never arrives, the only records of the window a sibling opened are the
|
|
309
|
+
// caller's transcript and the Codex thread's tool-call result; this parser is what reads the
|
|
310
|
+
// handle back out of them, and a metered LIVE failure must not be the first place it is tried.
|
|
311
|
+
// The LONGEST shape the renderer can produce for one launch: both optional lines present,
|
|
312
|
+
// a real project path, a long model id. The bound this replaced (400 characters) was already
|
|
313
|
+
// down to 56 characters of headroom on an ordinary cwd+seat receipt.
|
|
314
|
+
const codexReceipt = renderFreshCall({
|
|
315
|
+
ok: true,
|
|
316
|
+
receipt: {
|
|
317
|
+
backend: "codex",
|
|
318
|
+
runtimePath: "/home/junghan/.local/share/pnpm/global/5/node_modules/@openai/codex/bin/codex",
|
|
319
|
+
model: "gpt-5.6-thinking-high[extended-context]",
|
|
320
|
+
cwd: "/home/junghan/repos/work/a-fairly-long-company-project/packages/backend-service/apps/worker",
|
|
321
|
+
tmuxSession: "a-long-operator-project-session-name",
|
|
322
|
+
windowId: "@41",
|
|
323
|
+
windowIndex: "3",
|
|
324
|
+
sessionId: "$7",
|
|
325
|
+
paneId: "%88",
|
|
326
|
+
panePid: "4242",
|
|
327
|
+
nonce: "mux-fresh-call-abc123",
|
|
328
|
+
serverPid: "9001",
|
|
329
|
+
},
|
|
330
|
+
}).text;
|
|
331
|
+
const piReceipt = renderFreshCall({
|
|
332
|
+
ok: true,
|
|
333
|
+
receipt: {
|
|
334
|
+
backend: "pi",
|
|
335
|
+
runtimePath: "/usr/bin/pi",
|
|
336
|
+
model: "provider/model",
|
|
337
|
+
windowId: "@42",
|
|
338
|
+
windowIndex: "4",
|
|
339
|
+
sessionId: "$7",
|
|
340
|
+
paneId: "%89",
|
|
341
|
+
panePid: "4243",
|
|
342
|
+
nonce: "mux-fresh-call-def456",
|
|
343
|
+
serverPid: "9001",
|
|
344
|
+
},
|
|
345
|
+
}).text;
|
|
346
|
+
assert.deepEqual(
|
|
347
|
+
launchReceiptWindows(codexReceipt, "codex"),
|
|
348
|
+
["@41"],
|
|
349
|
+
"[QK:CODEX-ORPHAN-WINDOW-RECOVERY] the exact Codex window handle is recovered from the LONGEST receipt the renderer actually writes — cwd, seat and a long model id included, because a length bound between the backend and window lines is a guess this parser must not make",
|
|
350
|
+
);
|
|
351
|
+
// The transcript form: the same receipt with its newlines JSON-escaped, which is how the
|
|
352
|
+
// caller's own transcript and the Codex tool-call result actually carry it.
|
|
353
|
+
assert.deepEqual(
|
|
354
|
+
launchReceiptWindows(JSON.stringify({ text: codexReceipt }), "codex"),
|
|
355
|
+
["@41"],
|
|
356
|
+
"the escaped-newline transcript form recovers the same handle",
|
|
357
|
+
);
|
|
358
|
+
assert.deepEqual(launchReceiptWindows(codexReceipt, "pi"), [], "a codex receipt never answers as a pi window");
|
|
359
|
+
assert.deepEqual(
|
|
360
|
+
launchReceiptWindows(`${codexReceipt}\n${piReceipt}`, "pi"),
|
|
361
|
+
["@42"],
|
|
362
|
+
"each backend keeps its own handle",
|
|
363
|
+
);
|
|
364
|
+
assert.deepEqual(
|
|
365
|
+
launchReceiptWindows("an operator mentioned @99 and window: @98 with no backend line", "codex"),
|
|
366
|
+
[],
|
|
367
|
+
"a bare handle with no LAUNCH receipt around it is never adopted as ours",
|
|
368
|
+
);
|
|
369
|
+
assert.deepEqual(
|
|
370
|
+
launchReceiptWindows(`${codexReceipt}\nwindow: @97 mentioned after the receipt block`, "codex"),
|
|
371
|
+
["@41"],
|
|
372
|
+
"one receipt names ONE window — a later stray line does not inherit its backend",
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const adapter = createCodexNativePushAdapter({
|
|
376
|
+
env: { CODEX_HOME: "/fixed" },
|
|
377
|
+
checkSocket: () => ({ ok: true }),
|
|
378
|
+
openProtocol: opener(new FakeProtocol([{}, { data: ["target"], nextCursor: null }])),
|
|
379
|
+
runner: queueRunner,
|
|
380
|
+
});
|
|
381
|
+
assert.equal(adapter.id, "codex");
|
|
382
|
+
assert.equal(adapter.retriable, false, "[QK:CODEX-NATIVE-NO-RETRY] ambiguous queue failure must never be replayed");
|
|
383
|
+
|
|
384
|
+
console.log(
|
|
385
|
+
"check-codex-native-push: fixed UDS probe, terminal malformed frames, vendor queue send, no-retry policy, and the visible title's write AND read-back are green",
|
|
386
|
+
);
|