@minhspark/codex-mcp-bridge 1.26.9 → 1.26.10
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -0
- package/docs/daily-handoffs.md +18 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/scripts/update.mjs +2 -2
- package/src/antigravity-bridge.mjs +50 -10
- package/src/antigravity-deliveries.mjs +27 -2
- package/src/antigravity-desktop.mjs +206 -2
- package/src/antigravity-outbound-targets.mjs +1 -1
- package/src/antigravity-sidecar.mjs +5 -4
- package/src/background-codex-worker.mjs +26 -4
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/peer-protocol.mjs +116 -14
- package/src/platform.mjs +19 -18
- package/src/recipient-preflight.mjs +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.26.10] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Recover Claude replies whose native ancestry passes through a verified Monitor notification. Preserve outstanding background work and reject unrelated or malformed notifications. Pending-delivery diagnostics now retain the original message ID and direct callers to its receipt without suggesting a duplicate send.
|
|
12
|
+
- Observe Antigravity Desktop replies through a bounded, read-only native transcript adapter. New deliveries retain a pre-send boundary and prompt hash; completion requires the exact original message, execution, public final response and idle conversation. Unsupported schemas, interleaved messages and ambiguous matches remain unconfirmed. Reply reads retain sender, account and project isolation.
|
|
13
|
+
- Migrate pending Claude transcript observations during future worker updates only after a unique native message proves acceptance. Held and unobserved deliveries keep their locks and defer reload, preserving their original reply channel.
|
|
14
|
+
- Resolve Codex CLI candidates from the worker's actual environment, skip incomplete version directories and retry read-only inspection once if an unpinned executable disappears during an app update. Explicit executable selections, authentication failures and real jobs are never retried or silently redirected.
|
|
15
|
+
- Report background relay readiness separately from unverified Desktop connections after updating. A ready background worker does not establish a live native Desktop relay.
|
|
16
|
+
- Reject malformed transcript session identifiers before composing filesystem paths, and validate executable candidates before inspecting their file type. Configured external executable locations remain supported.
|
|
17
|
+
|
|
18
|
+
### Verification and limits
|
|
19
|
+
|
|
20
|
+
- Regression coverage includes Monitor ancestry, pending receipt ownership, reload safety, exact native reply correlation, account changes and CLI replacement races. Historical native transcripts confirmed both reply-observation failures without resending their prompts.
|
|
21
|
+
- An already-running older worker can still defer its own upgrade while a receipt is pending. Preserve that receipt and any verified historical result before reconnecting its MCP client; a missing receipt after reconnecting is not evidence of non-delivery. Legacy Antigravity receipts without the original account binding cannot disclose recovered reply text.
|
|
22
|
+
- A Desktop relay exiting on another machine still requires that machine's MCP host log and shutdown reason. This release does not infer an idle-chat cause or replace Desktop delivery with a CLI worker.
|
|
23
|
+
|
|
24
|
+
### Upgrade
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
npx.cmd -y @minhspark/codex-mcp-bridge@latest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For noninteractive Windows updates, use `codex-bridge-update --user`. Reconnect affected existing MCP clients after active work finishes, then check their status tools. The global installation path remains available with `npm install -g @minhspark/codex-mcp-bridge@latest --allow-scripts=@minhspark/codex-mcp-bridge --foreground-scripts`.
|
|
31
|
+
|
|
7
32
|
## [1.26.9] - 2026-11-03
|
|
8
33
|
|
|
9
34
|
### Fixed
|
package/docs/daily-handoffs.md
CHANGED
|
@@ -82,6 +82,12 @@ new task, account, process, prompt and directory to match.
|
|
|
82
82
|
turn. The exact dispatch must occur after the saved watermark, and commentary
|
|
83
83
|
alone is never a final reply. Receipt observation resolves that exact dispatch
|
|
84
84
|
even after newer turns start; it does not substitute the latest turn's answer.
|
|
85
|
+
For Antigravity Desktop, retain `requestId` and use
|
|
86
|
+
`read_antigravity_delivery`. `accepted` confirms only native acceptance;
|
|
87
|
+
`reply_received` requires an exact native message and execution with a public
|
|
88
|
+
final reply. Unsupported transcript formats or ambiguous interleaving keep
|
|
89
|
+
the delivery unconfirmed. Receipts without their original account binding
|
|
90
|
+
cannot disclose recovered reply text. None of these states authorizes a resend.
|
|
85
91
|
5. An `interrupted` receipt ends observation only: task outcome is unknown and
|
|
86
92
|
background work may continue. Inspect the existing task before a scoped
|
|
87
93
|
follow-up; do not repeat card creation or file operations.
|
|
@@ -113,6 +119,18 @@ defer hot reload. Save the original receipt and any verified historical reply
|
|
|
113
119
|
before reconnecting. An unknown ID after reconnecting is not evidence of
|
|
114
120
|
non-delivery. Verify recovery with one harmless message, not by resending work.
|
|
115
121
|
|
|
122
|
+
Updated workers can transfer a pending Claude transcript observation only when
|
|
123
|
+
the exact native message has already been observed. Held or unobserved deliveries
|
|
124
|
+
retain their original callback channel and continue to defer reload. Installing
|
|
125
|
+
new code cannot change the migration behavior of an already-running old worker.
|
|
126
|
+
|
|
127
|
+
Updater success verifies managed settings and the background relay separately
|
|
128
|
+
from Desktop connectivity. Check `codex_bridge_status`, `claude_bridge_status`
|
|
129
|
+
and `antigravity_bridge_status` in the existing clients. If a native companion
|
|
130
|
+
starts and then exits, retain its MCP host stderr and shutdown reason. A missing
|
|
131
|
+
relay pipe alone does not identify the cause; do not infer that an empty chat or
|
|
132
|
+
a dummy prompt is required to keep it alive.
|
|
133
|
+
|
|
116
134
|
Card revocation and communication revocation are independent. Archiving a
|
|
117
135
|
conversation does not guarantee worktree/branch removal. Check running work,
|
|
118
136
|
uncommitted files and unique commits before cleanup; preserve configuration,
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "codex-mcp-bridge",
|
|
4
|
-
"version": "1.26.
|
|
4
|
+
"version": "1.26.10",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
|
|
|
25
25
|
|
|
26
26
|
exitForVersionRequest(import.meta.url);
|
|
27
27
|
|
|
28
|
-
const VERSION = "1.26.
|
|
28
|
+
const VERSION = "1.26.10";
|
|
29
29
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
30
30
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
31
31
|
const entryArgs = ["native-relay-companion.mjs"];
|
package/scripts/update.mjs
CHANGED
|
@@ -220,7 +220,7 @@ export async function runUpdate({ args = [], env = process.env, platform = proce
|
|
|
220
220
|
if (setup?.configured !== true) throw failure("UPDATE_SETUP_INCOMPLETE", `The global bridge settings are incomplete. ${setupFailureSummary(setup) ?? SETUP_INSPECTION} ${RETRY}`, "verification");
|
|
221
221
|
onProgress("Verifying the owned relay runtime.");
|
|
222
222
|
const service = await awaitService(globalRoot, false, "verification");
|
|
223
|
-
return { updated: true, configured: true, version, previousVersion, migrated, service, opensApps: false, opensChats: false };
|
|
223
|
+
return { updated: true, configured: true, version, previousVersion, migrated, service, desktopConnectionsVerified: false, opensApps: false, opensChats: false };
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
export async function runUpdateCli(args = process.argv.slice(2)) {
|
|
@@ -228,7 +228,7 @@ export async function runUpdateCli(args = process.argv.slice(2)) {
|
|
|
228
228
|
process.chdir(neutralDirectory([ROOT], process.env));
|
|
229
229
|
const result = await runUpdate({ args, onProgress: message => process.stderr.write(`${message}\n`) });
|
|
230
230
|
process.stdout.write(result.help ? `${result.help}\n` : args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n`
|
|
231
|
-
: `Bridge ${result.version} updated; managed settings verified. Background relay ${result.service.ready ? "ready" : "remains intentionally disabled"}.\n`);
|
|
231
|
+
: `Bridge ${result.version} updated; managed settings verified. Background relay ${result.service.ready ? "ready" : "remains intentionally disabled"}.\nDesktop connections have not been verified. Check codex_bridge_status, claude_bridge_status and antigravity_bridge_status in the existing apps.\n`);
|
|
232
232
|
} catch (error) {
|
|
233
233
|
const options = { component: "bridge-update", operation: "update", deliveryState: "not_applicable" };
|
|
234
234
|
if (args.includes("--json")) process.stdout.write(JSON.stringify({ updated: false, pending: error.pending === true, diagnostic: bridgeDiagnostic(error, options) }, null, 2) + "\n");
|
|
@@ -7,9 +7,11 @@ import { z } from "zod";
|
|
|
7
7
|
import { resolveCodexSenderContext } from "./codex-sender-context.mjs";
|
|
8
8
|
import { readClaudeSenderContext, requireClaudeSenderContext, stopProcessInspectors } from "./claude-sender-context.mjs";
|
|
9
9
|
import { readClaudeAccountContext } from "./desktop-account-context.mjs";
|
|
10
|
+
import { readCodexAccountContext } from "./codex-account-context.mjs";
|
|
10
11
|
import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
11
12
|
import { antigravityBridgeDirectory, callAntigravityDesktop } from "./antigravity-ipc.mjs";
|
|
12
13
|
import { AntigravityDeliveries, antigravityDeliveryError } from "./antigravity-deliveries.mjs";
|
|
14
|
+
import { observeAntigravityDelivery } from "./antigravity-desktop.mjs";
|
|
13
15
|
import { AntigravityCliSession } from "./antigravity-cli.mjs";
|
|
14
16
|
import { exitForVersionRequest } from "./cli-version.mjs";
|
|
15
17
|
import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
|
|
@@ -45,6 +47,7 @@ const reload = createReloadControl({
|
|
|
45
47
|
|
|
46
48
|
async function identifySender(extra) {
|
|
47
49
|
if (caller === "codex") {
|
|
50
|
+
const account = readCodexAccountContext();
|
|
48
51
|
const context = await resolveCodexSenderContext(extra?._meta, { inspectDesktopTask: async (threadId) => {
|
|
49
52
|
const accounts = requireBridgeAccounts(readBridgeAccounts());
|
|
50
53
|
const { result } = await senderRelay.requestDesktop("read_thread", { threadId, hostId: "local", turnLimit: 1, includeOutputs: false, maxOutputCharsPerItem: 1 }, { accountContext: accounts, beforeSend: () => assertAccountIdentity(accounts) });
|
|
@@ -52,11 +55,16 @@ async function identifySender(extra) {
|
|
|
52
55
|
return { thread: result?.thread, latestTurnId: result?.turns?.[0]?.id ?? null };
|
|
53
56
|
} });
|
|
54
57
|
if (context.status !== "verified") throw new Error(context.reason ?? "Unverified Codex sender");
|
|
55
|
-
|
|
58
|
+
const current = readCodexAccountContext();
|
|
59
|
+
if (account.status !== "verified" || current.status !== "verified" || !/^[a-f0-9]{64}$/.test(account.fingerprint ?? "")) throw new Error("The calling Codex account is not verified");
|
|
60
|
+
if (current.fingerprint !== account.fingerprint) throw new Error("The calling Codex account changed during sender verification");
|
|
61
|
+
return { provider: "codex", id: context.threadId, cwd: context.cwd, accountFingerprint: account.fingerprint };
|
|
56
62
|
}
|
|
57
63
|
if (caller === "claude") {
|
|
58
64
|
const context = requireClaudeSenderContext(await readClaudeSenderContext({ account: readClaudeAccountContext() }));
|
|
59
|
-
|
|
65
|
+
const current = readClaudeAccountContext();
|
|
66
|
+
if (current.status !== "verified" || !/^[a-f0-9]{64}$/.test(context.accountFingerprint ?? "") || current.fingerprint !== context.accountFingerprint) throw new Error("The calling Claude account changed during sender verification");
|
|
67
|
+
return { provider: "claude", id: context.sessionId, cwd: context.cwd, accountFingerprint: context.accountFingerprint };
|
|
60
68
|
}
|
|
61
69
|
throw new Error("Install this bridge for Codex or Claude; unknown callers cannot read conversations or send work");
|
|
62
70
|
}
|
|
@@ -89,10 +97,25 @@ function sameCwd(expected, actual) {
|
|
|
89
97
|
|
|
90
98
|
function owns(record, identity) {
|
|
91
99
|
if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw preflightFailure("ANTIGRAVITY_RECEIPT_OWNER_MISMATCH", "Delivery does not belong to this sending conversation");
|
|
100
|
+
if (Object.hasOwn(record.sender, "accountFingerprint") && record.sender.accountFingerprint !== identity.accountFingerprint) throw preflightFailure("ANTIGRAVITY_RECEIPT_ACCOUNT_MISMATCH", "Delivery does not belong to the currently signed-in sending account");
|
|
92
101
|
sameCwd(identity.cwd, record.cwd);
|
|
93
102
|
return record;
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
function sameSender(left, right) {
|
|
106
|
+
return left?.provider === right?.provider && left?.id === right?.id && left?.accountFingerprint === right?.accountFingerprint;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function accountUnboundReceipt(record) {
|
|
110
|
+
const recovered = record.status === "reply_received" || record.result?.status === "reply_received" || record.replyObservation?.status === "reply_received";
|
|
111
|
+
const status = recovered ? "unknown" : record.status;
|
|
112
|
+
const { response, reply, ...originalResult } = record.result ?? {};
|
|
113
|
+
return { ...record, status,
|
|
114
|
+
result: recovered ? { conversationId: record.conversationId, surface: "desktop", status, completed: false } : originalResult,
|
|
115
|
+
replyObservation: { status: "unavailable", code: "ANTIGRAVITY_RECEIPT_ACCOUNT_UNBOUND", source: "native-transcript", retrySafe: false,
|
|
116
|
+
reason: "The original delivery did not record its sending account. Recovered reply text is withheld; retain the original receipt and do not resend." } };
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
function handoff(prompt, identity) {
|
|
97
120
|
return `[Agent handoff from ${identity.provider} conversation ${identity.id}. Native Antigravity permissions remain in effect.]\n\n${prompt}`;
|
|
98
121
|
}
|
|
@@ -146,7 +169,7 @@ register("list_antigravity_conversations", "List native Desktop conversations an
|
|
|
146
169
|
const identity = await sender(extra);
|
|
147
170
|
const records = surface === "desktop" ? await callAntigravityDesktop({ operation: "list" }) : [...sessions.values()].map((entry) => ({ ...entry.session.read(), sender: entry.sender }));
|
|
148
171
|
return records.filter((record) => {
|
|
149
|
-
try { sameCwd(identity.cwd, record.cwd); return surface !== "cli" || record.sender
|
|
172
|
+
try { sameCwd(identity.cwd, record.cwd); return surface !== "cli" || sameSender(record.sender, identity); }
|
|
150
173
|
catch { return false; }
|
|
151
174
|
});
|
|
152
175
|
});
|
|
@@ -156,14 +179,26 @@ register("send_to_antigravity_conversation", "Send one authorized prompt to an e
|
|
|
156
179
|
const cwd = sameCwd(identity.cwd, args.cwd);
|
|
157
180
|
const prompt = handoff(args.prompt, identity);
|
|
158
181
|
if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw preflightFailure("ANTIGRAVITY_PROMPT_INVALID", "Desktop handoff must fit within 30000 characters and contain no NUL");
|
|
159
|
-
if (args.surface === "desktop")
|
|
182
|
+
if (args.surface === "desktop") {
|
|
183
|
+
const receipt = await callAntigravityDesktop({ operation: "send", ...args, cwd, prompt, sender: identity });
|
|
184
|
+
try {
|
|
185
|
+
const current = await sender(extra);
|
|
186
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed while inspecting the native delivery receipt");
|
|
187
|
+
owns(receipt, current);
|
|
188
|
+
return Object.hasOwn(receipt.sender, "accountFingerprint") ? receipt : accountUnboundReceipt(receipt);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
error.phase = "receipt";
|
|
191
|
+
error.deliveryState = "unknown";
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
160
195
|
const entry = sessions.get(args.conversationId);
|
|
161
|
-
if (!entry || entry.sender
|
|
196
|
+
if (!entry || !sameSender(entry.sender, identity)) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender and bridge process");
|
|
162
197
|
sameCwd(cwd, entry.session.read().cwd);
|
|
163
198
|
return deliveries.dispatch({ ...args, cwd, sender: identity, prompt }, async () => {
|
|
164
199
|
const current = await sender(extra);
|
|
165
200
|
sameCwd(cwd, current.cwd);
|
|
166
|
-
if (current
|
|
201
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
167
202
|
return entry.session.send(prompt);
|
|
168
203
|
}, { component: "antigravity-bridge", operation: "send_to_antigravity_conversation" });
|
|
169
204
|
});
|
|
@@ -188,7 +223,7 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
188
223
|
reserved = false;
|
|
189
224
|
const current = await sender(extra);
|
|
190
225
|
sameCwd(cwd, current.cwd);
|
|
191
|
-
if (current
|
|
226
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
192
227
|
sending = true;
|
|
193
228
|
return await session.send(handoff(args.prompt, identity));
|
|
194
229
|
} catch (error) {
|
|
@@ -208,16 +243,21 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
208
243
|
}, { component: "antigravity-bridge", operation: "start_antigravity_cli_session" });
|
|
209
244
|
});
|
|
210
245
|
|
|
211
|
-
register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means
|
|
246
|
+
register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means native acceptance; reply_received requires a final reply correlated to the original message and execution.", { surface: z.enum(["desktop", "cli"]), requestId: z.string().uuid() }, true, async (args, extra) => {
|
|
212
247
|
const identity = await sender(extra);
|
|
213
248
|
const record = args.surface === "desktop" ? await callAntigravityDesktop({ operation: "receipt", requestId: args.requestId }) : deliveries.read(args.requestId);
|
|
214
|
-
|
|
249
|
+
owns(record, identity);
|
|
250
|
+
if (args.surface !== "desktop") return record;
|
|
251
|
+
const observed = Object.hasOwn(record.sender, "accountFingerprint") ? await observeAntigravityDelivery(record) : accountUnboundReceipt(record);
|
|
252
|
+
const current = await sender(extra);
|
|
253
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed while inspecting the original delivery");
|
|
254
|
+
return owns(observed, current);
|
|
215
255
|
});
|
|
216
256
|
|
|
217
257
|
register("close_antigravity_cli_session", "Close an idle bridge-owned CLI session belonging to the caller.", { conversationId: z.string().uuid() }, false, async ({ conversationId }, extra) => {
|
|
218
258
|
const identity = await sender(extra);
|
|
219
259
|
const entry = sessions.get(conversationId);
|
|
220
|
-
if (!entry || entry.sender
|
|
260
|
+
if (!entry || !sameSender(entry.sender, identity)) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender");
|
|
221
261
|
sameCwd(identity.cwd, entry.session.read().cwd);
|
|
222
262
|
if (["running", "starting"].includes(entry.session.read().status)) throw preflightFailure("ANTIGRAVITY_SESSION_BUSY", "Wait for the active CLI delivery before closing");
|
|
223
263
|
const closed = await entry.session.close();
|
|
@@ -64,6 +64,29 @@ export class AntigravityDeliveries {
|
|
|
64
64
|
this.write(record);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
captureObservation(requestId, observation) {
|
|
68
|
+
if (!this.active.has(requestId)) throw new Error("Cannot prepare inactive delivery observation");
|
|
69
|
+
if (observation?.version !== 1 || !["ready", "unavailable"].includes(observation.status)
|
|
70
|
+
|| !/^[a-f0-9]{64}$/.test(observation.promptSha256 ?? "") || typeof observation.projectId !== "string"
|
|
71
|
+
|| observation.status === "ready" && (!UUID.test(observation.trajectoryId ?? "") || !Number.isSafeInteger(observation.beforeStepIndex) || observation.beforeStepIndex < -1)) throw new Error("Invalid native observation boundary");
|
|
72
|
+
const record = this.read(requestId);
|
|
73
|
+
record.nativeObservation = { version: 1, status: observation.status, promptSha256: observation.promptSha256, projectId: observation.projectId,
|
|
74
|
+
...(observation.status === "ready" ? { trajectoryId: observation.trajectoryId, beforeStepIndex: observation.beforeStepIndex } : { code: "ANTIGRAVITY_TRANSCRIPT_UNAVAILABLE" }) };
|
|
75
|
+
this.write(record);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async refresh(requestId, observe) {
|
|
79
|
+
const record = this.read(requestId);
|
|
80
|
+
if (!record || this.active.has(requestId)) return record;
|
|
81
|
+
const refreshed = await observe(record);
|
|
82
|
+
if (!refreshed || refreshed.requestId !== record.requestId || refreshed.digest !== record.digest || refreshed.cwd !== record.cwd
|
|
83
|
+
|| refreshed.conversationId !== record.conversationId || refreshed.surface !== record.surface || JSON.stringify(refreshed.sender) !== JSON.stringify(record.sender)) throw new Error("Observation changed the delivery identity");
|
|
84
|
+
const current = this.read(requestId);
|
|
85
|
+
if (this.active.has(requestId) || JSON.stringify(current) !== JSON.stringify(record)) return current;
|
|
86
|
+
if (refreshed.status === "reply_received") this.write(refreshed);
|
|
87
|
+
return refreshed;
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
write(record) {
|
|
68
91
|
const temporary = `${this.file(record.requestId)}.${process.pid}.tmp`;
|
|
69
92
|
fs.writeFileSync(temporary, JSON.stringify(record), { flag: "wx", mode: 0o600 });
|
|
@@ -91,7 +114,7 @@ export class AntigravityDeliveries {
|
|
|
91
114
|
}
|
|
92
115
|
this.active.add(requestId);
|
|
93
116
|
void Promise.resolve().then(operation).then((result) => {
|
|
94
|
-
record.status = ["accepted", "sent_unconfirmed", "held", "refused"].includes(result?.status) ? result.status : result?.status && result.status !== "SUCCESS" ? "failed" : "completed";
|
|
117
|
+
record.status = ["accepted", "sent_unconfirmed", "held", "refused", "reply_received"].includes(result?.status) ? result.status : result?.status && result.status !== "SUCCESS" ? "failed" : "completed";
|
|
95
118
|
record.result = result;
|
|
96
119
|
record.conversationId = result?.conversationId ?? result?.conversation_id ?? record.conversationId;
|
|
97
120
|
if (record.status === "failed") {
|
|
@@ -107,7 +130,9 @@ export class AntigravityDeliveries {
|
|
|
107
130
|
}).finally(() => {
|
|
108
131
|
record.finishedAt = new Date().toISOString();
|
|
109
132
|
try {
|
|
110
|
-
|
|
133
|
+
const previous = this.read(requestId);
|
|
134
|
+
record.conversationId ??= previous?.conversationId;
|
|
135
|
+
record.nativeObservation ??= previous?.nativeObservation;
|
|
111
136
|
this.write(record);
|
|
112
137
|
} catch (error) { process.stderr.write(`Could not persist Antigravity delivery outcome: ${error.code ?? "WRITE_FAILED"}\n`); }
|
|
113
138
|
this.active.delete(requestId);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -8,6 +9,207 @@ import { promisify } from "node:util";
|
|
|
8
9
|
const execute = promisify(execFile);
|
|
9
10
|
const SURFACES = { desktop: "antigravity", cli: "antigravity-cli" };
|
|
10
11
|
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
12
|
+
const TRANSCRIPT_ROW_LIMIT = 4096;
|
|
13
|
+
const TRANSCRIPT_BYTE_LIMIT = 32 * 1024 * 1024;
|
|
14
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
15
|
+
const RESTART_NOTICE = "[Notice] All your subagents and background tasks have been stopped due to server restart. If you want a subagent to continue working, it needs to be revived by sending it a new message. If resuming work, please check on status and restart as needed.";
|
|
16
|
+
|
|
17
|
+
function transcriptFailure(code = "ANTIGRAVITY_TRANSCRIPT_UNSUPPORTED") {
|
|
18
|
+
return Object.assign(new Error("Native Antigravity reply observation is unavailable; retain the original receipt and do not resend."), { code });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function protobufFields(value) {
|
|
22
|
+
if (!(value instanceof Uint8Array) || value.length > 2 * 1024 * 1024) throw transcriptFailure();
|
|
23
|
+
const buffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
24
|
+
const fields = [];
|
|
25
|
+
let offset = 0;
|
|
26
|
+
const integer = () => {
|
|
27
|
+
let result = 0n;
|
|
28
|
+
for (let shift = 0n; shift < 70n; shift += 7n) {
|
|
29
|
+
if (offset >= buffer.length) throw transcriptFailure();
|
|
30
|
+
const byte = buffer[offset++];
|
|
31
|
+
result |= BigInt(byte & 127) << shift;
|
|
32
|
+
if (!(byte & 128)) {
|
|
33
|
+
if (result > BigInt(Number.MAX_SAFE_INTEGER)) throw transcriptFailure();
|
|
34
|
+
return Number(result);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw transcriptFailure();
|
|
38
|
+
};
|
|
39
|
+
while (offset < buffer.length) {
|
|
40
|
+
if (fields.length >= 4096) throw transcriptFailure();
|
|
41
|
+
const tag = integer();
|
|
42
|
+
const field = Math.floor(tag / 8);
|
|
43
|
+
const wire = tag % 8;
|
|
44
|
+
if (!field || field > 536870911) throw transcriptFailure();
|
|
45
|
+
if (wire === 0) fields.push({ field, wire, value: integer() });
|
|
46
|
+
else {
|
|
47
|
+
const length = wire === 2 ? integer() : wire === 1 ? 8 : wire === 5 ? 4 : -1;
|
|
48
|
+
if (length < 0 || length > buffer.length - offset) throw transcriptFailure();
|
|
49
|
+
fields.push({ field, wire, value: buffer.subarray(offset, offset + length) });
|
|
50
|
+
offset += length;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return fields;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function protobufField(fields, number, wire) {
|
|
57
|
+
const matches = fields.filter((field) => field.field === number);
|
|
58
|
+
if (matches.length > 1 || matches.length && matches[0].wire !== wire) throw transcriptFailure();
|
|
59
|
+
return matches[0]?.value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function protobufText(fields, number) {
|
|
63
|
+
const value = protobufField(fields, number, 2);
|
|
64
|
+
return value === undefined ? "" : utf8.decode(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function protobufTime(fields, number) {
|
|
68
|
+
const value = protobufField(fields, number, 2);
|
|
69
|
+
if (!value) return null;
|
|
70
|
+
const timestamp = protobufFields(value);
|
|
71
|
+
const seconds = protobufField(timestamp, 1, 0) ?? 0;
|
|
72
|
+
const nanos = protobufField(timestamp, 2, 0) ?? 0;
|
|
73
|
+
if (nanos >= 1e9 || seconds > 253402300799) throw transcriptFailure();
|
|
74
|
+
return seconds * 1000 + nanos / 1e6;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decodeNativeStep(row) {
|
|
78
|
+
if (!Number.isSafeInteger(row.idx) || row.idx < 0 || row.step_format !== 0) throw transcriptFailure();
|
|
79
|
+
const fields = protobufFields(row.step_payload);
|
|
80
|
+
if (protobufField(fields, 1, 0) !== row.step_type || protobufField(fields, 4, 0) !== row.status) throw transcriptFailure();
|
|
81
|
+
const rawMetadata = protobufField(fields, 5, 2);
|
|
82
|
+
if (!rawMetadata || !Buffer.from(row.metadata ?? []).equals(rawMetadata)) throw transcriptFailure();
|
|
83
|
+
const metadata = protobufFields(rawMetadata);
|
|
84
|
+
const step = { index: row.idx, type: row.step_type, status: row.status, executionId: protobufText(metadata, 12), createdAt: protobufTime(metadata, 1), completedAt: protobufTime(metadata, 8) };
|
|
85
|
+
if (step.type === 101) {
|
|
86
|
+
const system = protobufFields(protobufField(fields, 114, 2));
|
|
87
|
+
const rawAgent = protobufField(system, 4, 2);
|
|
88
|
+
if (rawAgent) {
|
|
89
|
+
const agent = protobufFields(rawAgent);
|
|
90
|
+
step.message = { id: protobufText(agent, 1), recipient: protobufText(agent, 2), sender: protobufText(agent, 3), content: protobufText(agent, 4), hidden: protobufField(agent, 11, 0) === 1 };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (step.type === 15) {
|
|
94
|
+
const response = protobufFields(protobufField(fields, 20, 2));
|
|
95
|
+
step.response = protobufText(response, 8) || protobufText(response, 1);
|
|
96
|
+
step.hasTools = response.some((field) => field.field === 7);
|
|
97
|
+
step.stopReason = protobufField(response, 12, 0);
|
|
98
|
+
step.finishedAt = protobufTime(metadata, 7);
|
|
99
|
+
}
|
|
100
|
+
return step;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readNativeSteps({ home, conversationId, boundaryOnly = false, afterStepIndex = -1, findRoot }) {
|
|
104
|
+
if (!UUID.test(conversationId ?? "")) throw transcriptFailure();
|
|
105
|
+
let directory = fs.realpathSync.native(home);
|
|
106
|
+
for (const segment of [".gemini", "antigravity", "conversations"]) {
|
|
107
|
+
directory = path.join(directory, segment);
|
|
108
|
+
const stat = fs.lstatSync(directory);
|
|
109
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw transcriptFailure();
|
|
110
|
+
}
|
|
111
|
+
const filename = path.join(directory, `${conversationId}.db`);
|
|
112
|
+
const before = fs.lstatSync(filename, { bigint: true });
|
|
113
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > 512n * 1024n * 1024n) throw transcriptFailure();
|
|
114
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
115
|
+
try {
|
|
116
|
+
const stat = fs.lstatSync(`${filename}${suffix}`);
|
|
117
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) throw transcriptFailure();
|
|
118
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
119
|
+
}
|
|
120
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
121
|
+
const db = new DatabaseSync(filename, { readOnly: true });
|
|
122
|
+
try {
|
|
123
|
+
db.exec("PRAGMA query_only=ON; BEGIN");
|
|
124
|
+
const trajectory = db.prepare("SELECT trajectory_id, cascade_id FROM trajectory_meta LIMIT 2").all();
|
|
125
|
+
if (trajectory.length !== 1 || trajectory[0].cascade_id !== conversationId || !UUID.test(trajectory[0].trajectory_id ?? "")) throw transcriptFailure();
|
|
126
|
+
const maximum = db.prepare("SELECT MAX(idx) AS lastStepIndex FROM steps").get().lastStepIndex ?? -1;
|
|
127
|
+
if (!Number.isSafeInteger(maximum) || maximum < -1) throw transcriptFailure();
|
|
128
|
+
const readSteps = (lowerBound, rootsOnly = false) => {
|
|
129
|
+
const selector = rootsOnly ? "step_type = 101" : "idx > ?";
|
|
130
|
+
const parameters = rootsOnly ? [] : [lowerBound];
|
|
131
|
+
const rows = db.prepare(`SELECT idx, step_type, status, step_format, length(metadata) AS metadata_bytes, length(step_payload) AS payload_bytes FROM steps WHERE ${selector} ORDER BY idx LIMIT ?`).all(...parameters, TRANSCRIPT_ROW_LIMIT + 1);
|
|
132
|
+
if (rows.length > TRANSCRIPT_ROW_LIMIT || rows.some((row) => row.metadata_bytes > 2 * 1024 * 1024 || row.payload_bytes > 2 * 1024 * 1024)
|
|
133
|
+
|| rows.reduce((sum, row) => sum + row.metadata_bytes + row.payload_bytes, 0) > TRANSCRIPT_BYTE_LIMIT) throw transcriptFailure("ANTIGRAVITY_TRANSCRIPT_LIMIT");
|
|
134
|
+
return rows.length ? db.prepare(`SELECT idx, step_type, status, step_format, metadata, step_payload FROM steps WHERE idx >= ? AND idx <= ?${rootsOnly ? " AND step_type = 101" : ""} ORDER BY idx`).all(rows[0].idx, rows.at(-1).idx).map(decodeNativeStep) : [];
|
|
135
|
+
};
|
|
136
|
+
let steps = [];
|
|
137
|
+
let roots;
|
|
138
|
+
if (!boundaryOnly) {
|
|
139
|
+
if (findRoot) {
|
|
140
|
+
roots = readSteps(-1, true).filter(findRoot);
|
|
141
|
+
if (roots.length === 1) steps = readSteps(roots[0].index - 1);
|
|
142
|
+
} else steps = readSteps(afterStepIndex);
|
|
143
|
+
}
|
|
144
|
+
const after = fs.lstatSync(filename, { bigint: true });
|
|
145
|
+
if (!after.isFile() || after.isSymbolicLink() || after.nlink !== 1n || before.dev !== after.dev || before.ino !== after.ino || before.birthtimeNs !== after.birthtimeNs) throw transcriptFailure();
|
|
146
|
+
return { trajectoryId: trajectory[0].trajectory_id, lastStepIndex: maximum, steps, ...(roots ? { roots } : {}) };
|
|
147
|
+
} finally { db.close(); }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function captureAntigravityObservation({ home = os.homedir(), conversationId, prompt, projectId }) {
|
|
151
|
+
const promptSha256 = crypto.createHash("sha256").update(prompt).digest("hex");
|
|
152
|
+
try {
|
|
153
|
+
const snapshot = await readNativeSteps({ home, conversationId, boundaryOnly: true });
|
|
154
|
+
return { version: 1, status: "ready", trajectoryId: snapshot.trajectoryId, beforeStepIndex: snapshot.lastStepIndex, promptSha256, projectId };
|
|
155
|
+
} catch {
|
|
156
|
+
return { version: 1, status: "unavailable", code: "ANTIGRAVITY_TRANSCRIPT_UNAVAILABLE", promptSha256, projectId };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function observeAntigravityDelivery(record, { home = os.homedir(), list = listAntigravityConversations } = {}) {
|
|
161
|
+
if (record?.surface !== "desktop" || !["accepted", "unknown", "sent_unconfirmed"].includes(record.status)) return record;
|
|
162
|
+
const observation = (status, code) => ({ ...record, replyObservation: { status, code, source: "native-transcript", retrySafe: false } });
|
|
163
|
+
try {
|
|
164
|
+
if (!UUID.test(record.conversationId ?? "") || !/^[a-f0-9]{64}$/.test(record.digest ?? "") || !["codex", "claude"].includes(record.sender?.provider)
|
|
165
|
+
|| typeof record.sender.id !== "string" || !record.sender.id) throw transcriptFailure();
|
|
166
|
+
const matches = (await list({ home, surface: "desktop", limit: 1000 })).filter((row) => row.conversationId === record.conversationId);
|
|
167
|
+
if (matches.length !== 1 || matches[0].surface !== "desktop" || canonicalCwd(matches[0].cwd) !== canonicalCwd(record.cwd)) throw transcriptFailure("ANTIGRAVITY_REPLY_SCOPE_CHANGED");
|
|
168
|
+
const boundary = record.nativeObservation;
|
|
169
|
+
if (boundary && (boundary.version !== 1 || !["ready", "unavailable"].includes(boundary.status) || !/^[a-f0-9]{64}$/.test(boundary.promptSha256 ?? "") || boundary.projectId !== matches[0].projectId)) throw transcriptFailure();
|
|
170
|
+
if (boundary?.status === "ready" && (!Number.isSafeInteger(boundary.beforeStepIndex) || boundary.beforeStepIndex < -1 || !UUID.test(boundary.trajectoryId ?? ""))) throw transcriptFailure();
|
|
171
|
+
const createdAt = Date.parse(record.createdAt);
|
|
172
|
+
const finishedAt = Date.parse(record.finishedAt);
|
|
173
|
+
if (!Number.isFinite(createdAt) || boundary?.status !== "ready" && !Number.isFinite(finishedAt)) throw transcriptFailure();
|
|
174
|
+
const matchesRoot = (step) => {
|
|
175
|
+
if (step.type !== 101 || !step.message || step.status !== 3 || !UUID.test(step.message.id) || step.message.recipient !== record.conversationId
|
|
176
|
+
|| step.createdAt === null || step.createdAt < createdAt - (boundary?.status === "ready" ? 2000 : 0) || !step.executionId) return false;
|
|
177
|
+
if (boundary?.status !== "ready" && step.createdAt > Math.min(createdAt + 120000, finishedAt + 30000)) return false;
|
|
178
|
+
const prompt = step.message.content;
|
|
179
|
+
if (boundary?.promptSha256 && crypto.createHash("sha256").update(prompt).digest("hex") !== boundary.promptSha256) return false;
|
|
180
|
+
const digest = crypto.createHash("sha256").update(JSON.stringify({ surface: "desktop", conversationId: record.conversationId, cwd: record.cwd, sender: record.sender, prompt })).digest("hex");
|
|
181
|
+
return digest === record.digest;
|
|
182
|
+
};
|
|
183
|
+
const snapshot = await readNativeSteps({ home, conversationId: record.conversationId, ...(boundary?.status === "ready" ? { afterStepIndex: boundary.beforeStepIndex } : { findRoot: matchesRoot }) });
|
|
184
|
+
if (boundary?.status === "ready" && (boundary.trajectoryId !== snapshot.trajectoryId || snapshot.lastStepIndex < boundary.beforeStepIndex)) throw transcriptFailure("ANTIGRAVITY_TRANSCRIPT_REPLACED");
|
|
185
|
+
const roots = snapshot.roots ?? snapshot.steps.filter(matchesRoot);
|
|
186
|
+
if (!roots.length) return observation("pending", "ANTIGRAVITY_MESSAGE_NOT_OBSERVED");
|
|
187
|
+
if (roots.length !== 1) return observation("ambiguous", "ANTIGRAVITY_REPLY_AMBIGUOUS");
|
|
188
|
+
const root = roots[0];
|
|
189
|
+
let reply;
|
|
190
|
+
for (const step of snapshot.steps.filter((step) => step.index > root.index)) {
|
|
191
|
+
if (step.type === 14 || step.executionId !== root.executionId) break;
|
|
192
|
+
if (step.type === 101) {
|
|
193
|
+
if (step.message?.hidden && step.message.sender === "system" && step.message.content === RESTART_NOTICE) continue;
|
|
194
|
+
if (!reply) return observation("ambiguous", "ANTIGRAVITY_REPLY_INTERLEAVED");
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
reply = step.type === 15 && step.status === 3 && step.response && !step.hasTools && step.stopReason === 2
|
|
198
|
+
&& step.finishedAt !== null && step.completedAt !== null && step.finishedAt >= root.createdAt && step.completedAt >= step.finishedAt ? step : null;
|
|
199
|
+
}
|
|
200
|
+
if (!reply) return observation("pending", "ANTIGRAVITY_REPLY_NOT_OBSERVED");
|
|
201
|
+
const current = (await list({ home, surface: "desktop", limit: 1000 })).filter((row) => row.conversationId === record.conversationId);
|
|
202
|
+
if (current.length !== 1 || current[0].surface !== "desktop" || current[0].projectId !== matches[0].projectId || canonicalCwd(current[0].cwd) !== canonicalCwd(record.cwd)) throw transcriptFailure("ANTIGRAVITY_REPLY_SCOPE_CHANGED");
|
|
203
|
+
if (current[0].status !== "CASCADE_RUN_STATUS_IDLE") return observation("pending", "ANTIGRAVITY_REPLY_STILL_RUNNING");
|
|
204
|
+
if (Buffer.byteLength(reply.response) > 256 * 1024) throw transcriptFailure("ANTIGRAVITY_REPLY_LIMIT");
|
|
205
|
+
const evidence = { source: "native-transcript", nativeMessageId: root.message.id, rootStepIndex: root.index, replyStepIndex: reply.index,
|
|
206
|
+
executionId: root.executionId, trajectoryId: snapshot.trajectoryId, replySha256: crypto.createHash("sha256").update(reply.response).digest("hex") };
|
|
207
|
+
return { ...record, status: "reply_received", replyObservation: { status: "reply_received", retrySafe: false, ...evidence },
|
|
208
|
+
result: { ...record.result, status: "reply_received", completed: true, response: reply.response, reply: reply.response, ...evidence } };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return observation("unavailable", /^ANTIGRAVITY_(?:TRANSCRIPT|REPLY)_[A-Z_]+$/.test(error.code ?? "") ? error.code : "ANTIGRAVITY_TRANSCRIPT_UNAVAILABLE");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
11
213
|
|
|
12
214
|
function failure(code, message) {
|
|
13
215
|
return Object.assign(new Error(message), { code, deliveryState: "not_sent" });
|
|
@@ -105,7 +307,7 @@ export async function runAgentApi(options) {
|
|
|
105
307
|
}
|
|
106
308
|
}
|
|
107
309
|
|
|
108
|
-
async function executeAgentApi({ command, conversationId, prompt, expectedCwd, home = os.homedir(), env = process.env, executable = env.ANTIGRAVITY_AGENTAPI_EXE, run = execute, list = listAntigravityConversations }) {
|
|
310
|
+
async function executeAgentApi({ command, conversationId, prompt, expectedCwd, home = os.homedir(), env = process.env, executable = env.ANTIGRAVITY_AGENTAPI_EXE, run = execute, list = listAntigravityConversations, beforeDispatch }) {
|
|
109
311
|
if (!UUID.test(conversationId)) throw failure("ANTIGRAVITY_ID_INVALID", "An exact conversation UUID is required");
|
|
110
312
|
if (!["metadata", "send"].includes(command)) throw new Error("Unsupported Antigravity agentapi command");
|
|
111
313
|
if (!env.ANTIGRAVITY_LS_ADDRESS || !env.ANTIGRAVITY_CSRF_TOKEN) throw failure("ANTIGRAVITY_SIDECAR_REQUIRED", "Antigravity must launch the bridge sidecar before native requests are available");
|
|
@@ -141,6 +343,8 @@ async function executeAgentApi({ command, conversationId, prompt, expectedCwd, h
|
|
|
141
343
|
if (metadata.cwd !== cwd || metadata.projectId !== before.projectId) throw failure("ANTIGRAVITY_METADATA_MISMATCH", "Native metadata does not match the selected conversation workspace and project");
|
|
142
344
|
const current = await select();
|
|
143
345
|
if (current.projectId !== before.projectId) throw failure("ANTIGRAVITY_METADATA_MISMATCH", "Conversation project changed during delivery preflight");
|
|
346
|
+
const nativeObservation = await captureAntigravityObservation({ home, conversationId, prompt, projectId: current.projectId });
|
|
347
|
+
await beforeDispatch?.(nativeObservation);
|
|
144
348
|
await invoke(["send-message", conversationId, prompt]);
|
|
145
|
-
return { conversationId, status: "accepted", completed: false, surface: "desktop" };
|
|
349
|
+
return { conversationId, status: "accepted", completed: false, surface: "desktop", nativeObservation };
|
|
146
350
|
}
|
|
@@ -236,7 +236,7 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
236
236
|
await context.check();
|
|
237
237
|
return { target, sessionId: recipient.sessionId, taskId, cwd: context.cwd, msgId, status: outcome.delivery?.status ?? "sent_unconfirmed", completed: false, accountContext: context.accountContext };
|
|
238
238
|
} catch (error) {
|
|
239
|
-
if (error.msgId) receipts.set(error.msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
|
|
239
|
+
if (error.msgId && error.code !== "PEER_REPLY_PENDING") receipts.set(error.msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
|
|
240
240
|
if (msgId) error.msgId = msgId;
|
|
241
241
|
if (msgId || error.deliveryUncertain) error.deliveryState = "unknown";
|
|
242
242
|
else if (!error.deliveryState) error.deliveryState = "not_sent";
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { antigravityBridgeDirectory, serveAntigravityDesktop } from "./antigravity-ipc.mjs";
|
|
5
5
|
import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
|
|
6
|
-
import { listAntigravityConversations, runAgentApi, sanitizeAntigravityMetadata } from "./antigravity-desktop.mjs";
|
|
6
|
+
import { listAntigravityConversations, observeAntigravityDelivery, runAgentApi, sanitizeAntigravityMetadata } from "./antigravity-desktop.mjs";
|
|
7
7
|
import { exitForVersionRequest } from "./cli-version.mjs";
|
|
8
8
|
import { readProcessAncestry } from "./claude-sender-context.mjs";
|
|
9
9
|
|
|
@@ -34,7 +34,7 @@ export async function createAntigravityParentGuard({ parentPid = process.ppid, i
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export async function startAntigravitySidecar({ directory = antigravityBridgeDirectory(), env = process.env, createParentGuard = createAntigravityParentGuard, serve = serveAntigravityDesktop, parentCheckIntervalMs = 5000, onParentLost = () => {} } = {}) {
|
|
37
|
+
export async function startAntigravitySidecar({ directory = antigravityBridgeDirectory(), env = process.env, createParentGuard = createAntigravityParentGuard, serve = serveAntigravityDesktop, parentCheckIntervalMs = 5000, onParentLost = () => {}, runNative = runAgentApi, observeDelivery = observeAntigravityDelivery } = {}) {
|
|
38
38
|
if (!env.ANTIGRAVITY_AGENTAPI_EXE || !env.ANTIGRAVITY_LS_ADDRESS) throw new Error("Antigravity must launch this sidecar. Do not run it as a replacement app server.");
|
|
39
39
|
const assertParent = await createParentGuard();
|
|
40
40
|
const deliveries = new AntigravityDeliveries(path.join(directory, "desktop-deliveries"));
|
|
@@ -68,7 +68,7 @@ export async function startAntigravitySidecar({ directory = antigravityBridgeDir
|
|
|
68
68
|
const native = sanitizeAntigravityMetadata(await runAgentApi({ command: "metadata", conversationId: request.conversationId, env }), request.conversationId);
|
|
69
69
|
return { ...record, ...native };
|
|
70
70
|
}
|
|
71
|
-
if (request.operation === "receipt") return deliveries.
|
|
71
|
+
if (request.operation === "receipt") return deliveries.refresh(request.requestId, observeDelivery);
|
|
72
72
|
if (request.operation !== "send" || typeof request.prompt !== "string" || !request.prompt.trim() || request.prompt.length > 30000 || request.prompt.includes("\0")
|
|
73
73
|
|| typeof request.cwd !== "string" || !path.isAbsolute(request.cwd) || !request.sender || !["codex", "claude"].includes(request.sender.provider)
|
|
74
74
|
|| typeof request.sender.id !== "string" || !request.sender.id) throw new Error("Invalid Antigravity delivery request");
|
|
@@ -76,7 +76,8 @@ export async function startAntigravitySidecar({ directory = antigravityBridgeDir
|
|
|
76
76
|
await checkParent();
|
|
77
77
|
if (sending.has(request.conversationId)) throw Object.assign(new Error("Another delivery is pending for this conversation"), { deliveryState: "not_sent" });
|
|
78
78
|
sending.add(request.conversationId);
|
|
79
|
-
try { return await
|
|
79
|
+
try { return await runNative({ command: "send", conversationId: request.conversationId, expectedCwd: request.cwd, prompt: request.prompt, env,
|
|
80
|
+
beforeDispatch: (observation) => deliveries.captureObservation(request.requestId, observation) }); }
|
|
80
81
|
finally { sending.delete(request.conversationId); }
|
|
81
82
|
});
|
|
82
83
|
}, { directory });
|
|
@@ -61,22 +61,29 @@ function workerEnv(env, relayEnv = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function binary(executable, env, platform = process.platform) {
|
|
64
|
-
const selected = executable ?? resolveCodexBin(undefined, { env, platform });
|
|
64
|
+
const selected = executable ?? env.CODEX_BIN ?? env.CODEX_CLI_PATH ?? resolveCodexBin(undefined, { env, platform });
|
|
65
65
|
try {
|
|
66
66
|
if (typeof selected !== "string" || !path.isAbsolute(selected) || /[\u0000-\u001f\u007f]/.test(selected)) throw new Error();
|
|
67
67
|
const resolved = fs.realpathSync.native(selected);
|
|
68
68
|
if (!fs.statSync(resolved).isFile() || platform === "win32" && !/\.(?:exe|com)$/i.test(resolved)) throw new Error();
|
|
69
69
|
fs.accessSync(resolved, platform === "win32" ? fs.constants.R_OK : fs.constants.X_OK);
|
|
70
70
|
return resolved;
|
|
71
|
-
} catch { throw failure("CLI_UNAVAILABLE", "inspect"); }
|
|
71
|
+
} catch (cause) { throw Object.assign(failure("CLI_UNAVAILABLE", "inspect"), { cause }); }
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
function missingExecutable(error) {
|
|
75
|
+
return ["ENOENT", "ENOTDIR"].includes(error?.code) || ["ENOENT", "ENOTDIR"].includes(error?.cause?.code);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function inspectCodexWorkerAttempt({ env, executable, platform, run, retryMissing = false }) {
|
|
75
79
|
const result = { agent: "codex", available: false, authenticated: null, ready: false };
|
|
76
80
|
if (!object(env) || typeof run !== "function") return { ...result, code: "INVALID_INPUT", reason: ERRORS.INVALID_INPUT };
|
|
77
81
|
let selected;
|
|
78
82
|
try { selected = binary(executable, env, platform); }
|
|
79
|
-
catch (error) {
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (retryMissing && missingExecutable(error)) throw error;
|
|
85
|
+
return { ...result, code: error.code, reason: error.message };
|
|
86
|
+
}
|
|
80
87
|
const options = { env: workerEnv(env), windowsHide: true, shell: false, timeout: 10000, maxBuffer: MAX_CAPTURE, encoding: "utf8" };
|
|
81
88
|
try {
|
|
82
89
|
const version = await run(selected, ["--version"], options);
|
|
@@ -88,6 +95,8 @@ export async function inspectCodexWorker({ env = process.env, executable, platfo
|
|
|
88
95
|
return { ...result, code: "CLI_UNSUPPORTED", reason: ERRORS.CLI_UNSUPPORTED };
|
|
89
96
|
}
|
|
90
97
|
} catch (error) {
|
|
98
|
+
if (retryMissing && missingExecutable(error)) throw error;
|
|
99
|
+
if (missingExecutable(error)) return { ...result, available: false, code: "CLI_UNAVAILABLE", reason: ERRORS.CLI_UNAVAILABLE };
|
|
91
100
|
const code = diagnosticCode(error.stderr, result.available ? "CLI_UNSUPPORTED" : "CLI_UNAVAILABLE");
|
|
92
101
|
return { ...result, code, reason: ERRORS[code] };
|
|
93
102
|
}
|
|
@@ -100,11 +109,24 @@ export async function inspectCodexWorker({ env = process.env, executable, platfo
|
|
|
100
109
|
const code = diagnosticCode(text, "AUTH_UNVERIFIED");
|
|
101
110
|
return { ...result, authenticated: code === "AUTH_REQUIRED" ? false : null, code, reason: ERRORS[code] };
|
|
102
111
|
} catch (error) {
|
|
112
|
+
if (retryMissing && missingExecutable(error)) throw error;
|
|
113
|
+
if (missingExecutable(error)) return { ...result, available: false, code: "CLI_UNAVAILABLE", reason: ERRORS.CLI_UNAVAILABLE };
|
|
103
114
|
const code = diagnosticCode(`${error.stdout ?? ""}\n${error.stderr ?? ""}`, "AUTH_UNVERIFIED");
|
|
104
115
|
return { ...result, authenticated: code === "AUTH_REQUIRED" ? false : null, code, reason: ERRORS[code] };
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
|
|
119
|
+
export async function inspectCodexWorker({ env = process.env, executable, platform = process.platform, run = execFileAsync } = {}) {
|
|
120
|
+
const settings = { env, executable, platform, run };
|
|
121
|
+
const pinned = executable != null || env?.CODEX_BIN != null || env?.CODEX_CLI_PATH != null;
|
|
122
|
+
try { return await inspectCodexWorkerAttempt({ ...settings, retryMissing: !pinned }); }
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!missingExecutable(error)) throw error;
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
126
|
+
return inspectCodexWorkerAttempt(settings);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
108
130
|
function capture(executable, args, { cwd, env, spawn, signal, timeoutMs }) {
|
|
109
131
|
return new Promise((resolve, reject) => {
|
|
110
132
|
if (signal?.aborted) return reject(failure("ABORTED", "configure"));
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
|
|
|
31
31
|
|
|
32
32
|
exitForVersionRequest(import.meta.url);
|
|
33
33
|
|
|
34
|
-
const VERSION = "1.26.
|
|
34
|
+
const VERSION = "1.26.10";
|
|
35
35
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
36
36
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
37
37
|
const FORWARD_MAX_PER_SESSION = 50;
|
package/src/index.mjs
CHANGED
|
@@ -44,7 +44,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
|
|
|
44
44
|
|
|
45
45
|
exitForVersionRequest(import.meta.url);
|
|
46
46
|
|
|
47
|
-
const VERSION = "1.26.
|
|
47
|
+
const VERSION = "1.26.10";
|
|
48
48
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
49
49
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
50
50
|
|
|
@@ -43,7 +43,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
|
|
|
43
43
|
|
|
44
44
|
exitForVersionRequest(import.meta.url);
|
|
45
45
|
|
|
46
|
-
const VERSION = "1.26.
|
|
46
|
+
const VERSION = "1.26.10";
|
|
47
47
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
48
48
|
|
|
49
49
|
function errorResponse(code, message, sent, details) {
|
package/src/peer-protocol.mjs
CHANGED
|
@@ -284,6 +284,7 @@ export function assertClaudeSessionProcess(session) {
|
|
|
284
284
|
* rather than by reconstructing the slug.
|
|
285
285
|
*/
|
|
286
286
|
function findTranscriptFile(sessionId, cwd) {
|
|
287
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 128 || /[^a-zA-Z0-9_-]/.test(sessionId)) return null;
|
|
287
288
|
const dir = projectsDir();
|
|
288
289
|
const guess = path.join(dir, String(cwd ?? "").replace(/[^a-zA-Z0-9]/g, "-"), `${sessionId}.jsonl`);
|
|
289
290
|
if (fs.existsSync(guess)) return guess;
|
|
@@ -297,7 +298,7 @@ function findTranscriptFile(sessionId, cwd) {
|
|
|
297
298
|
|
|
298
299
|
export function readTranscript(sessionId, cwd, limit = 10) {
|
|
299
300
|
const file = findTranscriptFile(sessionId, cwd);
|
|
300
|
-
if (!fs.existsSync(file)) return { file, messages: [] };
|
|
301
|
+
if (!file || !fs.existsSync(file)) return { file, messages: [] };
|
|
301
302
|
const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
302
303
|
const messages = [];
|
|
303
304
|
for (const line of lines) {
|
|
@@ -325,7 +326,7 @@ export function readTranscript(sessionId, cwd, limit = 10) {
|
|
|
325
326
|
|
|
326
327
|
export function readInitialTranscriptMessage(sessionId, cwd) {
|
|
327
328
|
const file = findTranscriptFile(sessionId, cwd);
|
|
328
|
-
if (!fs.existsSync(file)) return { file, messages: [] };
|
|
329
|
+
if (!file || !fs.existsSync(file)) return { file, messages: [] };
|
|
329
330
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
330
331
|
try {
|
|
331
332
|
const stat = fs.fstatSync(descriptor);
|
|
@@ -388,6 +389,46 @@ function isTerminalDelivery(receipt) {
|
|
|
388
389
|
return ["refused", "denied", "expired", "dropped"].includes(receipt?.status) || isInterruptedDelivery(receipt);
|
|
389
390
|
}
|
|
390
391
|
|
|
392
|
+
function resumableTranscriptDelivery(id, sent, receipt) {
|
|
393
|
+
const uuid = value => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
394
|
+
const session = sent?.transcriptSession;
|
|
395
|
+
return Boolean(sent) && uuid(id) && !sent.reply && !sent.failed && !isTerminalDelivery(receipt)
|
|
396
|
+
&& Number.isFinite(sent.sentAt) && (!receipt || receipt.status === "delivered" && receipt.fromSocket === sent.targetSocket)
|
|
397
|
+
&& session?.entrypoint === "claude-desktop" && uuid(session.sessionId)
|
|
398
|
+
&& typeof session.cwd === "string" && path.isAbsolute(session.cwd)
|
|
399
|
+
&& typeof sent.targetSocket === "string" && sent.targetSocket && session.socket === sent.targetSocket
|
|
400
|
+
&& session.desktop?.status === "matched" && typeof session.desktop.taskId === "string"
|
|
401
|
+
&& session.desktop.taskId.startsWith("local_") && uuid(session.desktop.taskId.slice(6))
|
|
402
|
+
&& session.desktop.cwd === session.cwd
|
|
403
|
+
&& /^[a-f0-9]{64}$/i.test(sent.accountContext?.claude ?? "") && /^[a-f0-9]{64}$/i.test(sent.accountContext?.codex ?? "")
|
|
404
|
+
&& session.desktop.accountFingerprint === sent.accountContext.claude
|
|
405
|
+
&& hasTranscriptDeliveryRoot(session.sessionId, session.cwd, id);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function hasTranscriptDeliveryRoot(sessionId, cwd, msgId) {
|
|
409
|
+
const file = findTranscriptFile(sessionId, cwd);
|
|
410
|
+
if (!file) return false;
|
|
411
|
+
let lines;
|
|
412
|
+
try { lines = fs.readFileSync(file, "utf8").split("\n"); }
|
|
413
|
+
catch { return false; }
|
|
414
|
+
let roots = 0;
|
|
415
|
+
const ids = new Set();
|
|
416
|
+
for (const line of lines) {
|
|
417
|
+
if (!line.trim()) continue;
|
|
418
|
+
let entry;
|
|
419
|
+
try { entry = JSON.parse(line); } catch { return false; }
|
|
420
|
+
if (!entry || typeof entry !== "object" || entry.isSidechain) continue;
|
|
421
|
+
if (typeof entry.uuid === "string") {
|
|
422
|
+
if (ids.has(entry.uuid)) return false;
|
|
423
|
+
ids.add(entry.uuid);
|
|
424
|
+
}
|
|
425
|
+
if (!injectedMessageRoot(entry, msgId)) continue;
|
|
426
|
+
if (entry.entrypoint !== "claude-desktop" || entry.sessionId !== sessionId || entry.cwd !== cwd) return false;
|
|
427
|
+
roots++;
|
|
428
|
+
}
|
|
429
|
+
return roots === 1;
|
|
430
|
+
}
|
|
431
|
+
|
|
391
432
|
export function readTranscriptReply(sessionId, cwd, msgId) {
|
|
392
433
|
return readTranscriptOutcome(sessionId, cwd, msgId)?.reply ?? null;
|
|
393
434
|
}
|
|
@@ -460,6 +501,7 @@ function compactContinuation(entry, root, entriesById, transcriptIds, sessionId,
|
|
|
460
501
|
// user turn's answer to the original request or claim its side effects stopped.
|
|
461
502
|
export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
462
503
|
const file = findTranscriptFile(sessionId, cwd);
|
|
504
|
+
if (!file) return null;
|
|
463
505
|
let lines;
|
|
464
506
|
try { lines = fs.readFileSync(file, "utf8").split("\n"); }
|
|
465
507
|
catch { return null; }
|
|
@@ -509,6 +551,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
509
551
|
}
|
|
510
552
|
}
|
|
511
553
|
const descendants = new Map();
|
|
554
|
+
const monitorTasksByEntry = new Map();
|
|
512
555
|
const backgroundCalls = new Map();
|
|
513
556
|
const openLeaves = new Set();
|
|
514
557
|
const passiveLeaves = new Set();
|
|
@@ -520,7 +563,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
520
563
|
interruptions.push({ entryId: entry.uuid, backgroundTaskIds: [...outstanding.keys()] });
|
|
521
564
|
};
|
|
522
565
|
for (const entry of orderedEntries) {
|
|
523
|
-
if (entry === root) { descendants.set(entry.uuid, new Map()); openLeaves.add(entry.uuid); continue; }
|
|
566
|
+
if (entry === root) { descendants.set(entry.uuid, new Map()); monitorTasksByEntry.set(entry.uuid, new Set()); openLeaves.add(entry.uuid); continue; }
|
|
524
567
|
let parentUuid = entry.parentUuid;
|
|
525
568
|
if (!ambiguous && !interruptions.length && descendants.has(entry.logicalParentUuid)
|
|
526
569
|
&& openLeaves.has(entry.logicalParentUuid)) {
|
|
@@ -549,6 +592,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
549
592
|
const role = entry.message?.role;
|
|
550
593
|
const content = entry.message?.content;
|
|
551
594
|
const outstanding = new Map(descendants.get(parentUuid));
|
|
595
|
+
const monitorTasks = new Set(monitorTasksByEntry.get(parentUuid));
|
|
552
596
|
const toolResults = Array.isArray(content) && content.length > 0 && content.every((part) => part?.type === "tool_result");
|
|
553
597
|
const imageCompanion = isImageToolCompanion(entry, entriesById);
|
|
554
598
|
if (role === "user" && !toolResults && !imageCompanion && !compactSummaries.has(entry.uuid)) {
|
|
@@ -575,13 +619,20 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
575
619
|
}
|
|
576
620
|
if (typeof taskNotification === "string" && /^<task-notification>\s[\s\S]*<\/task-notification>$/.test(taskNotification.trim())) {
|
|
577
621
|
const field = (name) => {
|
|
622
|
+
if (taskNotification.split(`<${name}>`).length !== 2 || taskNotification.split(`</${name}>`).length !== 2) return null;
|
|
578
623
|
const matches = [...taskNotification.matchAll(new RegExp(`<${name}>([^<>]+)</${name}>`, "g"))];
|
|
579
624
|
return matches.length === 1 ? matches[0][1] : null;
|
|
580
625
|
};
|
|
581
626
|
const taskId = field("task-id");
|
|
627
|
+
const hasField = name => taskNotification.split(`<${name}>`).length === 2
|
|
628
|
+
&& taskNotification.split(`</${name}>`).length === 2
|
|
629
|
+
&& new RegExp(`<${name}>[\\s\\S]+?</${name}>`).test(taskNotification);
|
|
630
|
+
const monitorProgress = monitorTasks.has(taskId) && !outstanding.has(taskId)
|
|
631
|
+
&& hasField("summary") && hasField("event")
|
|
632
|
+
&& !/<\/?(?:status|tool-use-id)(?:\s|>)/.test(taskNotification);
|
|
582
633
|
if (!outstanding.has(taskId) || outstanding.get(taskId) !== field("tool-use-id")
|
|
583
634
|
|| !["completed", "failed", "killed"].includes(field("status"))) {
|
|
584
|
-
if (role === "user") continue;
|
|
635
|
+
if (role === "user" && !monitorProgress) continue;
|
|
585
636
|
} else outstanding.delete(taskId);
|
|
586
637
|
}
|
|
587
638
|
if (typeof entry.uuid !== "string" || !entry.uuid.trim()) continue;
|
|
@@ -600,6 +651,18 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
600
651
|
// Parallel tool results and hook records can be leaf siblings of the real
|
|
601
652
|
// continuation. They are completed observations, not separate reply turns.
|
|
602
653
|
const parentEntry = entriesById.get(entry.parentUuid);
|
|
654
|
+
const monitorTaskId = entry.toolUseResult?.taskId;
|
|
655
|
+
if (role === "user" && toolResults && content.length === 1 && typeof monitorTaskId === "string" && /^[\w-]{1,128}$/.test(monitorTaskId)
|
|
656
|
+
&& entry.sourceToolAssistantUUID === parentEntry?.uuid
|
|
657
|
+
&& parentEntry?.message?.role === "assistant"
|
|
658
|
+
&& Number.isSafeInteger(entry.toolUseResult.timeoutMs) && entry.toolUseResult.timeoutMs > 0
|
|
659
|
+
&& typeof entry.toolUseResult.persistent === "boolean") {
|
|
660
|
+
const matches = content.filter(part => !part.is_error && typeof part.tool_use_id === "string" && part.tool_use_id
|
|
661
|
+
&& Array.isArray(parentEntry.message.content)
|
|
662
|
+
&& parentEntry.message.content.filter(call => call.type === "tool_use" && call.name === "Monitor" && call.id === part.tool_use_id).length === 1);
|
|
663
|
+
if (matches.length === 1) monitorTasks.add(monitorTaskId);
|
|
664
|
+
}
|
|
665
|
+
monitorTasksByEntry.set(entry.uuid, monitorTasks);
|
|
603
666
|
const matchedResults = toolResults && !entry.toolUseResult?.backgroundTaskId
|
|
604
667
|
&& content.every((part) => Array.isArray(parentEntry?.message?.content)
|
|
605
668
|
&& parentEntry.message.content.some((call) => call.type === "tool_use" && call.id === part.tool_use_id));
|
|
@@ -654,6 +717,7 @@ export class PeerEndpoint {
|
|
|
654
717
|
this.sentMessages = new Map();
|
|
655
718
|
this.pendingMessages = new Map();
|
|
656
719
|
this.responsePoll = null;
|
|
720
|
+
this.transcriptPollingPaused = false;
|
|
657
721
|
this.connections = new Set();
|
|
658
722
|
this.activityRevision = 0;
|
|
659
723
|
this.reloadPaused = false;
|
|
@@ -779,6 +843,8 @@ export class PeerEndpoint {
|
|
|
779
843
|
}
|
|
780
844
|
|
|
781
845
|
this.started = true;
|
|
846
|
+
this.transcriptPollingPaused = false;
|
|
847
|
+
this.#startTranscriptPolling();
|
|
782
848
|
this.log(`peer "${this.name}" listening on ${this.socketPath}`);
|
|
783
849
|
}
|
|
784
850
|
|
|
@@ -903,11 +969,20 @@ export class PeerEndpoint {
|
|
|
903
969
|
}
|
|
904
970
|
}
|
|
905
971
|
|
|
972
|
+
#startTranscriptPolling() {
|
|
973
|
+
if (this.reloadPaused || this.transcriptPollingPaused || this.responsePoll
|
|
974
|
+
|| ![...this.pendingMessages.values()].some(entry => entry.transcriptSession)) return;
|
|
975
|
+
this.responsePoll = globalThis.setInterval(() => this.#refreshTranscriptReplies(), 250);
|
|
976
|
+
this.responsePoll.unref();
|
|
977
|
+
}
|
|
978
|
+
|
|
906
979
|
#refreshTranscriptReplies() {
|
|
980
|
+
if (this.reloadPaused || this.transcriptPollingPaused) return;
|
|
907
981
|
for (const [msgId, pending] of this.pendingMessages) {
|
|
908
982
|
if (!pending.transcriptSession) continue;
|
|
909
983
|
const { sessionId, cwd } = pending.transcriptSession;
|
|
910
984
|
const file = findTranscriptFile(sessionId, cwd);
|
|
985
|
+
if (!file) continue;
|
|
911
986
|
let stamp;
|
|
912
987
|
try { const stat = fs.statSync(file, { bigint: true }); stamp = `${stat.dev}:${stat.ino}:${stat.mtimeNs}:${stat.ctimeNs}:${stat.size}`; }
|
|
913
988
|
catch { continue; }
|
|
@@ -1051,6 +1126,7 @@ export class PeerEndpoint {
|
|
|
1051
1126
|
);
|
|
1052
1127
|
error.code = "PEER_REPLY_PENDING";
|
|
1053
1128
|
error.sent = false;
|
|
1129
|
+
error.msgId = [...this.pendingMessages].find(([, entry]) => entry.targetSocket === targetSocket)?.[0];
|
|
1054
1130
|
throw error;
|
|
1055
1131
|
}
|
|
1056
1132
|
const since = Date.now();
|
|
@@ -1059,10 +1135,7 @@ export class PeerEndpoint {
|
|
|
1059
1135
|
let msgId = crypto.randomUUID();
|
|
1060
1136
|
this.pendingMessages.set(msgId, { targetSocket, transcriptSession });
|
|
1061
1137
|
this.sentMessages.set(msgId, { targetSocket, transcriptSession, sentAt: since, replyThreadId, permissionMode, senderApprovalPolicy, ...(senderReview ? { senderReview: { ...senderReview } } : {}), ...(recipient ? { recipient: { ...recipient } } : {}), ...(accountContext ? { accountContext: Object.freeze({ ...accountContext }) } : {}), ...(scopeBindings ? { scopeBindings: Object.freeze(structuredClone(scopeBindings)) } : {}) });
|
|
1062
|
-
|
|
1063
|
-
this.responsePoll = globalThis.setInterval(() => this.#refreshTranscriptReplies(), 250);
|
|
1064
|
-
this.responsePoll.unref();
|
|
1065
|
-
}
|
|
1138
|
+
this.#startTranscriptPolling();
|
|
1066
1139
|
try {
|
|
1067
1140
|
const sentId = await this.send(targetSocket, text, { priority, msgId, permissionMode, beforeSend });
|
|
1068
1141
|
if (sentId !== msgId) {
|
|
@@ -1191,9 +1264,21 @@ export class PeerEndpoint {
|
|
|
1191
1264
|
reloadReason() {
|
|
1192
1265
|
this.#refreshTranscriptReplies();
|
|
1193
1266
|
if (this.connections.size) return "Peer socket connections are still open";
|
|
1194
|
-
if (this.requestQueues.size
|
|
1267
|
+
if (this.requestQueues.size) return "Claude messages are still being sent";
|
|
1268
|
+
const counts = new Map();
|
|
1269
|
+
for (const [id, pending] of this.pendingMessages) {
|
|
1270
|
+
const sent = this.sentMessages.get(id);
|
|
1271
|
+
if (!resumableTranscriptDelivery(id, sent, this.deliveryReceipts.get(id))
|
|
1272
|
+
|| pending.targetSocket !== sent.targetSocket || JSON.stringify(pending.transcriptSession) !== JSON.stringify(sent.transcriptSession)) {
|
|
1273
|
+
return "Claude messages still have pending or unconfirmed delivery";
|
|
1274
|
+
}
|
|
1275
|
+
counts.set(sent.targetSocket, (counts.get(sent.targetSocket) ?? 0) + 1);
|
|
1276
|
+
}
|
|
1277
|
+
if (counts.size !== this.unconfirmedReplies.size || [...counts].some(([socket, count]) => this.unconfirmedReplies.get(socket) !== count)) {
|
|
1278
|
+
return "Claude messages still have pending or unconfirmed delivery";
|
|
1279
|
+
}
|
|
1195
1280
|
for (const [id, sent] of this.sentMessages) {
|
|
1196
|
-
if (!sent.reply && !sent.failed && !isTerminalDelivery(this.deliveryReceipts.get(id))) {
|
|
1281
|
+
if (!sent.reply && !sent.failed && !isTerminalDelivery(this.deliveryReceipts.get(id)) && !this.pendingMessages.has(id)) {
|
|
1197
1282
|
return "A Claude delivery outcome remains unconfirmed";
|
|
1198
1283
|
}
|
|
1199
1284
|
}
|
|
@@ -1243,14 +1328,17 @@ export class PeerEndpoint {
|
|
|
1243
1328
|
const reason = this.reloadReason();
|
|
1244
1329
|
if (reason || this.started && !this.reloadPaused) throw new Error(reason ?? "Quiesce the peer endpoint before exporting state");
|
|
1245
1330
|
return cloneReloadState({ name: this.name, messageSequence: this.messageSequence, inbox: this.inbox,
|
|
1246
|
-
sentMessages: [...this.sentMessages], deliveryReceipts: [...this.deliveryReceipts]
|
|
1331
|
+
sentMessages: [...this.sentMessages], deliveryReceipts: [...this.deliveryReceipts],
|
|
1332
|
+
pendingMessages: [...this.pendingMessages].map(([id, pending]) => [id, { targetSocket: pending.targetSocket, transcriptSession: pending.transcriptSession }]) });
|
|
1247
1333
|
}
|
|
1248
1334
|
|
|
1249
1335
|
restoreReloadState(input) {
|
|
1250
|
-
if (this.started || this.connections.size || this.sentMessages.size || this.inbox.length
|
|
1336
|
+
if (this.started || this.connections.size || this.sentMessages.size || this.inbox.length || this.pendingMessages.size
|
|
1337
|
+
|| this.unconfirmedReplies.size || this.requestQueues.size || this.responsePoll) throw new Error("Peer reload state requires an empty dormant endpoint");
|
|
1251
1338
|
const state = cloneReloadState(input);
|
|
1252
1339
|
if (!state || typeof state.name !== "string" || !state.name || !Number.isSafeInteger(state.messageSequence) || state.messageSequence < 0
|
|
1253
|
-
|| !Array.isArray(state.inbox) || !Array.isArray(state.sentMessages) || !Array.isArray(state.deliveryReceipts)
|
|
1340
|
+
|| !Array.isArray(state.inbox) || !Array.isArray(state.sentMessages) || !Array.isArray(state.deliveryReceipts)
|
|
1341
|
+
|| state.pendingMessages !== undefined && !Array.isArray(state.pendingMessages)) throw new Error("Invalid peer reload state");
|
|
1254
1342
|
const restoreMap = (entries) => {
|
|
1255
1343
|
const restored = new Map();
|
|
1256
1344
|
for (const pair of entries) {
|
|
@@ -1262,6 +1350,17 @@ export class PeerEndpoint {
|
|
|
1262
1350
|
};
|
|
1263
1351
|
const sentMessages = restoreMap(state.sentMessages);
|
|
1264
1352
|
const deliveryReceipts = restoreMap(state.deliveryReceipts);
|
|
1353
|
+
const pendingMessages = restoreMap(state.pendingMessages ?? []);
|
|
1354
|
+
const unconfirmedReplies = new Map();
|
|
1355
|
+
for (const [id, pending] of pendingMessages) {
|
|
1356
|
+
const sent = sentMessages.get(id);
|
|
1357
|
+
if (!resumableTranscriptDelivery(id, sent, deliveryReceipts.get(id))
|
|
1358
|
+
|| pending.targetSocket !== sent.targetSocket || JSON.stringify(pending.transcriptSession) !== JSON.stringify(sent.transcriptSession)) {
|
|
1359
|
+
throw new Error("Invalid pending Claude transcript reload record");
|
|
1360
|
+
}
|
|
1361
|
+
delete pending.transcriptStamp;
|
|
1362
|
+
unconfirmedReplies.set(sent.targetSocket, (unconfirmedReplies.get(sent.targetSocket) ?? 0) + 1);
|
|
1363
|
+
}
|
|
1265
1364
|
for (const record of state.inbox) {
|
|
1266
1365
|
if (!record || typeof record.text !== "string" || !Number.isSafeInteger(record.sequence) || record.sequence < 1
|
|
1267
1366
|
|| record.sequence > state.messageSequence || !Number.isFinite(record.receivedAt)) throw new Error("Invalid inbox reload record");
|
|
@@ -1269,7 +1368,7 @@ export class PeerEndpoint {
|
|
|
1269
1368
|
for (const [id, sent] of sentMessages) {
|
|
1270
1369
|
if (typeof sent.targetSocket !== "string" || !sent.targetSocket || !Number.isFinite(sent.sentAt)
|
|
1271
1370
|
|| sent.reply && (typeof sent.reply.text !== "string" || sent.reply.inReplyTo !== id)
|
|
1272
|
-
|| !sent.reply && !sent.failed && !isTerminalDelivery(deliveryReceipts.get(id))
|
|
1371
|
+
|| !sent.reply && !sent.failed && !isTerminalDelivery(deliveryReceipts.get(id)) && !pendingMessages.has(id)
|
|
1273
1372
|
|| isInterruptedDelivery(deliveryReceipts.get(id)) && deliveryReceipts.get(id).inReplyTo !== id) {
|
|
1274
1373
|
throw new Error("Invalid or unconfirmed Claude delivery reload record");
|
|
1275
1374
|
}
|
|
@@ -1279,6 +1378,9 @@ export class PeerEndpoint {
|
|
|
1279
1378
|
this.inbox = state.inbox;
|
|
1280
1379
|
this.sentMessages = sentMessages;
|
|
1281
1380
|
this.deliveryReceipts = deliveryReceipts;
|
|
1381
|
+
this.pendingMessages = pendingMessages;
|
|
1382
|
+
this.unconfirmedReplies = unconfirmedReplies;
|
|
1383
|
+
this.transcriptPollingPaused = true;
|
|
1282
1384
|
}
|
|
1283
1385
|
|
|
1284
1386
|
#removeRegistration() {
|
package/src/platform.mjs
CHANGED
|
@@ -88,9 +88,10 @@ function workspaceRoots() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function isRunnable(candidate) {
|
|
91
|
-
if (!candidate ||
|
|
92
|
-
if (IS_WINDOWS) return true;
|
|
91
|
+
if (typeof candidate !== "string" || !candidate || candidate.includes("\0")) return false;
|
|
93
92
|
try {
|
|
93
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile()) return false;
|
|
94
|
+
if (IS_WINDOWS) return true;
|
|
94
95
|
accessSync(candidate, constants.X_OK);
|
|
95
96
|
return true;
|
|
96
97
|
} catch {
|
|
@@ -98,9 +99,9 @@ function isRunnable(candidate) {
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function windowsCodexCandidates() {
|
|
102
|
-
const roaming =
|
|
103
|
-
const local =
|
|
102
|
+
function windowsCodexCandidates(env, runnable) {
|
|
103
|
+
const roaming = env.APPDATA;
|
|
104
|
+
const local = env.LOCALAPPDATA;
|
|
104
105
|
const versionedRoot = local && path.join(local, "OpenAI", "Codex", "bin");
|
|
105
106
|
let versioned = [];
|
|
106
107
|
if (versionedRoot) {
|
|
@@ -108,7 +109,7 @@ function windowsCodexCandidates() {
|
|
|
108
109
|
versioned = readdirSync(versionedRoot, { withFileTypes: true })
|
|
109
110
|
.filter((entry) => entry.isDirectory())
|
|
110
111
|
.map((entry) => path.join(versionedRoot, entry.name, "codex.exe"))
|
|
111
|
-
.filter((candidate) =>
|
|
112
|
+
.filter((candidate) => runnable(candidate))
|
|
112
113
|
.sort((left, right) => {
|
|
113
114
|
try {
|
|
114
115
|
return statSync(path.dirname(right)).mtimeMs - statSync(path.dirname(left)).mtimeMs;
|
|
@@ -119,16 +120,16 @@ function windowsCodexCandidates() {
|
|
|
119
120
|
} catch {}
|
|
120
121
|
}
|
|
121
122
|
return [
|
|
122
|
-
|
|
123
|
+
env.CODEX_CLI_PATH,
|
|
123
124
|
...versioned,
|
|
124
125
|
local && path.join(local, "Programs", "OpenAI", "Codex", "bin", "codex.exe"),
|
|
125
126
|
roaming && path.join(roaming, "npm", "codex.cmd"),
|
|
126
|
-
|
|
127
|
+
env.ProgramFiles && path.join(env.ProgramFiles, "nodejs", "codex.cmd"),
|
|
127
128
|
];
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
function unixCodexCandidates() {
|
|
131
|
-
const home =
|
|
131
|
+
function unixCodexCandidates(env) {
|
|
132
|
+
const home = env.HOME ?? env.USERPROFILE ?? os.homedir();
|
|
132
133
|
return [
|
|
133
134
|
path.join(home, ".local", "bin", "codex"),
|
|
134
135
|
path.join(home, ".npm-global", "bin", "codex"),
|
|
@@ -147,17 +148,17 @@ function unixCodexCandidates() {
|
|
|
147
148
|
* two versions write that same state, so prefer the app's binary whenever the
|
|
148
149
|
* app is installed and fall back to the standalone CLI installs otherwise.
|
|
149
150
|
*/
|
|
150
|
-
function macosCodexCandidates() {
|
|
151
|
+
function macosCodexCandidates(env) {
|
|
151
152
|
return [
|
|
152
153
|
hasCodexDesktopApp() ? CODEX_DESKTOP_BIN_MACOS : null,
|
|
153
|
-
...unixCodexCandidates(),
|
|
154
|
+
...unixCodexCandidates(env),
|
|
154
155
|
];
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
function pathCandidates() {
|
|
158
|
-
const separator =
|
|
159
|
-
const names =
|
|
160
|
-
return (
|
|
158
|
+
function pathCandidates(env, platform) {
|
|
159
|
+
const separator = platform === "win32" ? ";" : ":";
|
|
160
|
+
const names = platform === "win32" ? ["codex.exe", "codex.cmd"] : ["codex"];
|
|
161
|
+
return (env.PATH ?? "")
|
|
161
162
|
.split(separator)
|
|
162
163
|
.filter(Boolean)
|
|
163
164
|
.flatMap((dir) => names.map((name) => path.join(dir, name)));
|
|
@@ -217,8 +218,8 @@ export function resolveCodexBin(explicit, {
|
|
|
217
218
|
explicit,
|
|
218
219
|
env.CODEX_BIN,
|
|
219
220
|
...(discovered ?? [
|
|
220
|
-
...(
|
|
221
|
-
...pathCandidates(),
|
|
221
|
+
...(platform === "win32" ? windowsCodexCandidates(env, runnable) : platform === "darwin" ? macosCodexCandidates(env) : unixCodexCandidates(env)),
|
|
222
|
+
...pathCandidates(env, platform),
|
|
222
223
|
]),
|
|
223
224
|
].filter(Boolean);
|
|
224
225
|
|
|
@@ -55,8 +55,9 @@ export function bridgeDiagnostic(error, options = {}) {
|
|
|
55
55
|
phase: name(previous?.phase ?? options.phase ?? error?.phase ?? error?.diagnostic?.stage, deliveryState === "not_sent" ? "preflight" : options.readOnly ? "inspection" : "delivery"),
|
|
56
56
|
deliveryState,
|
|
57
57
|
sent: deliveryState === "not_sent" ? false : deliveryState === "sent" ? true : null,
|
|
58
|
-
retrySafe: deliveryState === "not_sent",
|
|
59
|
-
nextAction:
|
|
58
|
+
retrySafe: deliveryState === "not_sent" && code !== "PEER_REPLY_PENDING",
|
|
59
|
+
nextAction: code === "PEER_REPLY_PENDING" ? "Read read_claude_delivery with the original msgId. Wait for its correlated outcome; do not resend or start another bridge to bypass pending work."
|
|
60
|
+
: deliveryState === "not_sent" ? "Inspect the reported cause and bridge status before retrying. Do not open or create chats to load a relay."
|
|
60
61
|
: deliveryState === "not_applicable" ? "Inspect bridge status and include this diagnostic in the report."
|
|
61
62
|
: "Inspect the original delivery receipt. Do not resend automatically or create a replacement chat.",
|
|
62
63
|
reason: diagnosticText(previous?.reason ?? error?.preflight?.reason ?? error?.message ?? error),
|