@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
@@ -0,0 +1,399 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { launchReceiptWindows } from "./launch-receipt-windows.ts";
3
+
4
+ export type SourceToolStatus = "pending" | "completed" | "failed";
5
+
6
+ export interface SourceToolReceipt {
7
+ toolName: string;
8
+ arguments: Record<string, unknown>;
9
+ text: string;
10
+ status: SourceToolStatus;
11
+ isError: boolean;
12
+ }
13
+
14
+ export interface ExpectedSourceToolCall {
15
+ toolName: string;
16
+ arguments: Record<string, unknown>;
17
+ }
18
+
19
+ /** Select one source call in its backend/target scope, rejecting drift, errors, and duplicates. */
20
+ export function selectExactSourceToolReceipt(
21
+ receipts: readonly SourceToolReceipt[],
22
+ toolName: string,
23
+ expectedArguments: Record<string, unknown>,
24
+ label: string,
25
+ ): SourceToolReceipt | null {
26
+ const scopeKey = toolName === "entwurf_v2" ? "target" : null;
27
+ const matchingScope = receipts.filter(
28
+ (receipt) =>
29
+ receipt.toolName === toolName &&
30
+ (scopeKey === null || receipt.arguments[scopeKey] === expectedArguments[scopeKey]),
31
+ );
32
+ if (matchingScope.length > 1) throw new Error(`${label}: duplicate source tool calls (${matchingScope.length})`);
33
+ const receipt = matchingScope[0] ?? null;
34
+ if (receipt === null) return null;
35
+ if (!isDeepStrictEqual(receipt.arguments, expectedArguments))
36
+ throw new Error(
37
+ `${label}: source tool arguments differ\nexpected=${JSON.stringify(expectedArguments)}\nactual=${JSON.stringify(receipt.arguments)}`,
38
+ );
39
+ if (receipt.status === "pending") return null;
40
+ if (receipt.status === "failed") throw new Error(`${label}: exact source tool result failed: ${receipt.text}`);
41
+ return receipt;
42
+ }
43
+
44
+ export function assertSourceCallScopes(
45
+ receipts: readonly SourceToolReceipt[],
46
+ toolName: string,
47
+ scopeKey: string,
48
+ allowed: readonly unknown[],
49
+ label: string,
50
+ ): void {
51
+ for (const receipt of receipts) {
52
+ if (receipt.toolName !== toolName) continue;
53
+ if (!allowed.includes(receipt.arguments[scopeKey]))
54
+ throw new Error(`${label}: unexpected ${scopeKey} ${JSON.stringify(receipt.arguments[scopeKey])}`);
55
+ }
56
+ }
57
+
58
+ /** Final exact-once audit over every source-observed Entwurf role in one smoke citizen. */
59
+ export function assertExactSourceToolCalls(
60
+ receipts: readonly SourceToolReceipt[],
61
+ expected: readonly ExpectedSourceToolCall[],
62
+ label: string,
63
+ ): void {
64
+ if (receipts.length !== expected.length)
65
+ throw new Error(`${label}: source call count differs: expected=${expected.length} actual=${receipts.length}`);
66
+ const remaining = [...expected];
67
+ for (const receipt of receipts) {
68
+ if (receipt.status !== "completed") throw new Error(`${label}: ${receipt.toolName} remained ${receipt.status}`);
69
+ const index = remaining.findIndex(
70
+ (candidate) =>
71
+ candidate.toolName === receipt.toolName && isDeepStrictEqual(candidate.arguments, receipt.arguments),
72
+ );
73
+ if (index < 0)
74
+ throw new Error(`${label}: unexpected source call ${receipt.toolName} ${JSON.stringify(receipt.arguments)}`);
75
+ remaining.splice(index, 1);
76
+ }
77
+ if (remaining.length > 0) throw new Error(`${label}: missing source calls ${JSON.stringify(remaining)}`);
78
+ }
79
+
80
+ /** The only production leaf that turns joined source receipts into cleanup window authority. */
81
+ export function sourceCleanupWindows(receipts: readonly SourceToolReceipt[]): string[] {
82
+ const windows: string[] = [];
83
+ for (const receipt of receipts) {
84
+ if (receipt.toolName !== "entwurf_fresh_call" || receipt.status !== "completed" || receipt.isError) continue;
85
+ const backend = receipt.arguments.backend;
86
+ if (backend !== "pi" && backend !== "codex") continue;
87
+ windows.push(...launchReceiptWindows(receipt.text, backend));
88
+ }
89
+ return windows;
90
+ }
91
+
92
+ function textContent(value: unknown): string {
93
+ if (!Array.isArray(value)) return "";
94
+ return value
95
+ .map((block) => {
96
+ if (typeof block !== "object" || block === null) return "";
97
+ const text = (block as { text?: unknown }).text;
98
+ return typeof text === "string" ? text : "";
99
+ })
100
+ .join("\n");
101
+ }
102
+
103
+ function looksLikeIncompleteJsonAppend(source: string): boolean {
104
+ let depth = 0;
105
+ let inString = false;
106
+ let escaped = false;
107
+ for (const character of source) {
108
+ if (inString) {
109
+ if (escaped) escaped = false;
110
+ else if (character === "\\") escaped = true;
111
+ else if (character === '"') inString = false;
112
+ continue;
113
+ }
114
+ if (character === '"') inString = true;
115
+ else if (character === "{" || character === "[") depth++;
116
+ else if (character === "}" || character === "]") depth--;
117
+ if (depth < 0) return false;
118
+ }
119
+ return inString || depth > 0;
120
+ }
121
+
122
+ /**
123
+ * Resolve a source transcript path whose own record may not carry it yet. The record stays the
124
+ * sole authority: an empty cache re-reads it, so a path the runtime writes AFTER callback
125
+ * correlation is still observed instead of being frozen out of the run. Measured — a pi record
126
+ * gained its `transcriptPath` 34s after correlation, and a one-shot read made every later
127
+ * source observation permanently empty. A reader that throws is fail-loud; only "the record has
128
+ * no path yet" is pending. Model prose and tmux listings are never a substitute source.
129
+ */
130
+ export function resolveSourceTranscriptPath(cached: string, readRecordPath: () => string | null | undefined): string {
131
+ if (cached !== "") return cached;
132
+ return readRecordPath() ?? "";
133
+ }
134
+
135
+ /** Parse complete JSONL rows; only one structurally incomplete final append is pending. */
136
+ export function parseJsonLines(source: string): unknown[] {
137
+ const lines = source.split("\n");
138
+ const entries: unknown[] = [];
139
+ for (const [index, line] of lines.entries()) {
140
+ if (line.trim() === "") continue;
141
+ try {
142
+ entries.push(JSON.parse(line));
143
+ } catch (error) {
144
+ const isIncompleteLastAppend =
145
+ index === lines.length - 1 && !source.endsWith("\n") && looksLikeIncompleteJsonAppend(line);
146
+ if (isIncompleteLastAppend) return entries;
147
+ throw new Error(
148
+ `invalid complete JSONL row ${index + 1}: ${error instanceof Error ? error.message : String(error)}`,
149
+ );
150
+ }
151
+ }
152
+ return entries;
153
+ }
154
+
155
+ /** Join Pi's own toolCall and toolResult rows by native id, retaining pending calls. */
156
+ export function piSourceToolReceipts(entries: readonly unknown[]): SourceToolReceipt[] {
157
+ const calls = new Map<string, { toolName: string; arguments: Record<string, unknown> }>();
158
+ const results = new Map<string, { toolName: string; text: string; isError: boolean }>();
159
+ for (const raw of entries) {
160
+ if (typeof raw !== "object" || raw === null) continue;
161
+ const entry = raw as { type?: unknown; message?: unknown };
162
+ if (entry.type !== "message" || typeof entry.message !== "object" || entry.message === null) continue;
163
+ const message = entry.message as {
164
+ role?: unknown;
165
+ content?: unknown;
166
+ toolCallId?: unknown;
167
+ toolName?: unknown;
168
+ isError?: unknown;
169
+ };
170
+ if (message.role === "assistant" && Array.isArray(message.content)) {
171
+ for (const rawBlock of message.content) {
172
+ if (typeof rawBlock !== "object" || rawBlock === null) continue;
173
+ const block = rawBlock as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
174
+ if (block.type !== "toolCall") continue;
175
+ if (
176
+ typeof block.id !== "string" ||
177
+ typeof block.name !== "string" ||
178
+ typeof block.arguments !== "object" ||
179
+ block.arguments === null ||
180
+ Array.isArray(block.arguments)
181
+ )
182
+ throw new Error("malformed Pi toolCall row");
183
+ if (calls.has(block.id)) throw new Error(`duplicate Pi toolCall id ${block.id}`);
184
+ calls.set(block.id, {
185
+ toolName: block.name,
186
+ arguments: block.arguments as Record<string, unknown>,
187
+ });
188
+ }
189
+ }
190
+ if (message.role === "toolResult") {
191
+ if (typeof message.toolCallId !== "string" || typeof message.toolName !== "string")
192
+ throw new Error("malformed Pi toolResult row");
193
+ if (typeof message.isError !== "boolean")
194
+ throw new Error(`Pi toolResult ${message.toolCallId} lacks explicit boolean isError`);
195
+ if (results.has(message.toolCallId)) throw new Error(`duplicate Pi toolResult id ${message.toolCallId}`);
196
+ results.set(message.toolCallId, {
197
+ toolName: message.toolName,
198
+ text: textContent(message.content),
199
+ isError: message.isError,
200
+ });
201
+ }
202
+ }
203
+ for (const id of results.keys()) {
204
+ if (!calls.has(id)) throw new Error(`unmatched Pi toolResult id ${id}`);
205
+ }
206
+ const joined: SourceToolReceipt[] = [];
207
+ for (const [id, call] of calls) {
208
+ const result = results.get(id);
209
+ if (result === undefined) {
210
+ joined.push({ ...call, text: "", status: "pending", isError: false });
211
+ continue;
212
+ }
213
+ if (result.toolName !== call.toolName)
214
+ throw new Error(`Pi tool name drift for ${id}: call=${call.toolName} result=${result.toolName}`);
215
+ joined.push({
216
+ ...call,
217
+ text: result.text,
218
+ status: result.isError ? "failed" : "completed",
219
+ isError: result.isError,
220
+ });
221
+ }
222
+ return joined;
223
+ }
224
+
225
+ export function codexThreadIsTerminal(thread: unknown): boolean {
226
+ if (typeof thread !== "object" || thread === null || !Array.isArray((thread as { turns?: unknown }).turns))
227
+ throw new Error("malformed Codex thread/read turns");
228
+ const turns = (thread as { turns: unknown[] }).turns;
229
+ if (turns.length === 0) return false;
230
+ for (const rawTurn of turns) {
231
+ if (typeof rawTurn !== "object" || rawTurn === null) throw new Error("malformed Codex thread/read turn");
232
+ if (!Array.isArray((rawTurn as { items?: unknown }).items)) throw new Error("malformed Codex thread/read items");
233
+ const status = (rawTurn as { status?: unknown }).status;
234
+ if (status === "inProgress") return false;
235
+ if (status === "failed" || status === "interrupted") throw new Error(`Codex turn ended ${status}, not completed`);
236
+ if (status !== "completed") throw new Error(`unknown Codex turn status ${String(status)}`);
237
+ }
238
+ return true;
239
+ }
240
+
241
+ /** Read Entwurf MCP calls from Codex thread/read, retaining vendor inProgress state. */
242
+ export function codexSourceToolReceipts(thread: unknown): SourceToolReceipt[] {
243
+ if (typeof thread !== "object" || thread === null) return [];
244
+ const turns = (thread as { turns?: unknown }).turns;
245
+ if (!Array.isArray(turns)) return [];
246
+ const found: SourceToolReceipt[] = [];
247
+ const ids = new Set<string>();
248
+ for (const rawTurn of turns) {
249
+ if (typeof rawTurn !== "object" || rawTurn === null) continue;
250
+ const items = (rawTurn as { items?: unknown }).items;
251
+ if (!Array.isArray(items)) continue;
252
+ for (const rawItem of items) {
253
+ if (typeof rawItem !== "object" || rawItem === null) continue;
254
+ const item = rawItem as {
255
+ type?: unknown;
256
+ id?: unknown;
257
+ server?: unknown;
258
+ tool?: unknown;
259
+ status?: unknown;
260
+ arguments?: unknown;
261
+ result?: unknown;
262
+ error?: unknown;
263
+ };
264
+ if (item.type !== "mcpToolCall" || item.server !== "entwurf-bridge") continue;
265
+ if (typeof item.id !== "string" || typeof item.tool !== "string")
266
+ throw new Error("malformed Codex entwurf MCP call identity");
267
+ if (ids.has(item.id)) throw new Error(`duplicate Codex MCP tool-call id ${item.id}`);
268
+ ids.add(item.id);
269
+ if (typeof item.arguments !== "object" || item.arguments === null || Array.isArray(item.arguments))
270
+ throw new Error(`malformed Codex MCP arguments for ${item.id}`);
271
+ const result = typeof item.result === "object" && item.result !== null ? item.result : null;
272
+ if (item.status === "inProgress") {
273
+ if (result !== null || item.error !== null)
274
+ throw new Error(`malformed inProgress Codex MCP state for ${item.id}`);
275
+ found.push({
276
+ toolName: item.tool,
277
+ arguments: item.arguments as Record<string, unknown>,
278
+ text: "",
279
+ status: "pending",
280
+ isError: false,
281
+ });
282
+ continue;
283
+ }
284
+ if (item.status !== "completed" && item.status !== "failed")
285
+ throw new Error(`unknown Codex MCP status ${String(item.status)} for ${item.id}`);
286
+ const completed = item.status === "completed" && item.error === null && result !== null;
287
+ found.push({
288
+ toolName: item.tool,
289
+ arguments: item.arguments as Record<string, unknown>,
290
+ text: textContent((result as { content?: unknown } | null)?.content),
291
+ status: completed ? "completed" : "failed",
292
+ isError: !completed,
293
+ });
294
+ }
295
+ }
296
+ return found;
297
+ }
298
+
299
+ function exactSenderInfoSuffix(text: string, nonce: string): string | null {
300
+ const prefix = `${nonce}\n\n<sender_info>`;
301
+ const suffix = "</sender_info>";
302
+ if (!text.startsWith(prefix) || !text.endsWith(suffix)) return null;
303
+ const json = text.slice(prefix.length, -suffix.length);
304
+ if (json.includes("\n") || json.includes("<sender_info>")) return null;
305
+ try {
306
+ const sender = JSON.parse(json) as { sessionId?: unknown };
307
+ return typeof sender.sessionId === "string" ? sender.sessionId : null;
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+
313
+ /** Exact callback in Pi's own transcript: nonce plus one terminal generated sender envelope. */
314
+ export function piCallbackSenders(entries: readonly unknown[], nonce: string): string[] {
315
+ const found: string[] = [];
316
+ for (const raw of entries) {
317
+ if (typeof raw !== "object" || raw === null) continue;
318
+ const entry = raw as { type?: unknown; customType?: unknown; content?: unknown };
319
+ if (entry.type !== "custom_message" || entry.customType !== "entwurf-message" || typeof entry.content !== "string")
320
+ continue;
321
+ const sender = exactSenderInfoSuffix(entry.content, nonce);
322
+ if (sender !== null) found.push(sender);
323
+ }
324
+ return found;
325
+ }
326
+
327
+ export interface MetaMailboxBody {
328
+ sender: string;
329
+ payload: string;
330
+ }
331
+
332
+ /** Strip exactly formatMetaMailboxBody's anchored outer frame and one terminal newline. */
333
+ export function parseMetaMailboxBody(text: string): MetaMailboxBody | null {
334
+ const frame =
335
+ /^\[entwurf received ⟵\]\n {2}from: {8}[^\n]*\n {2}session: {5}(\S+)(?: [^\n]*)?\n {2}at: {10}[^\n]+\n {2}wants reply: (?:yes|no)\n────────────────────────────────────────\n/;
336
+ const match = frame.exec(text);
337
+ if (match === null || !text.endsWith("\n")) return null;
338
+ return { sender: match[1] as string, payload: text.slice(match[0].length, -1) };
339
+ }
340
+
341
+ export interface FinalMailboxValidation {
342
+ accepted: boolean;
343
+ reason: string;
344
+ parsed: MetaMailboxBody | null;
345
+ }
346
+
347
+ export function validateFinalMailboxBody(
348
+ body: string,
349
+ expectedSender: string,
350
+ expectedPayload: string,
351
+ ): FinalMailboxValidation {
352
+ const parsed = parseMetaMailboxBody(body);
353
+ if (parsed === null) return { accepted: false, reason: "rejected: invalid outer frame", parsed: null };
354
+ if (parsed.sender !== expectedSender)
355
+ return { accepted: false, reason: `rejected: sender differs from ${expectedSender}`, parsed };
356
+ if (parsed.payload !== expectedPayload)
357
+ return { accepted: false, reason: "rejected: payload differs from source-call argument", parsed };
358
+ return { accepted: true, reason: "accepted: exact outer frame, sender, and source-call payload", parsed };
359
+ }
360
+
361
+ export interface MailboxCallback {
362
+ body: string;
363
+ sender: string;
364
+ }
365
+
366
+ /** Exact callback bodies read by the record-backed fixture's mailbox. */
367
+ export function mailboxCallbacks(bodies: readonly string[], nonce: string): MailboxCallback[] {
368
+ const found: MailboxCallback[] = [];
369
+ for (const body of bodies) {
370
+ const parsed = parseMetaMailboxBody(body);
371
+ if (parsed !== null && parsed.payload === nonce) found.push({ body, sender: parsed.sender });
372
+ }
373
+ return found;
374
+ }
375
+
376
+ /** Exact callback in a Codex thread/read userMessage item. */
377
+ export function codexCallbackSenders(thread: unknown, nonce: string): string[] {
378
+ if (typeof thread !== "object" || thread === null) return [];
379
+ const turns = (thread as { turns?: unknown }).turns;
380
+ if (!Array.isArray(turns)) return [];
381
+ const found: string[] = [];
382
+ for (const rawTurn of turns) {
383
+ if (typeof rawTurn !== "object" || rawTurn === null) continue;
384
+ const items = (rawTurn as { items?: unknown }).items;
385
+ if (!Array.isArray(items)) continue;
386
+ for (const rawItem of items) {
387
+ if (typeof rawItem !== "object" || rawItem === null || (rawItem as { type?: unknown }).type !== "userMessage")
388
+ continue;
389
+ const content = (rawItem as { content?: unknown }).content;
390
+ if (!Array.isArray(content) || content.length !== 1) continue;
391
+ const block = content[0];
392
+ if (typeof block !== "object" || block === null || typeof (block as { text?: unknown }).text !== "string")
393
+ continue;
394
+ const parsed = parseMetaMailboxBody((block as { text: string }).text);
395
+ if (parsed !== null && parsed.payload === nonce) found.push(parsed.sender);
396
+ }
397
+ }
398
+ return found;
399
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The one parser that turns a LAUNCH receipt back into the tmux window handle it named.
3
+ *
4
+ * It exists for ONE job: when a travelling receipt never arrives, the visible window a
5
+ * sibling opened would be orphaned, because cleanup only ever kills handles the smoke
6
+ * recorded. The handle is still written down — in the caller's own transcript and in the
7
+ * Codex thread's own tool-call result — so recovery reads it from there instead of
8
+ * scanning tmux inventory or guessing.
9
+ *
10
+ * WHY THIS IS LINE-ORIENTED AND NOT A CHARACTER WINDOW. `renderFreshCall` writes one block
11
+ * whose `backend:` and `window:` lines are separated by a VARIABLE number of optional lines
12
+ * (cwd, seat). A character bound between them is a guess about how long those lines are, and
13
+ * the guess was already nearly spent: `[측정]` a rendered receipt carrying both cwd and seat
14
+ * spans 344 escaped characters, against the 400-character bound this replaced. A longer
15
+ * project path would have silently recovered NOTHING. The boundary here is the receipt's own
16
+ * line structure, which does not have a length.
17
+ *
18
+ * Transcripts and tool results carry the receipt JSON-escaped, so a line ends at a real
19
+ * newline OR at a literal backslash-n. Both are separators.
20
+ *
21
+ * It stays anchored on the receipt: a `window:` line with no `backend:` line before it is
22
+ * not a window this run opened, and a receipt for the OTHER backend is never mistaken for
23
+ * this one's.
24
+ */
25
+ const LINE = /\r?\n|\\n/;
26
+ const BACKEND_LINE = /^\s*backend:\s+([a-z-]+)\b/;
27
+ const WINDOW_LINE = /^\s*window:\s+(@\d+)\b/;
28
+
29
+ export function launchReceiptWindows(text: string, backend: string): string[] {
30
+ const found: string[] = [];
31
+ let open: string | null = null;
32
+ for (const line of text.split(LINE)) {
33
+ const backendLine = BACKEND_LINE.exec(line);
34
+ if (backendLine !== null) {
35
+ open = backendLine[1] ?? null;
36
+ continue;
37
+ }
38
+ const windowLine = WINDOW_LINE.exec(line);
39
+ if (windowLine === null) continue;
40
+ if (open === backend) found.push(windowLine[1] as string);
41
+ // One receipt names one window: close the block so a later stray line cannot inherit
42
+ // this backend.
43
+ open = null;
44
+ }
45
+ return found;
46
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * tmux-coordinate-row — parse ONE positional TSV row out of `tmux display-message -p`.
3
+ *
4
+ * WHY THIS EXISTS, measured. The Codex first-admission LIVE asked tmux for four coordinates in
5
+ * one row and split the result with `.trim().split("\t")`. On tmux 3.6a `#{server_pid}` answers
6
+ * with an EMPTY string, so the row arrived as `"\t$150\t@342\t%342\n"` — and `.trim()` ate the
7
+ * leading tab along with the newline. Every field then shifted one place left: the session id
8
+ * was read as the server pid, the window id as the session id, and the pane id vanished. The
9
+ * gate failed, which was right, but it failed at the wrong coordinate and blamed the host for a
10
+ * field the format never filled.
11
+ *
12
+ * So two rules, and the second one is the reason this is a separate leaf:
13
+ *
14
+ * 1. Exactly ONE terminal newline is removed — `\n` or `\r\n`, and only at the end. Anything
15
+ * else (an embedded newline, a second trailing one) means the row is not the single line
16
+ * the caller asked for, and is refused rather than silently reshaped.
17
+ * 2. EMPTY FIELDS ARE KEPT, leading and trailing alike. A format key that answers with
18
+ * nothing must fail at its own position — "the server pid is empty" is a fact the caller
19
+ * can act on; "everything after it moved" is a fact nobody can.
20
+ *
21
+ * The field COUNT is the caller's contract, so it is passed in and enforced here: a row with
22
+ * more or fewer columns than the format asked for is malformed, never truncated to fit.
23
+ */
24
+
25
+ export class TmuxRowError extends Error {}
26
+
27
+ /**
28
+ * @param raw the exact stdout of a single `display-message -p` invocation
29
+ * @param fieldCount how many `\t`-separated fields the caller's format string asked for
30
+ */
31
+ export function parseTmuxRow(raw: string, fieldCount: number): string[] {
32
+ if (!Number.isInteger(fieldCount) || fieldCount < 1) {
33
+ throw new TmuxRowError(`fieldCount must be a positive integer, got ${String(fieldCount)}`);
34
+ }
35
+ let line = raw;
36
+ if (line.endsWith("\r\n")) line = line.slice(0, -2);
37
+ else if (line.endsWith("\n")) line = line.slice(0, -1);
38
+ if (line.includes("\n") || line.includes("\r")) {
39
+ throw new TmuxRowError(
40
+ `tmux answered with more than one line (${JSON.stringify(raw)}); a positional row must be exactly one`,
41
+ );
42
+ }
43
+ const fields = line.split("\t");
44
+ if (fields.length !== fieldCount) {
45
+ throw new TmuxRowError(
46
+ `tmux answered ${fields.length} field(s), the format asked for ${fieldCount} (${JSON.stringify(raw)})`,
47
+ );
48
+ }
49
+ return fields;
50
+ }
51
+
52
+ /**
53
+ * The server pid key that actually answers. `[host]` measured on tmux 3.6a: `#{pid}` returns
54
+ * 5772, the same pid the `$TMUX` tuple carries, while `#{server_pid}` returns an empty string.
55
+ * Pinned as one literal so the LIVE inspector and its deterministic oracle cannot drift apart.
56
+ */
57
+ export const TMUX_COORDINATE_FORMAT = "#{pid}\t#{session_id}\t#{window_id}\t#{pane_id}";
58
+ export const TMUX_COORDINATE_FIELDS = 4;
@@ -442,7 +442,12 @@ async function inspectNativePushCitizens(storeDir: string): Promise<QuiesceViola
442
442
  try {
443
443
  const probe = await resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId);
444
444
  status = probe.status;
445
- reason = probe.status === "alive" ? `route ${probe.route.lsAddress}` : probe.reason;
445
+ reason =
446
+ probe.status === "alive"
447
+ ? probe.route.backend === "antigravity"
448
+ ? `route ${probe.route.lsAddress}`
449
+ : `socket ${probe.route.socketPath}`
450
+ : probe.reason;
446
451
  } catch (err) {
447
452
  // A probe that cannot run is not a dead conversation.
448
453
  violations.push({