@minhspark/codex-mcp-bridge 1.22.12 → 1.22.14
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 +44 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-claude-desktop.mjs +37 -9
- package/scripts/install-native-relay.mjs +6 -3
- package/src/bridge-readiness.mjs +3 -1
- package/src/claude-bridge.mjs +6 -3
- package/src/claude-code-registration.mjs +28 -4
- package/src/codex-mcp-toml.mjs +26 -4
- package/src/codex-native-response.mjs +47 -26
- package/src/codex-sender-context.mjs +13 -2
- package/src/index.mjs +29 -8
- package/src/mcp-supervisor.mjs +30 -6
- package/src/native-relay-companion.mjs +39 -34
- package/src/native-relay.mjs +4 -4
- package/src/peer-protocol.mjs +3 -2
- package/src/platform.mjs +40 -7
- package/src/project-onboarding-tools.mjs +6 -4
- package/src/project-onboarding.mjs +43 -16
- package/src/relay-wake.mjs +129 -0
- package/src/release-snapshot.mjs +360 -33
- package/src/rollout-reader.mjs +160 -24
- package/src/rollout-segments.mjs +35 -7
- package/src/thread-delivery.mjs +20 -3
- package/src/vscode-bridge.mjs +9 -23
package/src/rollout-reader.mjs
CHANGED
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import zlib from "node:zlib";
|
|
2
3
|
import { fileIdentity } from "./file-identity.mjs";
|
|
3
4
|
|
|
4
5
|
const CHUNK_BYTES = 256 * 1024;
|
|
6
|
+
const COMPRESSED_SUFFIX = ".zst";
|
|
5
7
|
const VERSION_FIELDS = ["size", "mtimeNs", "ctimeNs", "ino", "dev"];
|
|
6
8
|
const sameVersion = (a, b) => VERSION_FIELDS.every((key) => a[key] === b[key]);
|
|
9
|
+
const versionKey = (version) => VERSION_FIELDS.map((key) => String(version[key])).join(":");
|
|
10
|
+
const VANISHED_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
11
|
+
const COMPRESSED_SCAN_CACHE_ENTRIES = 16;
|
|
12
|
+
const compressedScans = new Map();
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
export const MAX_DECOMPRESSED_ROLLOUT_BYTES = 256 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export function isCompressedRollout(file) {
|
|
17
|
+
return typeof file === "string" && file.endsWith(`.jsonl${COMPRESSED_SUFFIX}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rolloutBasePath(file) {
|
|
21
|
+
return isCompressedRollout(file) ? file.slice(0, -COMPRESSED_SUFFIX.length) : file;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function asRolloutChange(error, message = "The rollout changed while reading") {
|
|
25
|
+
return VANISHED_CODES.has(error?.code) ? new Error(message) : error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function zstdUnsupported() {
|
|
29
|
+
return new Error(`Reading a compressed Codex rollout (.jsonl.zst) requires Node.js with zstd support (22.15+ or 23.8+); this runtime is ${process.version} [check=rollout_zstd_unsupported]`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function recordParser(size, { maxRecordBytes, onRecord, onChunk }) {
|
|
33
|
+
let parts = [], length = 0, start = 0;
|
|
34
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
35
|
+
return {
|
|
36
|
+
push(chunk, position) {
|
|
37
|
+
const count = chunk.length;
|
|
29
38
|
onChunk(chunk, position);
|
|
30
39
|
let offset = 0;
|
|
31
40
|
while (offset < count) {
|
|
@@ -46,15 +55,142 @@ export function scanRollout(file, { maxRecordBytes, onRecord, onChunk = () => {}
|
|
|
46
55
|
offset = newline + 1;
|
|
47
56
|
start = position + offset;
|
|
48
57
|
}
|
|
49
|
-
|
|
58
|
+
},
|
|
59
|
+
finish() {
|
|
60
|
+
if (start !== size) throw new Error("The rollout has an incomplete final record");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readExactly(descriptor, size) {
|
|
66
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
67
|
+
for (let position = 0; position < size;) {
|
|
68
|
+
const count = fs.readSync(descriptor, buffer, position, size - position, position);
|
|
69
|
+
if (!count) throw new Error("The rollout changed while reading");
|
|
70
|
+
position += count;
|
|
71
|
+
}
|
|
72
|
+
return buffer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function decompressRollout(descriptor, storedSize, { zstdDecompress, maxDecompressedBytes }) {
|
|
76
|
+
if (storedSize > maxDecompressedBytes) throw new Error(`The compressed rollout exceeds the ${maxDecompressedBytes}-byte decompression limit [check=rollout_zstd_limit]`);
|
|
77
|
+
const stored = readExactly(descriptor, storedSize);
|
|
78
|
+
try {
|
|
79
|
+
return zstdDecompress(stored, { maxOutputLength: maxDecompressedBytes });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error?.code === "ERR_BUFFER_TOO_LARGE") throw new Error(`The compressed rollout expands beyond the ${maxDecompressedBytes}-byte decompression limit [check=rollout_zstd_limit]`);
|
|
82
|
+
throw new Error(`The compressed rollout could not be decompressed (${error?.code ?? "invalid data"}) [check=rollout_zstd_invalid]`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function scanContent(descriptor, size, parser) {
|
|
87
|
+
for (let position = 0; position < size;) {
|
|
88
|
+
const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, size - position));
|
|
89
|
+
const count = fs.readSync(descriptor, buffer, 0, buffer.length, position);
|
|
90
|
+
if (!count) throw new Error("The rollout changed while reading");
|
|
91
|
+
parser.push(buffer.subarray(0, count), position);
|
|
92
|
+
position += count;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readHead(file, maxBytes, createZstdDecompress) {
|
|
97
|
+
if (!isCompressedRollout(file)) {
|
|
98
|
+
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
99
|
+
try {
|
|
100
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
101
|
+
return buffer.subarray(0, fs.readSync(descriptor, buffer, 0, maxBytes, 0));
|
|
102
|
+
} finally { fs.closeSync(descriptor); }
|
|
103
|
+
}
|
|
104
|
+
if (typeof createZstdDecompress !== "function") throw zstdUnsupported();
|
|
105
|
+
const decompressor = createZstdDecompress();
|
|
106
|
+
const source = fs.createReadStream(file, { flags: fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0) });
|
|
107
|
+
source.on("error", (error) => decompressor.destroy(error));
|
|
108
|
+
const stream = source.pipe(decompressor);
|
|
109
|
+
const parts = [];
|
|
110
|
+
let length = 0;
|
|
111
|
+
try {
|
|
112
|
+
for await (const chunk of stream) {
|
|
113
|
+
parts.push(chunk);
|
|
114
|
+
length += chunk.length;
|
|
115
|
+
if (length >= maxBytes || chunk.includes(0x0a)) break;
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
source.destroy();
|
|
119
|
+
stream.destroy();
|
|
120
|
+
}
|
|
121
|
+
return Buffer.concat(parts, length).subarray(0, maxBytes);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function readRolloutFirstRecord(file, { maxBytes = 64 * 1024, createZstdDecompress = zlib.createZstdDecompress } = {}) {
|
|
125
|
+
const named = fs.lstatSync(file);
|
|
126
|
+
if (!named.isFile() || named.isSymbolicLink()) throw new Error("The rollout is not a regular file");
|
|
127
|
+
const head = await readHead(file, maxBytes, createZstdDecompress);
|
|
128
|
+
const newline = head.indexOf(0x0a);
|
|
129
|
+
if (newline < 0) throw new Error("The rollout's first record is incomplete or exceeds its read limit");
|
|
130
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(head.subarray(0, newline)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Scan the complete history, but retain at most one bounded JSONL record.
|
|
134
|
+
// Skipping the middle of a rollout would miss duplicate identities or corruption.
|
|
135
|
+
export function scanRollout(file, options) {
|
|
136
|
+
try {
|
|
137
|
+
return scanRolloutOnce(file, options);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw asRolloutChange(error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function memoizeCompressedScan(file, key, scan, { keep = () => true } = {}) {
|
|
144
|
+
if (!isCompressedRollout(file)) return scan();
|
|
145
|
+
let named;
|
|
146
|
+
try { named = fs.lstatSync(file, { bigint: true }); }
|
|
147
|
+
catch (error) { throw asRolloutChange(error); }
|
|
148
|
+
const entry = `${file}\0${key}`;
|
|
149
|
+
const cached = compressedScans.get(entry);
|
|
150
|
+
compressedScans.delete(entry);
|
|
151
|
+
if (cached?.version === versionKey(named)) {
|
|
152
|
+
compressedScans.set(entry, cached);
|
|
153
|
+
return cached.value;
|
|
154
|
+
}
|
|
155
|
+
const value = scan();
|
|
156
|
+
if (versionKey(value.version) === versionKey(named) && keep(value)) {
|
|
157
|
+
compressedScans.set(entry, { version: versionKey(named), value });
|
|
158
|
+
if (compressedScans.size > COMPRESSED_SCAN_CACHE_ENTRIES) compressedScans.delete(compressedScans.keys().next().value);
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scanRolloutOnce(file, { maxRecordBytes, onRecord, onChunk = () => {}, zstdDecompress = zlib.zstdDecompressSync, maxDecompressedBytes = MAX_DECOMPRESSED_ROLLOUT_BYTES }) {
|
|
164
|
+
if (!Number.isSafeInteger(maxRecordBytes) || maxRecordBytes < 1) throw new Error("The rollout record read limit is invalid");
|
|
165
|
+
if (!Number.isSafeInteger(maxDecompressedBytes) || maxDecompressedBytes < 1) throw new Error("The rollout decompression limit is invalid");
|
|
166
|
+
const compressed = isCompressedRollout(file);
|
|
167
|
+
if (compressed && typeof zstdDecompress !== "function") throw zstdUnsupported();
|
|
168
|
+
const named = fs.lstatSync(file, { bigint: true });
|
|
169
|
+
if (!named.isFile() || named.isSymbolicLink()) throw new Error("The rollout is not a regular file");
|
|
170
|
+
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
171
|
+
try {
|
|
172
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
173
|
+
if (!before.isFile()) throw new Error("The opened rollout is not a regular file");
|
|
174
|
+
if (!sameVersion(named, before)) throw new Error("The rollout changed while opening");
|
|
175
|
+
if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("The rollout exceeds the safe size range");
|
|
176
|
+
const storedSize = Number(before.size);
|
|
177
|
+
if (storedSize === 0) throw new Error("The rollout is empty (0 bytes)");
|
|
178
|
+
const content = compressed ? decompressRollout(descriptor, storedSize, { zstdDecompress, maxDecompressedBytes }) : null;
|
|
179
|
+
const size = content ? content.length : storedSize;
|
|
180
|
+
if (size === 0) throw new Error("The rollout is empty (0 bytes)");
|
|
181
|
+
const parser = recordParser(size, { maxRecordBytes, onRecord, onChunk });
|
|
182
|
+
if (content) {
|
|
183
|
+
for (let position = 0; position < size; position += CHUNK_BYTES) parser.push(content.subarray(position, Math.min(size, position + CHUNK_BYTES)), position);
|
|
184
|
+
} else {
|
|
185
|
+
scanContent(descriptor, size, parser);
|
|
50
186
|
}
|
|
51
|
-
|
|
187
|
+
parser.finish();
|
|
52
188
|
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
53
189
|
const current = fs.lstatSync(file, { bigint: true });
|
|
54
190
|
if (!current.isFile() || current.isSymbolicLink() || !sameVersion(before, after) || !sameVersion(after, current)) {
|
|
55
191
|
throw new Error("The rollout changed while reading");
|
|
56
192
|
}
|
|
57
|
-
return { size, identity: fileIdentity(before),
|
|
193
|
+
return { size, storedSize, compressed, identity: fileIdentity(before),
|
|
58
194
|
version: Object.fromEntries(VERSION_FIELDS.map((key) => [key, String(before[key])])) };
|
|
59
195
|
} finally {
|
|
60
196
|
fs.closeSync(descriptor);
|
package/src/rollout-segments.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { isCompressedRollout, rolloutBasePath, asRolloutChange, readRolloutFirstRecord } from "./rollout-reader.mjs";
|
|
3
4
|
|
|
4
5
|
const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
|
|
5
6
|
|
|
@@ -10,8 +11,8 @@ export function findRolloutSegments(sessions, threadId) {
|
|
|
10
11
|
const root = fs.lstatSync(sessions);
|
|
11
12
|
if (!root.isDirectory() || root.isSymbolicLink()) throw new Error("The Codex sessions path is not a regular directory");
|
|
12
13
|
const canonical = fs.realpathSync.native(sessions);
|
|
13
|
-
const pattern = new RegExp(`^rollout-.+-${threadId}(?:_${UUID})?\\.jsonl
|
|
14
|
-
const queue = [{ directory: canonical, depth: 0 }], matches =
|
|
14
|
+
const pattern = new RegExp(`^rollout-.+-${threadId}(?:_${UUID})?\\.jsonl(?:\\.zst)?$`);
|
|
15
|
+
const queue = [{ directory: canonical, depth: 0 }], matches = new Map();
|
|
15
16
|
let entries = 0, directories = 0;
|
|
16
17
|
while (queue.length) {
|
|
17
18
|
const { directory, depth } = queue.pop();
|
|
@@ -32,13 +33,14 @@ export function findRolloutSegments(sessions, threadId) {
|
|
|
32
33
|
}
|
|
33
34
|
if (depth === 3 && pattern.test(child.name)) {
|
|
34
35
|
if (!child.isFile() || child.isSymbolicLink()) throw new Error("The Codex rollout is not a regular file");
|
|
35
|
-
|
|
36
|
-
if (matches.
|
|
36
|
+
const base = rolloutBasePath(candidate);
|
|
37
|
+
if (!matches.has(base) || isCompressedRollout(matches.get(base))) matches.set(base, candidate);
|
|
38
|
+
if (matches.size > 64) throw new Error("The Codex task exceeds the 64-segment discovery limit");
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
|
-
if (!matches.
|
|
41
|
-
return matches.sort();
|
|
42
|
+
if (!matches.size) throw new Error(`No rollout file (plain .jsonl or compressed .jsonl.zst) matches the Codex task [check=rollout_discovery; directoriesScanned=${directories}; entriesScanned=${entries}; layout=sessions/YYYY/MM/DD/rollout-*-taskId[_continuationId].jsonl[.zst]]`);
|
|
43
|
+
return [...matches].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, file]) => file);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// Only independently retrieved native metadata may be supplied here. Tool
|
|
@@ -86,7 +88,33 @@ export function assertRolloutSetStable(sessions, threadId, snapshots) {
|
|
|
86
88
|
const files = findRolloutSegments(sessions, threadId);
|
|
87
89
|
if (files.length !== snapshots.length || files.some((file, i) => file !== snapshots[i].file)) throw new Error("The rollout segment set changed while reading");
|
|
88
90
|
for (const { file, version } of snapshots) {
|
|
89
|
-
|
|
91
|
+
let current;
|
|
92
|
+
try { current = fs.lstatSync(file, { bigint: true }); }
|
|
93
|
+
catch (error) { throw asRolloutChange(error, "A rollout segment changed while reading"); }
|
|
90
94
|
if (!current.isFile() || current.isSymbolicLink() || Object.entries(version).some(([key, value]) => typeof value !== "string" || String(current[key]) !== value)) throw new Error("A rollout segment changed while reading");
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
|
|
98
|
+
export async function readRolloutHeads(sessions) {
|
|
99
|
+
const queue = [{ directory: sessions, depth: 0 }];
|
|
100
|
+
const heads = [];
|
|
101
|
+
let count = 0;
|
|
102
|
+
while (queue.length) {
|
|
103
|
+
const { directory, depth } = queue.pop();
|
|
104
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
105
|
+
const plain = new Set(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name));
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (++count > 100000) throw new Error("Session scan limit exceeded");
|
|
108
|
+
if (entry.isSymbolicLink()) continue;
|
|
109
|
+
const file = path.join(directory, entry.name);
|
|
110
|
+
if (depth < 3 && entry.isDirectory() && /^\d{2,4}$/.test(entry.name)) queue.push({ directory: file, depth: depth + 1 });
|
|
111
|
+
if (depth !== 3 || !entry.isFile() || !(entry.name.endsWith(".jsonl") || isCompressedRollout(entry.name))) continue;
|
|
112
|
+
if (isCompressedRollout(entry.name) && plain.has(rolloutBasePath(entry.name))) continue;
|
|
113
|
+
let first;
|
|
114
|
+
try { first = await readRolloutFirstRecord(file); }
|
|
115
|
+
catch { continue; }
|
|
116
|
+
heads.push(first);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return heads;
|
|
120
|
+
}
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -72,8 +72,14 @@ export function matchDesktopProject(projects, cwd, { canonicalize = realpathSync
|
|
|
72
72
|
return matches[0];
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export const RELAY_DOWN_GUIDANCE = "The Codex Desktop native relay is not listening: Codex Desktop runs codex-native-relay only while a task has its MCP servers loaded. Open any regular project task (not the internal Native Relay executor task) and send it a turn or reconnect its MCP servers, then retry.";
|
|
76
|
+
|
|
77
|
+
function relayUnreachable(error) {
|
|
78
|
+
return error?.code === "RELAY_UNREACHABLE" && error.sent === false;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
export class DesktopTaskDelivery {
|
|
76
|
-
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, senderContext, captureResponse = captureCodexRolloutWatermark, readResponse = readCodexNativeTurnResponse, inspectResponse = inspectCodexNativeTurn, resolveResponse = readResponse === readCodexNativeTurnResponse ? resolveCodexNativeDispatchResponse : null } = {}) {
|
|
82
|
+
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, senderContext, captureResponse = captureCodexRolloutWatermark, readResponse = readCodexNativeTurnResponse, inspectResponse = inspectCodexNativeTurn, resolveResponse = readResponse === readCodexNativeTurnResponse ? resolveCodexNativeDispatchResponse : null, wakeRelay = null } = {}) {
|
|
77
83
|
this.relay = relay;
|
|
78
84
|
this.security = security;
|
|
79
85
|
this.sleep = sleep;
|
|
@@ -86,6 +92,7 @@ export class DesktopTaskDelivery {
|
|
|
86
92
|
this.readResponse = readResponse;
|
|
87
93
|
this.inspectResponse = inspectResponse;
|
|
88
94
|
this.resolveResponse = resolveResponse;
|
|
95
|
+
this.wakeRelay = wakeRelay;
|
|
89
96
|
this.threadOperations = new Map();
|
|
90
97
|
}
|
|
91
98
|
|
|
@@ -120,7 +127,7 @@ export class DesktopTaskDelivery {
|
|
|
120
127
|
const remaining = deadline === undefined ? undefined : deadline - this.now();
|
|
121
128
|
if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
|
|
122
129
|
const accountContext = this.accountContext?.();
|
|
123
|
-
const
|
|
130
|
+
const send = () => this.relay.requestDesktop(operation, args, {
|
|
124
131
|
...(remaining === undefined ? {} : { timeoutMs: remaining }),
|
|
125
132
|
...(this.beforeRequest || projectScope || scopeBinding ? { beforeSend: async () => {
|
|
126
133
|
await this.beforeRequest?.({ operation, args, phase: "write" });
|
|
@@ -134,11 +141,21 @@ export class DesktopTaskDelivery {
|
|
|
134
141
|
...(accountContext ? { accountContext } : {}),
|
|
135
142
|
...(creationReceiptId ? { creationReceiptId } : {}),
|
|
136
143
|
});
|
|
144
|
+
let response;
|
|
145
|
+
try {
|
|
146
|
+
response = await send();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!relayUnreachable(error) || typeof this.wakeRelay !== "function") throw error;
|
|
149
|
+
const socketPath = accountContext ? this.relay.accountSocketPath : this.relay.socketPath;
|
|
150
|
+
if (!await this.wakeRelay({ operation, socketPath, threadId: args?.threadId, deadline })) throw error;
|
|
151
|
+
response = await send();
|
|
152
|
+
}
|
|
137
153
|
return includeRelayContext ? { result: response.result, executorThreadId: response.executorThreadId } : response.result;
|
|
138
154
|
} catch (err) {
|
|
139
155
|
if (err.code === "RESPONSE_TOO_LARGE") throw err;
|
|
140
156
|
if (err.code?.startsWith("PROJECT_SCOPE_") || err.code === "CLAUDE_SENDER_CONTEXT_UNVERIFIED") throw err;
|
|
141
|
-
|
|
157
|
+
const guidance = relayUnreachable(err) ? `${RELAY_DOWN_GUIDANCE} ` : "";
|
|
158
|
+
throw new Error(`Codex Desktop operation ${operation} failed: ${String(err.message).replace(/[.\s]+$/, "")}. Desktop-only mode will not start or use an external app-server. ${guidance}Inspect the existing task before retrying a send.`, { cause: err });
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
|
package/src/vscode-bridge.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { VSCodeIpc } from "./vscode-ipc.mjs";
|
|
8
8
|
import { classifyClaudeDelivery, classifyCodexCompletion } from "./vscode-delivery.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { readCodexSenderContext, readTaskState } from "./codex-sender-context.mjs";
|
|
10
|
+
import { readRolloutHeads } from "./rollout-segments.mjs";
|
|
10
11
|
import { readProcessAncestry } from "./claude-sender-context.mjs";
|
|
11
12
|
import { listClaudeSessions, assertClaudeSessionCwd, assertClaudeSessionProcess, PeerEndpoint } from "./peer-protocol.mjs";
|
|
12
13
|
import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
|
|
@@ -50,34 +51,19 @@ async function caller(meta) {
|
|
|
50
51
|
|
|
51
52
|
function codexTask(threadId, cwd) {
|
|
52
53
|
if (!/^[0-9a-f-]{36}$/.test(threadId)) throw new Error("Invalid task ID");
|
|
53
|
-
const
|
|
54
|
-
const
|
|
54
|
+
const state = readTaskState(sessions, threadId, 64 * 1024 * 1024);
|
|
55
|
+
const { file } = state;
|
|
55
56
|
if (state.session?.id !== threadId || state.session.originator !== "codex_vscode" || state.session.source !== "vscode") throw new Error("The target is not a Codex VS Code task");
|
|
56
57
|
assertRoot(state.session.cwd);
|
|
57
58
|
if (!samePath(state.session.cwd, cwd) || !samePath(state.context.cwd, cwd)) throw new Error("Target and caller must use the same project directory");
|
|
58
59
|
return { ...state, file };
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function taskCandidates(cwd) {
|
|
62
|
-
const queue = [{ directory: sessions, depth: 0 }];
|
|
62
|
+
async function taskCandidates(cwd) {
|
|
63
63
|
const rows = [];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
68
|
-
if (++count > 100000) throw new Error("Session scan limit exceeded");
|
|
69
|
-
if (entry.isSymbolicLink()) continue;
|
|
70
|
-
const file = path.join(directory, entry.name);
|
|
71
|
-
if (depth < 3 && entry.isDirectory() && /^\d{2,4}$/.test(entry.name)) queue.push({ directory: file, depth: depth + 1 });
|
|
72
|
-
if (depth !== 3 || !entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
73
|
-
const fd = fs.openSync(file, "r");
|
|
74
|
-
let first;
|
|
75
|
-
try { const data = Buffer.alloc(65536); const size = fs.readSync(fd, data); first = JSON.parse(data.subarray(0, size).toString("utf8").split("\n")[0]); }
|
|
76
|
-
catch { continue; }
|
|
77
|
-
finally { fs.closeSync(fd); }
|
|
78
|
-
const session = first.payload;
|
|
79
|
-
if (first.type === "session_meta" && session?.originator === "codex_vscode" && session.source === "vscode" && samePath(session.cwd, cwd)) rows.push({ threadId: session.id, cwd: session.cwd });
|
|
80
|
-
}
|
|
64
|
+
for (const first of await readRolloutHeads(sessions)) {
|
|
65
|
+
const session = first?.payload;
|
|
66
|
+
if (first?.type === "session_meta" && session?.originator === "codex_vscode" && session.source === "vscode" && samePath(session.cwd, cwd)) rows.push({ threadId: session.id, cwd: session.cwd });
|
|
81
67
|
}
|
|
82
68
|
return rows.slice(-50);
|
|
83
69
|
}
|
|
@@ -95,7 +81,7 @@ tool("vscode_bridge_status", "Verify this extension caller and list same-project
|
|
|
95
81
|
const ipc = await new VSCodeIpc().connect();
|
|
96
82
|
try {
|
|
97
83
|
const tasks = [];
|
|
98
|
-
for (const candidate of taskCandidates(sender.cwd)) {
|
|
84
|
+
for (const candidate of await taskCandidates(sender.cwd)) {
|
|
99
85
|
try { await ipc.owner(candidate.threadId); tasks.push(candidate); } catch {}
|
|
100
86
|
}
|
|
101
87
|
return { host, cwd: sender.cwd, tasks };
|