codex-overleaf-link 2.4.0 → 2.4.1

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.
@@ -25,12 +25,14 @@ const { createSubagentBroker } = require('./subagentBroker');
25
25
  const { prepareBinaryAssetChanges } = require('./nativeAssetTransfer');
26
26
  const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
27
27
  const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
28
- const { buildReadProgressRules, createReadProgressController } = require('./readProgressGuard');
28
+ const { buildReadProgressRules, createReadProgressController, createRunEventScope } = require('./readProgressGuard');
29
29
  const {
30
30
  createCodexIdleWatchdog,
31
31
  createOptionalTimeout,
32
32
  getAbortReason,
33
33
  parseOptionalPositiveInteger,
34
+ rejectPendingRequests,
35
+ stopCodexAppServer,
34
36
  throwIfAborted
35
37
  } = require('./codexSessionTiming');
36
38
 
@@ -806,23 +808,26 @@ function runCodexAppServerProcess(input) {
806
808
  const child = spawn(codexCommand, buildCodexAppServerArgs(input), {
807
809
  env: childEnv,
808
810
  shell: shouldUseShellForCommand(codexCommand, childEnv),
811
+ detached: process.platform !== 'win32',
809
812
  stdio: ['pipe', 'pipe', 'pipe']
810
813
  });
811
814
  const pending = new Map();
812
815
  let nextId = 1;
813
816
  let stdoutBuffer = '';
814
817
  let stderr = '';
815
- let activeThreadId = '';
818
+ let activeThreadId = String(input.threadId || '');
816
819
  let activeTurnId = '';
817
820
  let controlPublished = false;
818
821
  const assistantMessages = new Map();
819
822
  const assistantMessageOrder = [];
823
+ const eventScope = createRunEventScope(() => ({ threadId: activeThreadId, turnId: activeTurnId }));
820
824
  const readProgressController = createReadProgressController({
821
825
  input, request, fail,
822
826
  getTurn: () => ({ threadId: activeThreadId, turnId: activeTurnId }),
823
827
  emitEvent: (...args) => emitCodexEvent(input.emit, ...args)
824
828
  });
825
829
  let settled = false;
830
+ let stopping = false;
826
831
  // Two-layer timeout strategy:
827
832
  // 1. Optional absolute deadline (CODEX_OVERLEAF_CODEX_TIMEOUT_MS) —
828
833
  // legacy override; off by default. When set, the whole run must
@@ -847,23 +852,14 @@ function runCodexAppServerProcess(input) {
847
852
  }
848
853
  fail(new Error(`Codex app-server produced no events for ${ms}ms (idle watchdog); the run was aborted to release the project lock.`));
849
854
  });
850
- const onAbort = () => {
851
- const reason = getAbortReason(input.signal);
852
- if (!activeThreadId || !activeTurnId) {
853
- fail(reason);
854
- return;
855
- }
856
- Promise.race([
857
- interruptActiveTurn().catch(() => null),
858
- new Promise(resolveDelay => setTimeout(resolveDelay, 1500))
859
- ]).finally(() => fail(reason));
860
- };
855
+ const onAbort = () => fail(getAbortReason(input.signal));
861
856
  input.signal?.addEventListener('abort', onAbort, { once: true });
862
857
 
863
858
  child.stdout.setEncoding('utf8');
864
859
  child.stderr.setEncoding('utf8');
