@minhspark/codex-mcp-bridge 1.22.13 → 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.
@@ -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
- // Scan the complete history, but retain at most one bounded JSONL record.
9
- // Skipping the middle of a rollout would miss duplicate identities or corruption.
10
- export function scanRollout(file, { maxRecordBytes, onRecord, onChunk = () => {} }) {
11
- if (!Number.isSafeInteger(maxRecordBytes) || maxRecordBytes < 1) throw new Error("The rollout record read limit is invalid");
12
- const named = fs.lstatSync(file, { bigint: true });
13
- if (!named.isFile() || named.isSymbolicLink()) throw new Error("The rollout is not a regular file");
14
- const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
15
- try {
16
- const before = fs.fstatSync(descriptor, { bigint: true });
17
- if (!before.isFile()) throw new Error("The opened rollout is not a regular file");
18
- if (!sameVersion(named, before)) throw new Error("The rollout changed while opening");
19
- if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("The rollout exceeds the safe size range");
20
- const size = Number(before.size);
21
- if (size === 0) throw new Error("The rollout is empty (0 bytes)");
22
- let parts = [], length = 0, start = 0, position = 0;
23
- const decoder = new TextDecoder("utf-8", { fatal: true });
24
- while (position < size) {
25
- const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, size - position));
26
- const count = fs.readSync(descriptor, buffer, 0, buffer.length, position);
27
- if (!count) throw new Error("The rollout changed while reading");
28
- const chunk = buffer.subarray(0, count);
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
- position += count;
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
- if (start !== size) throw new Error("The rollout has an incomplete final record");
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);
@@ -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
- matches.push(candidate);
36
- if (matches.length > 64) throw new Error("The Codex task exceeds the 64-segment discovery limit");
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.length) throw new Error(`No rollout matches the calling Codex task [check=rollout_discovery; directoriesScanned=${directories}; entriesScanned=${entries}; layout=sessions/YYYY/MM/DD/rollout-*-taskId[_continuationId].jsonl]`);
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
- const current = fs.lstatSync(file, { bigint: true });
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
+ }
@@ -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 response = await this.relay.requestDesktop(operation, args, {
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
- 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 });
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
 
@@ -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 { findRollout, readState, readCodexSenderContext } from "./codex-sender-context.mjs";
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 file = findRollout(sessions, threadId);
54
- const state = readState(file, 64 * 1024 * 1024);
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
- let count = 0;
65
- while (queue.length) {
66
- const { directory, depth } = queue.pop();
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 };