@minhspark/codex-mcp-bridge 1.13.8 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/README.md +37 -18
- package/package.json +6 -6
- package/scripts/install-claude-desktop.mjs +12 -3
- package/scripts/install-codex-mcp.mjs +8 -5
- package/scripts/install-native-relay.mjs +15 -9
- package/src/bridge-account-context.mjs +77 -0
- package/src/claude-bridge-launcher.mjs +3 -0
- package/src/claude-bridge.mjs +90 -28
- package/src/claude-desktop-context.mjs +11 -6
- package/src/claude-sender-context.mjs +206 -0
- package/src/claude-session-router.mjs +92 -0
- package/src/codex-account-context.mjs +116 -0
- package/src/codex-bridge-launcher.mjs +3 -0
- package/src/codex-mcp-registration.mjs +14 -9
- package/src/desktop-account-context.mjs +140 -0
- package/src/desktop-task-receipts.mjs +3 -1
- package/src/index.mjs +86 -8
- package/src/mcp-supervisor.mjs +314 -0
- package/src/native-relay-companion.mjs +169 -34
- package/src/native-relay-launcher.mjs +3 -0
- package/src/native-relay.mjs +119 -47
- package/src/peer-protocol.mjs +124 -21
- package/src/platform.mjs +4 -1
- package/src/release-snapshot.mjs +95 -0
- package/src/reload-control.mjs +96 -0
- package/src/reply-forwarder.mjs +43 -0
- package/src/thread-delivery.mjs +27 -11
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
function digestFiles(root, files) {
|
|
7
|
+
const hash = createHash("sha256");
|
|
8
|
+
for (const file of files) hash.update(file).update("\0").update(fs.readFileSync(path.join(root, file))).update("\0");
|
|
9
|
+
return hash.digest("hex");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function inventory(root, prefix = "") {
|
|
13
|
+
const files = [];
|
|
14
|
+
for (const entry of fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
15
|
+
if (entry.name === ".bin") continue;
|
|
16
|
+
const relative = path.join(prefix, entry.name);
|
|
17
|
+
const stat = entry.isSymbolicLink() ? fs.statSync(path.join(root, relative)) : entry;
|
|
18
|
+
if (stat.isDirectory()) files.push(...inventory(root, relative));
|
|
19
|
+
else if (stat.isFile()) files.push(relative);
|
|
20
|
+
else throw new Error(`Unsupported release file: ${relative}`);
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sourceRevision(root) {
|
|
26
|
+
const files = ["package.json", ...inventory(root, "src")];
|
|
27
|
+
for (const file of ["package-lock.json", "node_modules/.package-lock.json"]) {
|
|
28
|
+
if (fs.existsSync(path.join(root, file))) files.push(file);
|
|
29
|
+
}
|
|
30
|
+
return digestFiles(root, files);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function snapshotRoot(env = process.env) {
|
|
34
|
+
return path.resolve(env.CODEX_BRIDGE_RUNTIME_CACHE ?? path.join(env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "bridge-runtimes"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeOwned(directory, cache) {
|
|
38
|
+
const relative = path.relative(cache, directory);
|
|
39
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Refusing to remove an unowned runtime directory");
|
|
40
|
+
if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) throw new Error("Refusing to remove a linked runtime directory");
|
|
41
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createReleaseSnapshot(root, { cache = snapshotRoot(), expectedRevision = sourceRevision(root) } = {}) {
|
|
45
|
+
root = fs.realpathSync.native(root);
|
|
46
|
+
if (fs.existsSync(cache) && fs.lstatSync(cache).isSymbolicLink()) throw new Error("The runtime cache cannot be a symbolic link");
|
|
47
|
+
const missing = [];
|
|
48
|
+
let ancestor = path.resolve(cache);
|
|
49
|
+
while (!fs.existsSync(ancestor)) {
|
|
50
|
+
missing.unshift(path.basename(ancestor));
|
|
51
|
+
const parent = path.dirname(ancestor);
|
|
52
|
+
if (parent === ancestor) throw new Error("The runtime cache has no accessible filesystem root");
|
|
53
|
+
ancestor = parent;
|
|
54
|
+
}
|
|
55
|
+
cache = path.join(fs.realpathSync.native(ancestor), ...missing);
|
|
56
|
+
fs.mkdirSync(cache, { recursive: true, mode: 0o700 });
|
|
57
|
+
if (fs.lstatSync(cache).isSymbolicLink()) throw new Error("The runtime cache cannot be a symbolic link");
|
|
58
|
+
cache = fs.realpathSync.native(cache);
|
|
59
|
+
const dependencies = inventory(root, "node_modules");
|
|
60
|
+
const dependencyRevision = digestFiles(root, dependencies);
|
|
61
|
+
const key = createHash("sha256").update(expectedRevision).update(dependencyRevision).digest("hex");
|
|
62
|
+
const target = path.join(cache, key);
|
|
63
|
+
const validate = (directory) => {
|
|
64
|
+
if (fs.lstatSync(directory).isSymbolicLink()) throw new Error("The release directory cannot be a symbolic link");
|
|
65
|
+
return sourceRevision(directory) === expectedRevision && digestFiles(directory, inventory(directory, "node_modules")) === dependencyRevision;
|
|
66
|
+
};
|
|
67
|
+
if (fs.existsSync(target)) {
|
|
68
|
+
if (!validate(target)) throw new Error("An existing immutable runtime failed integrity verification");
|
|
69
|
+
if (sourceRevision(root) !== expectedRevision) throw new Error("The installation changed during snapshot verification");
|
|
70
|
+
return { directory: target, revision: expectedRevision, key };
|
|
71
|
+
}
|
|
72
|
+
const temporary = path.join(cache, `.preparing-${process.pid}-${randomUUID()}`);
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(temporary, { mode: 0o700 });
|
|
75
|
+
const files = ["package.json", ...inventory(root, "src"), ...dependencies];
|
|
76
|
+
if (fs.existsSync(path.join(root, "package-lock.json"))) files.push("package-lock.json");
|
|
77
|
+
const directories = new Set(["src", "node_modules", ...files.map((file) => path.dirname(file))]);
|
|
78
|
+
for (const directory of directories) fs.mkdirSync(path.join(temporary, directory), { recursive: true, mode: 0o700 });
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
fs.copyFileSync(path.join(root, file), path.join(temporary, file), fs.constants.COPYFILE_FICLONE);
|
|
81
|
+
}
|
|
82
|
+
if (sourceRevision(root) !== expectedRevision || digestFiles(root, inventory(root, "node_modules")) !== dependencyRevision || !validate(temporary)) {
|
|
83
|
+
throw new Error("The installation changed while its immutable runtime was being prepared");
|
|
84
|
+
}
|
|
85
|
+
try { fs.renameSync(temporary, target); }
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (!fs.existsSync(target) || !validate(target)) throw error;
|
|
88
|
+
removeOwned(temporary, cache);
|
|
89
|
+
}
|
|
90
|
+
return { directory: target, revision: expectedRevision, key };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
removeOwned(temporary, cache);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function cloneReloadState(value) {
|
|
2
|
+
const encoded = JSON.stringify(value);
|
|
3
|
+
if (typeof encoded !== "string" || Buffer.byteLength(encoded) > 16 * 1024 * 1024) {
|
|
4
|
+
throw new Error("Reload state is missing or exceeds its transfer limit");
|
|
5
|
+
}
|
|
6
|
+
return JSON.parse(encoded);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function clientReloadReason(client) {
|
|
10
|
+
if (!client) return null;
|
|
11
|
+
for (const field of ["pending", "threadOperations", "attachingThreads", "activeTurns", "threadListeners"]) {
|
|
12
|
+
if (client[field]?.size) return `The app-server still has ${field}`;
|
|
13
|
+
}
|
|
14
|
+
if (client.connecting) return "The app-server connection is still being established";
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createReloadControl({ entry, inspect = () => null, quiesce = () => {}, exportState = () => ({}), restore = () => {}, activate = () => {}, resume = activate, env = process.env, channel = process } = {}) {
|
|
19
|
+
if (!["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs"].includes(entry)) throw new Error("Invalid reload entry point");
|
|
20
|
+
const enabled = env.CODEX_BRIDGE_WORKER === "1" && typeof channel.send === "function";
|
|
21
|
+
let phase = enabled && env.CODEX_BRIDGE_STAGED === "1" ? "staged" : "active";
|
|
22
|
+
let active = 0;
|
|
23
|
+
let deferred = null;
|
|
24
|
+
let listening = false;
|
|
25
|
+
let commands = Promise.resolve();
|
|
26
|
+
|
|
27
|
+
const status = () => {
|
|
28
|
+
const reason = active ? "MCP tool calls are still active" : deferred ?? inspect();
|
|
29
|
+
return { reloadable: phase === "active" && !reason, reason: reason ?? (phase === "active" ? null : `The worker is ${phase}`), schema: 1, phase };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const control = async (action, data) => {
|
|
33
|
+
if (action === "inspect") return status();
|
|
34
|
+
if (action === "quiesce") {
|
|
35
|
+
const current = status();
|
|
36
|
+
if (!current.reloadable) throw new Error(current.reason);
|
|
37
|
+
phase = "quiescing";
|
|
38
|
+
try {
|
|
39
|
+
await quiesce();
|
|
40
|
+
const reason = deferred ?? inspect();
|
|
41
|
+
if (active || reason) throw new Error(reason ?? "A tool call raced with quiescence");
|
|
42
|
+
const state = cloneReloadState({ schema: 1, entry, payload: exportState() });
|
|
43
|
+
phase = "quiesced";
|
|
44
|
+
return { state };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
phase = "quiesced";
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (action === "restore") {
|
|
51
|
+
if (phase !== "staged") throw new Error("Reload state can only be restored into a staged worker");
|
|
52
|
+
const state = cloneReloadState(data);
|
|
53
|
+
if (!state || state.schema !== 1 || state.entry !== entry || !state.payload || typeof state.payload !== "object" || Array.isArray(state.payload)) {
|
|
54
|
+
throw new Error("Reload state schema or entry point does not match this worker");
|
|
55
|
+
}
|
|
56
|
+
await restore(state.payload);
|
|
57
|
+
return { restored: true, schema: 1 };
|
|
58
|
+
}
|
|
59
|
+
if (action === "activate" || action === "resume") {
|
|
60
|
+
if (phase === "active") return { active: true, schema: 1 };
|
|
61
|
+
if (action === "activate" && phase !== "staged" || action === "resume" && phase !== "quiesced") {
|
|
62
|
+
throw new Error(`Cannot ${action} a ${phase} worker`);
|
|
63
|
+
}
|
|
64
|
+
await (action === "activate" ? activate() : resume());
|
|
65
|
+
phase = "active";
|
|
66
|
+
return { active: true, schema: 1 };
|
|
67
|
+
}
|
|
68
|
+
throw new Error("Unknown bridge control action");
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
get staged() { return phase === "staged"; },
|
|
73
|
+
inspect: status,
|
|
74
|
+
defer(reason) { deferred ??= String(reason); },
|
|
75
|
+
async run(handler) {
|
|
76
|
+
if (phase !== "active") throw new Error(`Bridge worker is ${phase}; no tool operation was started`);
|
|
77
|
+
active += 1;
|
|
78
|
+
try { return await handler(); }
|
|
79
|
+
finally { active -= 1; }
|
|
80
|
+
},
|
|
81
|
+
control,
|
|
82
|
+
listen() {
|
|
83
|
+
if (!enabled || listening) return;
|
|
84
|
+
listening = true;
|
|
85
|
+
channel.on("message", (message) => {
|
|
86
|
+
if (message?.type !== "bridge:control" || !["string", "number"].includes(typeof message.id)) return;
|
|
87
|
+
commands = commands.catch(() => {}).then(async () => {
|
|
88
|
+
let response;
|
|
89
|
+
try { response = { result: await control(message.action, message.data) }; }
|
|
90
|
+
catch (error) { response = { error: error?.message ?? String(error) }; }
|
|
91
|
+
if (channel.connected !== false) channel.send({ type: "bridge:control-result", id: message.id, ...response });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/reply-forwarder.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cloneReloadState } from "./reload-control.mjs";
|
|
2
|
+
|
|
1
3
|
const KNOWN_UNSENT_CODES = new Set([
|
|
2
4
|
"RELAY_UNREACHABLE",
|
|
3
5
|
"RELAY_MESSAGE_TOO_LARGE",
|
|
@@ -97,6 +99,47 @@ export class ReplyForwarder {
|
|
|
97
99
|
return { ...counts, attempts: this.attempts, maxPerSession: this.maxPerSession, closed: this.closed };
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
reloadReason() {
|
|
103
|
+
if (this.active || this.queue.length || this.timer !== null) return "Reply forwarding is still active or queued";
|
|
104
|
+
if ([...this.records.values()].some(({ receipt }) => ["queued", "sending", "unknown"].includes(receipt.status))) {
|
|
105
|
+
return "A reply forwarding outcome remains unconfirmed";
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
exportReloadState() {
|
|
111
|
+
const reason = this.reloadReason();
|
|
112
|
+
if (reason) throw new Error(reason);
|
|
113
|
+
return cloneReloadState({ records: [...this.records], attempts: this.attempts, lastAt: this.lastAt, closed: this.closed,
|
|
114
|
+
maxPerSession: this.maxPerSession, minIntervalMs: this.minIntervalMs });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
restoreReloadState(input) {
|
|
118
|
+
if (this.records.size || this.active || this.queue.length || this.timer !== null) throw new Error("Reply forwarding state is not empty");
|
|
119
|
+
const state = cloneReloadState(input);
|
|
120
|
+
if (!state || !Array.isArray(state.records) || !Number.isSafeInteger(state.attempts) || state.attempts < 0
|
|
121
|
+
|| !Number.isSafeInteger(state.maxPerSession) || state.maxPerSession < 1 || state.attempts > state.maxPerSession
|
|
122
|
+
|| !Number.isFinite(state.minIntervalMs) || state.minIntervalMs < 0 || typeof state.closed !== "boolean"
|
|
123
|
+
|| state.lastAt !== null && (!Number.isFinite(state.lastAt) || state.lastAt < 0)) throw new Error("Invalid reply forwarding reload state");
|
|
124
|
+
const records = new Map();
|
|
125
|
+
for (const pair of state.records) {
|
|
126
|
+
if (!Array.isArray(pair) || pair.length !== 2) throw new Error("Invalid reply forwarding record");
|
|
127
|
+
const [id, entry] = pair;
|
|
128
|
+
if (!validIdentity(id) || records.has(id) || !entry?.record || !entry.receipt
|
|
129
|
+
|| entry.receipt.msgId !== id || !validIdentity(entry.receipt.threadId)
|
|
130
|
+
|| entry.record.replyThreadId !== entry.receipt.threadId || (entry.record.inReplyTo ?? entry.record.msgId) !== id
|
|
131
|
+
|| typeof entry.record.text !== "string" || !entry.record.text.trim()
|
|
132
|
+
|| !["forwarded", "failed", "blocked"].includes(entry.receipt.status)) throw new Error("Invalid or unconfirmed reply forwarding record");
|
|
133
|
+
records.set(id, { record: Object.freeze(entry.record), receipt: entry.receipt });
|
|
134
|
+
}
|
|
135
|
+
this.records = records;
|
|
136
|
+
this.attempts = state.attempts;
|
|
137
|
+
this.lastAt = state.lastAt;
|
|
138
|
+
this.closed = state.closed;
|
|
139
|
+
this.maxPerSession = Math.min(this.maxPerSession, state.maxPerSession);
|
|
140
|
+
this.minIntervalMs = Math.max(this.minIntervalMs, state.minIntervalMs);
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
close() {
|
|
101
144
|
this.closed = true;
|
|
102
145
|
if (this.timer !== null) this.cancel(this.timer);
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NativeDesktopRelay, desktopTaskSocketPath, desktopTasksConfigured } from "./native-relay.mjs";
|
|
1
|
+
import { NativeDesktopRelay, accountRelaySocketPath, desktopTaskSocketPath, desktopTasksConfigured } from "./native-relay.mjs";
|
|
2
2
|
import { runTurn } from "./turn.mjs";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -42,20 +42,28 @@ export function matchDesktopProject(projects, cwd, { canonicalize = realpathSync
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export class DesktopTaskDelivery {
|
|
45
|
-
constructor({ relay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath() }), security, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now, receipts = new DesktopTaskReceipts() } = {}) {
|
|
45
|
+
constructor({ relay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() }), security, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now, receipts = new DesktopTaskReceipts(), beforeRequest, accountContext } = {}) {
|
|
46
46
|
this.relay = relay;
|
|
47
47
|
this.security = security;
|
|
48
48
|
this.sleep = sleep;
|
|
49
49
|
this.now = now;
|
|
50
50
|
this.receipts = receipts;
|
|
51
|
+
this.beforeRequest = beforeRequest;
|
|
52
|
+
this.accountContext = accountContext;
|
|
51
53
|
this.threadOperations = new Map();
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
async request(operation, args, { deadline } = {}) {
|
|
55
57
|
try {
|
|
58
|
+
await this.beforeRequest?.({ operation, args });
|
|
56
59
|
const remaining = deadline === undefined ? undefined : deadline - this.now();
|
|
57
60
|
if (remaining !== undefined && remaining <= 0) throw new Error("The bridge's response deadline has elapsed; this operation was not sent");
|
|
58
|
-
const
|
|
61
|
+
const accountContext = this.accountContext?.();
|
|
62
|
+
const response = await this.relay.requestDesktop(operation, args, {
|
|
63
|
+
...(remaining === undefined ? {} : { timeoutMs: remaining }),
|
|
64
|
+
...(this.beforeRequest ? { beforeSend: () => this.beforeRequest({ operation, args, phase: "write" }) } : {}),
|
|
65
|
+
...(accountContext ? { accountContext } : {}),
|
|
66
|
+
});
|
|
59
67
|
return response.result;
|
|
60
68
|
} catch (err) {
|
|
61
69
|
throw new Error(`Codex Desktop operation ${operation} failed: ${err.message}. Desktop-only mode will not start or use an external app-server. Open Codex Desktop and reconnect its native relay, then inspect the existing task before retrying a send.`, { cause: err });
|
|
@@ -87,12 +95,15 @@ export class DesktopTaskDelivery {
|
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
async status() {
|
|
98
|
+
const accountContext = this.accountContext?.();
|
|
99
|
+
const socketPath = this.relay.status?.({ accountContext })?.socketPath ?? this.relay.socketPath;
|
|
100
|
+
if (this.accountContext && !accountContext) return { available: false, socketPath, reason: "The current Claude and Codex accounts could not both be verified. Finish signing in and inspect the account diagnostics before sending work." };
|
|
90
101
|
try {
|
|
91
102
|
const response = await this.request("list_projects", {});
|
|
92
103
|
if (!Array.isArray(response?.projects)) throw new Error("Desktop returned no project list");
|
|
93
|
-
return { available: true, socketPath
|
|
104
|
+
return { available: true, socketPath, localProjects: response.projects.filter((project) => project.projectKind === "local" && project.hostId === "local").length };
|
|
94
105
|
} catch (err) {
|
|
95
|
-
return { available: false, socketPath
|
|
106
|
+
return { available: false, socketPath, reason: err.message };
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
|
|
@@ -118,8 +129,11 @@ export class DesktopTaskDelivery {
|
|
|
118
129
|
return { rows, coverage: "Codex Desktop's recent/pinned snapshot; local Codex workspaces only. Agent-created tasks visible in the sidebar can be omitted; read a known task ID directly before assuming creation failed." };
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
async reuseReceipt(receipt, { cwd, promptHash, deadline }) {
|
|
132
|
+
async reuseReceipt(receipt, { cwd, promptHash, deadline, accountContext }) {
|
|
122
133
|
if (!receipt) return null;
|
|
134
|
+
if (accountContext && (!receipt.accountContext || ["claude", "codex"].some((provider) => receipt.accountContext[provider] !== accountContext[provider]))) {
|
|
135
|
+
throw new Error(`The existing Desktop creation receipt ${receipt.threadId ? `for task ${receipt.threadId} ` : ""}${receipt.accountContext ? "belongs to different accounts" : "has no verified original account binding"}. Its receipt and task ID were retained. No creation or prompt resend was attempted; inspect the explicit existing task before continuing.`);
|
|
136
|
+
}
|
|
123
137
|
if (path.relative(cwd, realpathSync.native(receipt.cwd))) throw new Error("The stored Desktop creation receipt belongs to another workspace. No prompt was sent.");
|
|
124
138
|
if (receipt.state !== "known") throw new Error(`An earlier Desktop creation is ${receipt.state} and may already have started. ${receipt.threadId ? `threadId: ${receipt.threadId}. ` : ""}Do not resend or create another task; inspect the existing Desktop task and resolve its creation receipt first.`);
|
|
125
139
|
let response;
|
|
@@ -180,7 +194,9 @@ export class DesktopTaskDelivery {
|
|
|
180
194
|
this.security.assertCwd(cwd);
|
|
181
195
|
dedupeName = dedupeName?.normalize("NFC").trim().replace(/\s+/g, " ") || undefined;
|
|
182
196
|
const identity = this.receipts.key({ cwd, prompt, name: dedupeName });
|
|
183
|
-
const
|
|
197
|
+
const accountContext = this.accountContext?.();
|
|
198
|
+
if (this.accountContext && !accountContext) throw new Error("Desktop creation requires the original verified account context.");
|
|
199
|
+
const options = { cwd, promptHash: identity.promptHash, deadline, accountContext };
|
|
184
200
|
const reused = await this.reuseReceipt(await this.receipts.read(identity.key), options);
|
|
185
201
|
if (reused) return reused;
|
|
186
202
|
const listed = await this.request("list_projects", {}, { deadline });
|
|
@@ -190,7 +206,7 @@ export class DesktopTaskDelivery {
|
|
|
190
206
|
const existing = await this.reuseReceipt(await this.receipts.read(identity.key), options);
|
|
191
207
|
if (existing) return existing;
|
|
192
208
|
if (this.now() >= deadline) throw new Error("The response deadline elapsed before Desktop creation. No task was created.");
|
|
193
|
-
const receipt = { version: 1, ...identity, cwd, state: "pending", startedAt: this.now(), ...(dedupeName ? { name: dedupeName } : {}), projectId: project.projectId, ...(project.label ? { projectName: project.label } : {}) };
|
|
209
|
+
const receipt = { version: 1, ...identity, cwd, state: "pending", startedAt: this.now(), ...(accountContext ? { accountContext: { ...accountContext } } : {}), ...(dedupeName ? { name: dedupeName } : {}), projectId: project.projectId, ...(project.label ? { projectName: project.label } : {}) };
|
|
194
210
|
await this.receipts.write(identity.key, receipt);
|
|
195
211
|
let threadId;
|
|
196
212
|
let creationConfirmed = false;
|
|
@@ -309,11 +325,11 @@ export function createThreadDelivery({
|
|
|
309
325
|
* app-server only spawns a process that contends for the ~/.codex state and
|
|
310
326
|
* then fails on the writer lock the native path exists to avoid.
|
|
311
327
|
*/
|
|
312
|
-
async function deliver(threadId, text) {
|
|
313
|
-
const status = relay.status();
|
|
328
|
+
async function deliver(threadId, text, options = {}) {
|
|
329
|
+
const status = relay.status(options);
|
|
314
330
|
if (status.enabled) {
|
|
315
331
|
try {
|
|
316
|
-
const ack = await relay.sendMessage(threadId, text);
|
|
332
|
+
const ack = await relay.sendMessage(threadId, text, options);
|
|
317
333
|
reportedUnavailable = null;
|
|
318
334
|
return { backend: NATIVE_BACKEND, threadId, ack };
|
|
319
335
|
} catch (err) {
|