865
860
  child.stdout.on('data', chunk => {
866
- idleWatchdog.reset();
861
+ if (settled) return;
862
+ if (!stopping) idleWatchdog.reset();
867
863
  stdoutBuffer += chunk;
868
864
  const lines = stdoutBuffer.split(/\r?\n/);
869
865
  stdoutBuffer = lines.pop() || '';
@@ -876,6 +872,7 @@ function runCodexAppServerProcess(input) {
876
872
  child.stderr.on('data', chunk => {
877
873
  stderr += chunk;
878
874
  });
875
+ child.stdin.on?.('error', fail);
879
876
  child.on('error', fail);
880
877
  child.on('close', code => {
881
878
  if (settled) {
@@ -951,12 +948,14 @@ function runCodexAppServerProcess(input) {
951
948
  }
952
949
 
953
950
  function request(method, params) {
954
- idleWatchdog.reset();
951
+ if (settled || (stopping && method !== 'turn/interrupt')) return Promise.reject(new Error('Codex turn is stopping or has ended.'));
952
+ if (!stopping) idleWatchdog.reset();
955
953
  const id = nextId++;
956
954
  const message = { id, method, params };
957
955
  child.stdin.write(`${JSON.stringify(message)}\n`);
958
956
  return new Promise((resolveRequest, rejectRequest) => {
959
957
  pending.set(id, {
958
+ method, params,
960
959
  resolve: resolveRequest,
961
960
  reject: rejectRequest
962
961
  });
@@ -972,10 +971,12 @@ function runCodexAppServerProcess(input) {
972
971
  }
973
972
 
974
973
  function handleMessage(line) {
974
+ if (settled) return;
975
975
  let message;
976
976
  try {
977
977
  message = JSON.parse(line);
978
978
  } catch {
979
+ if (stopping) return;
979
980
  emitCodexEvent(input.emit, 'codex.session.raw', 'Codex app-server emitted non-JSON output', {
980
981
  text: truncateText(line, 1000)
981
982
  });
@@ -992,23 +993,29 @@ function runCodexAppServerProcess(input) {
992
993
  if (message.error) {
993
994
  pendingRequest.reject(new Error(message.error.message || JSON.stringify(message.error)));
994
995
  } else {
996
+ if (pendingRequest.method === 'thread/start' || pendingRequest.method === 'thread/resume') {
997
+ activeThreadId = message.result?.thread?.id || message.result?.threadId || pendingRequest.params?.threadId || activeThreadId;
998
+ }
999
+ if (pendingRequest.method === 'turn/start') activeTurnId = message.result?.turn?.id || activeTurnId;
995
1000
  pendingRequest.resolve(message.result);
996
1001
  }
997
1002
  return;
998
1003
  }
999
1004
 
1000
1005
  if (Object.prototype.hasOwnProperty.call(message, 'id') && message.method) {
1006
+ if (stopping) { response(message.id, { decision: 'decline' }); return; }
1001
1007
  handleServerRequest(message);
1002
1008
  return;
1003
1009
  }
1004
1010
 
1005
1011
  if (message.method) {
1012
+ if (stopping || !eventScope.accepts(message.params)) return;
1006
1013
  if (message.method === 'turn/started') {
1007
- activeThreadId = message.params?.thread?.id || message.params?.threadId || activeThreadId;
1008
- activeTurnId = message.params?.turn?.id || message.params?.turnId || activeTurnId;
1014
+ const threadId = message.params?.thread?.id || message.params?.threadId;
1015
+ if (!activeTurnId && threadId === activeThreadId) activeTurnId = message.params?.turn?.id || message.params?.turnId || '';
1009
1016
  publishActiveTurnControl();
1010
1017
  }
1011
- recordAssistantMessage(message);
1018
+ if (activeTurnId) recordAssistantMessage(message);
1012
1019
  // For `error` events surface the actual error text as the visible
1013
1020
  // title so the run timeline reads "Reconnecting... 2/5" instead of
1014
1021
  // a generic "error". Other methods continue to use the method name.
@@ -1019,9 +1026,9 @@ function runCodexAppServerProcess(input) {
1019
1026
  method: message.method,
1020
1027
  params: message.params || {}
1021
1028
  }, inferNotificationStatus(message));
1022
- if (message.method === 'item/completed' &&
1029
+ if (message.method === 'item/completed' && eventScope.owns(message.params) &&
1023
1030
  !readProgressController.observe(message.params?.item || {})) return;
1024
- if (message.method === 'turn/completed' && (!activeTurnId || message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
1031
+ if (message.method === 'turn/completed' && activeTurnId && (message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
1025
1032
  succeed();
1026
1033
  }
1027
1034
  if (message.method === 'error' && isTransientCodexAppServerError(message.params)) {
@@ -1084,7 +1091,7 @@ function runCodexAppServerProcess(input) {
1084
1091
  }
1085
1092
 
1086
1093
  function publishActiveTurnControl() {
1087
- if (controlPublished || !activeThreadId || !activeTurnId) {
1094
+ if (settled || stopping || controlPublished || !activeThreadId || !activeTurnId) {
1088
1095
  return;
1089
1096
  }
1090
1097
  controlPublished = true;
@@ -1100,6 +1107,7 @@ function runCodexAppServerProcess(input) {
1100
1107
  interrupt: () => interruptActiveTurn()
1101
1108
  });
1102
1109
  input.onControlReady?.(control);
1110
+ if (settled || stopping) return;
1103
1111
  emitCodexEvent(input.emit, 'codex.turn.bound', 'Codex turn is ready for follow-up guidance', {
1104
1112
  threadId: activeThreadId,
1105
1113
  turnId: activeTurnId
@@ -1117,16 +1125,13 @@ function runCodexAppServerProcess(input) {
1117
1125
  }
1118
1126
 
1119
1127
  function succeed() {
1120
- if (settled) {
1128
+ if (settled || stopping) {
1121
1129
  return;
1122
1130
  }
1123
1131
  settled = true;
1124
1132
  const turnEndedError = new Error('Codex turn ended before the pending app-server request completed.');
1125
1133
  turnEndedError.code = 'codex_turn_ended';
1126
- for (const pendingRequest of pending.values()) {
1127
- pendingRequest.reject(turnEndedError);
1128
- }
1129
- pending.clear();
1134
+ rejectPendingRequests(pending, turnEndedError);
1130
1135
  cleanup();
1131
1136
  child.kill('SIGTERM');
1132
1137
  resolve({
@@ -1136,19 +1141,17 @@ function runCodexAppServerProcess(input) {
1136
1141
  }
1137
1142
 
1138
1143
  function fail(error) {
1139
- if (settled) {
1140
- return;
1141
- }
1142
- settled = true;
1144
+ if (settled || stopping) return;
1145
+ stopping = true;
1143
1146
  cleanup();
1144
- for (const pendingRequest of pending.values()) {
1145
- pendingRequest.reject(error);
1146
- }
1147
- pending.clear();
1148
- if (child.exitCode === null && !child.killed) {
1149
- child.kill('SIGTERM');
1150
- }
1151
- reject(error);
1147
+ rejectPendingRequests(pending, error);
1148
+ return stopCodexAppServer(child, {
1149
+ interrupt: interruptActiveTurn, reason: error, processGroup: process.platform !== 'win32'
1150
+ }).then(() => error, stopError => stopError).then(finalError => {
1151
+ settled = true;
1152
+ rejectPendingRequests(pending, finalError);
1153
+ reject(finalError);
1154
+ });
1152
1155
  }
1153
1156
 
1154
1157
  function cleanup() {
@@ -36,10 +36,78 @@ function throwIfAborted(signal) {
36
36
  if (signal?.aborted) throw getAbortReason(signal);
37
37
  }
38
38
 
39
+ function rejectPendingRequests(pending, error) {
40
+ for (const entry of pending.values()) entry.reject(error);
41
+ pending.clear();
42
+ }
43
+
44
+ async function stopCodexAppServer(child, {
45
+ interrupt, reason, processGroup = false, platform = process.platform,
46
+ interruptTimeoutMs = 1500, terminateTimeoutMs = 500, forceTimeoutMs = 1500,
47
+ kill = process.kill.bind(process), taskkill = defaultTaskkill
48
+ } = {}) {
49
+ const pid = Number(child?.pid);
50
+ if (!Number.isInteger(pid) || pid <= 0) return;
51
+ const grouped = processGroup && platform !== 'win32';
52
+ const exited = () => child.exitCode != null || child.signalCode != null;
53
+ const groupGone = () => {
54
+ if (!grouped) return true;
55
+ try { kill(-pid, 0); return false; } catch (error) { return error?.code === 'ESRCH'; }
56
+ };
57
+ const stopped = () => exited() && groupGone();
58
+ const waitForStop = async timeoutMs => {
59
+ const deadline = Date.now() + timeoutMs;
60
+ while (!stopped() && Date.now() < deadline) {
61
+ await new Promise(resolve => setTimeout(resolve, Math.min(20, Math.max(1, deadline - Date.now()))));
62
+ }
63
+ return stopped();
64
+ };
65
+ const signal = value => {
66
+ if (grouped) {
67
+ try { kill(-pid, value); return; } catch (_error) { /* fall back to the owned child */ }
68
+ }
69
+ if (!exited()) {
70
+ try { child.kill(value); } catch (_error) { /* the exit check remains authoritative */ }
71
+ }
72
+ };
73
+ if (stopped()) return;
74
+ if (!exited() && typeof interrupt === 'function') {
75
+ let timer;
76
+ try {
77
+ await Promise.race([
78
+ Promise.resolve().then(interrupt).catch(() => {}),
79
+ new Promise(resolve => { timer = setTimeout(resolve, interruptTimeoutMs); })
80
+ ]);
81
+ } finally { clearTimeout(timer); }
82
+ }
83
+ if (platform === 'win32') {
84
+ if (!exited()) await taskkill(pid, forceTimeoutMs).catch(() => {});
85
+ } else {
86
+ signal('SIGTERM');
87
+ if (await waitForStop(terminateTimeoutMs)) return;
88
+ signal('SIGKILL');
89
+ }
90
+ if (!await waitForStop(forceTimeoutMs)) {
91
+ const error = new Error('Codex process exit could not be confirmed. Do not retry this project until the old process has stopped.');
92
+ error.code = 'codex_process_stop_unconfirmed';
93
+ error.cause = reason;
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ function defaultTaskkill(pid, timeout) {
99
+ return new Promise((resolve, reject) => {
100
+ require('node:child_process').execFile('taskkill', ['/pid', String(pid), '/t', '/f'],
101
+ { windowsHide: true, timeout }, error => error ? reject(error) : resolve());
102
+ });
103
+ }
104
+
39
105
  module.exports = {
40
106
  createCodexIdleWatchdog,
41
107
  createOptionalTimeout,
42
108
  getAbortReason,
43
109
  parseOptionalPositiveInteger,
110
+ rejectPendingRequests,
111
+ stopCodexAppServer,
44
112
  throwIfAborted
45
113
  };
@@ -19,6 +19,36 @@ function buildReadProgressRules() {
19
19
  ].join('\n');
20
20
  }
21
21
 
22
+ function createRunEventScope(getTurn = () => ({})) {
23
+ const owners = new Map();
24
+ const identity = params => ({
25
+ threadId: String(params?.threadId || params?.thread?.id || ''),
26
+ turnId: String(params?.turnId || params?.turn?.id || '')
27
+ });
28
+ const itemId = params => String(params?.itemId || params?.item?.id || '');
29
+ const matches = owner => {
30
+ const active = getTurn() || {};
31
+ return (!owner.threadId || owner.threadId === active.threadId)
32
+ && (!owner.turnId || !active.turnId || owner.turnId === active.turnId);
33
+ };
34
+ return {
35
+ accepts(params = {}) {
36
+ const owner = identity(params);
37
+ const id = itemId(params);
38
+ if (owner.threadId || owner.turnId) {
39
+ if (id) owners.set(id, owner);
40
+ return matches(owner);
41
+ }
42
+ return !owners.has(id) || matches(owners.get(id));
43
+ },
44
+ owns(params = {}) {
45
+ const explicit = identity(params);
46
+ const owner = explicit.threadId || explicit.turnId ? explicit : owners.get(itemId(params));
47
+ return Boolean((getTurn() || {}).turnId && owner?.threadId && matches(owner));
48
+ }
49
+ };
50
+ }
51
+
22
52
  function createReadProgressGuard(options = {}) {
23
53
  const settings = {
24
54
  minReadCommands: positiveInteger(options.minReadCommands, DEFAULT_MIN_READ_COMMANDS),
@@ -37,11 +67,19 @@ function createReadProgressGuard(options = {}) {
37
67
  let redundantStreak = 0;
38
68
  let postSteerRedundantStreak = 0;
39
69
  let steerIssued = false;
70
+ let steerAcknowledged = false;
40
71
  let abortIssued = false;
41
72
 
42
73
  return {
43
74
  observe(item) {
44
75
  if (abortIssued) return noAction();
76
+ if (item?.type === 'fileChange') {
77
+ coverageByFile.clear();
78
+ commandCounts.clear();
79
+ readCount = redundantStreak = postSteerRedundantStreak = 0;
80
+ steerIssued = steerAcknowledged = false;
81
+ return noAction();
82
+ }
45
83
  const inspection = extractReadInspection(item, settings.workspacePath);
46
84
  if (!inspection) {
47
85
  if (item?.type === 'commandExecution') {
@@ -52,8 +90,9 @@ function createReadProgressGuard(options = {}) {
52
90
  }
53
91
 
54
92
  readCount += 1;
55
- const signatureCount = (commandCounts.get(inspection.signature) || 0) + 1;
56
- commandCounts.set(inspection.signature, signatureCount);
93
+ const signatureKey = inspection.fileKey + '\0' + inspection.signature;
94
+ const signatureCount = (commandCounts.get(signatureKey) || 0) + 1;
95
+ commandCounts.set(signatureKey, signatureCount);
57
96
  let overlapRatio = 0;
58
97
  let redundant = signatureCount > 1;
59
98
 
@@ -85,6 +124,7 @@ function createReadProgressGuard(options = {}) {
85
124
  });
86
125
 
87
126
  if (steerIssued) {
127
+ if (!steerAcknowledged) return noAction(evidence);
88
128
  postSteerRedundantStreak = redundant ? postSteerRedundantStreak + 1 : 0;
89
129
  if (postSteerRedundantStreak >= settings.postSteerRedundantStreak) {
90
130
  abortIssued = true;
@@ -101,12 +141,20 @@ function createReadProgressGuard(options = {}) {
101
141
  return noAction(evidence);
102
142
  },
103
143
 
144
+ acknowledgeSteer() {
145
+ if (steerIssued && !abortIssued) {
146
+ steerAcknowledged = true;
147
+ postSteerRedundantStreak = 0;
148
+ }
149
+ },
150
+
104
151
  snapshot() {
105
152
  return {
106
153
  readCount,
107
154
  redundantStreak,
108
155
  postSteerRedundantStreak,
109
156
  steerIssued,
157
+ steerAcknowledged,
110
158
  abortIssued
111
159
  };
112
160
  }
@@ -120,9 +168,15 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
120
168
  });
121
169
  let pendingSteer = null;
122
170
  let steerInFlight = false;
171
+ let generation = 0;
123
172
 
124
173
  return {
125
174
  observe(item) {
175
+ if (item?.type === 'fileChange') {
176
+ generation += 1;
177
+ pendingSteer = null;
178
+ steerInFlight = false;
179
+ }
126
180
  const decision = guard.observe(item);
127
181
  if (decision.action === 'steer') {
128
182
  steer(decision);
@@ -153,6 +207,7 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
153
207
  return;
154
208
  }
155
209
  steerInFlight = true;
210
+ const steerGeneration = generation;
156
211
  emitEvent('codex.no_progress.steered', 'Repeated file reads detected; asking Codex to synthesize', {
157
212
  ...decision.evidence,
158
213
  guardAction: 'steer'
@@ -165,7 +220,10 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
165
220
  text: buildReadProgressSteerText(decision.evidence, input.mode),
166
221
  text_elements: []
167
222
  }]
223
+ }).then(() => {
224
+ if (steerGeneration === generation) guard.acknowledgeSteer();
168
225
  }).catch(error => {
226
+ if (steerGeneration !== generation) return;
169
227
  const progressError = createNoProgressError(decision.evidence);
170
228
  progressError.message += ` Automatic steering failed: ${String(error?.message || error).slice(0, 500)}`;
171
229
  fail(progressError);
@@ -175,30 +233,35 @@ function createReadProgressController({ input = {}, request, fail, getTurn, emit
175
233
 
176
234
  function extractReadInspection(item = {}, workspacePath = '') {
177
235
  if (item.type !== 'commandExecution') return null;
236
+ if (item.status && item.status !== 'completed') return null;
237
+ if (Number.isInteger(item.exitCode) && item.exitCode !== 0) return null;
178
238
  const command = String(item.command || '').trim();
179
239
  if (!command) return null;
240
+ const cwd = typeof item.cwd === 'string' && item.cwd ? path.resolve(workspacePath || '.', item.cwd) : workspacePath;
180
241
  const actions = Array.isArray(item.commandActions) ? item.commandActions : [];
181
- const readAction = actions.find(action => action?.type === 'read');
182
- if (!readAction && !looksLikeReadCommand(command)) return null;
183
-
242
+ const readActions = actions.filter(action => action?.type === 'read');
243
+ if (!readActions.length && !looksLikeReadCommand(command)) return null;
244
+ const paths = [...new Set(readActions.map(action => normalizeFileKey(action.path, cwd)).filter(Boolean))];
245
+ if (paths.length > 1) return null;
184
246
  const bounded = extractBoundedRange(command);
185
- const rawPath = bounded?.path || readAction?.path || '';
186
- const fileKey = normalizeFileKey(rawPath, workspacePath);
187
- const displayPath = safeDisplayPath(fileKey, workspacePath) || path.basename(fileKey) || 'project file';
188
- const range = bounded
189
- ? { startLine: bounded.startLine, endLine: bounded.endLine }
190
- : null;
247
+ const boundedKey = normalizeFileKey(bounded?.path, cwd);
248
+ if (boundedKey && paths[0] && boundedKey !== paths[0]) return null;
249
+ const fileKey = paths[0] || boundedKey;
250
+ if (!fileKey) return null;
191
251
  return {
192
- fileKey: fileKey || displayPath,
193
- displayPath,
194
- range,
252
+ fileKey,
253
+ displayPath: safeDisplayPath(fileKey, workspacePath) || path.basename(fileKey),
254
+ range: bounded && boundedKey === fileKey
255
+ ? { startLine: bounded.startLine, endLine: bounded.endLine } : null,
195
256
  signature: normalizeCommandSignature(command, workspacePath)
196
257
  };
197
258
  }
198
259
 
199
260
  function extractBoundedRange(command) {
200
261
  const source = String(command || '');
201
- const sedMatch = source.match(/\bsed\s+(?:-[A-Za-z]+\s+)*["']?(\d+)\s*,\s*(\d+)p["']?(?:\s+((?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s;&|]+)))?/i);
262
+ // Stream pagination and compound commands do not prove source-file ranges.
263
+ if (/[|;&<>\x60]/.test(source)) return null;
264
+ const sedMatch = source.match(/\bsed\s+(?:-[A-Za-z]+\s+)*["']?(\d+)\s*,\s*(\d+)p["']?\s+((?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s;&|]+))/i);
202
265
  if (sedMatch) {
203
266
  return normalizeRangeMatch(sedMatch[1], sedMatch[2], sedMatch[3]);
204
267
  }
@@ -212,13 +275,15 @@ function extractBoundedRange(command) {
212
275
  function normalizeRangeMatch(startValue, endValue, pathValue) {
213
276
  const startLine = Number(startValue);
214
277
  const endLine = Number(endValue);
278
+ const filePath = unquoteShellWord(pathValue);
279
+ if (!filePath || filePath === '-') return null;
215
280
  if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine) {
216
281
  return null;
217
282
  }
218
283
  return {
219
284
  startLine,
220
285
  endLine,
221
- path: unquoteShellWord(pathValue)
286
+ path: filePath
222
287
  };
223
288
  }
224
289
 
@@ -228,8 +293,9 @@ function looksLikeReadCommand(command) {
228
293
  }
229
294
 
230
295
  function normalizeFileKey(value, workspacePath) {
296
+ if (typeof value !== 'string') return '';
231
297
  const source = unquoteShellWord(value);
232
- if (!source) return '';
298
+ if (!source || source === '-' || source.startsWith('~') || /[$\x60*?\[\]{}]/.test(source)) return '';
233
299
  if (path.isAbsolute(source)) return path.normalize(source);
234
300
  return path.resolve(workspacePath || '.', source);
235
301
  }
@@ -355,6 +421,7 @@ module.exports = {
355
421
  createNoProgressError,
356
422
  createReadProgressController,
357
423
  createReadProgressGuard,
424
+ createRunEventScope,
358
425
  extractBoundedRange,
359
426
  extractReadInspection
360
427
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-overleaf-link",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",