codex-overleaf-link 2.1.4 → 2.2.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/README.md +27 -23
- package/extension/bootstrap/background.js +14 -1
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +11 -0
- package/extension/src/background.js +507 -8
- package/extension/src/backgroundUpdateCoordinator.js +284 -19
- package/extension/src/content/activeTurnControl.js +179 -0
- package/extension/src/content/composerPanel.js +19 -6
- package/extension/src/content/contentRuntime.js +335 -317
- package/extension/src/content/diagnosticsPanel.js +132 -1
- package/extension/src/content/markdownDomRenderer.js +451 -0
- package/extension/src/content/markdownText.js +86 -7
- package/extension/src/content/mathText.js +49 -4
- package/extension/src/content/modelPicker.js +14 -5
- package/extension/src/content/nativeChannel.js +7 -1
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/panelRenderer.js +112 -12
- package/extension/src/content/pendingInputView.js +106 -0
- package/extension/src/content/projectProviderSelection.js +68 -0
- package/extension/src/content/projectSessionCleanup.js +104 -0
- package/extension/src/content/providerSettingsCoordinator.js +170 -22
- package/extension/src/content/providerSettingsDialog.js +2 -2
- package/extension/src/content/recentProjects.js +24 -13
- package/extension/src/content/runController.js +2 -2
- package/extension/src/content/runGuidanceController.js +70 -0
- package/extension/src/content/runGuidanceView.js +48 -0
- package/extension/src/content/runQueueScheduler.js +260 -0
- package/extension/src/content/runTimelineView.js +33 -80
- package/extension/src/content/sessionManager.js +2 -2
- package/extension/src/content/sessionPersistence.js +62 -0
- package/extension/src/content/settingsPanel.js +1 -1
- package/extension/src/content/updateNotice.js +80 -1
- package/extension/src/page/writebackRouter.js +3 -9
- package/extension/src/shared/compatibility.js +2 -1
- package/extension/src/shared/i18n.js +54 -0
- package/extension/src/shared/providerProfiles.js +3 -2
- package/extension/src/shared/runInputQueue.js +253 -0
- package/extension/src/shared/sessionState.js +45 -22
- package/extension/src/shared/storageDb.js +39 -44
- package/extension/src/shared/storageMigration.js +123 -3
- package/extension/src/shared/storageRunActions.js +34 -1
- package/extension/src/shared/updateConsent.js +15 -4
- package/extension/styles/panel.css +370 -23
- package/extension/vendor/katex/katex.min.css +1 -1
- package/extension/vendor/markdown-it/LICENSE +22 -0
- package/extension/vendor/markdown-it/SOURCE.json +7 -0
- package/extension/vendor/markdown-it/VERSION +1 -0
- package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
- package/native-host/src/anthropicBridgeRequest.js +3 -3
- package/native-host/src/anthropicBridgeResponse.js +58 -17
- package/native-host/src/anthropicMessagesBridge.js +156 -23
- package/native-host/src/chatBridgeRequest.js +1 -1
- package/native-host/src/chatBridgeResponse.js +12 -8
- package/native-host/src/chatCompletionsBridge.js +22 -40
- package/native-host/src/codexPromptAssembly.js +3 -1
- package/native-host/src/codexProviderLaunch.js +6 -3
- package/native-host/src/codexSessionRunner.js +66 -66
- package/native-host/src/codexSessionTiming.js +45 -0
- package/native-host/src/diffEngine.js +10 -5
- package/native-host/src/index.js +58 -3
- package/native-host/src/managedInstall.js +45 -7
- package/native-host/src/providerProfile.js +2 -2
- package/native-host/src/providerStreamLifecycle.js +100 -0
- package/native-host/src/responsesPassthroughBridge.js +636 -0
- package/native-host/src/taskRunnerRuntime.js +81 -2
- package/native-host/src/updateArchive.js +4 -3
- package/native-host/src/updateManager.js +306 -19
- package/native-host/src/updateTrust.js +1 -1
- package/package.json +3 -1
|
@@ -22,6 +22,13 @@ const { createSubagentBroker } = require('./subagentBroker');
|
|
|
22
22
|
const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
|
|
23
23
|
const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
|
|
24
24
|
const { buildReadProgressRules, createReadProgressController } = require('./readProgressGuard');
|
|
25
|
+
const {
|
|
26
|
+
createCodexIdleWatchdog,
|
|
27
|
+
createOptionalTimeout,
|
|
28
|
+
getAbortReason,
|
|
29
|
+
parseOptionalPositiveInteger,
|
|
30
|
+
throwIfAborted
|
|
31
|
+
} = require('./codexSessionTiming');
|
|
25
32
|
|
|
26
33
|
const PARALLEL_SUBAGENTS_SKILL_ID = 'parallel-subagents';
|
|
27
34
|
|
|
@@ -30,7 +37,7 @@ const MAX_TURN_ATTACHMENT_BYTES = 12 * 1024 * 1024;
|
|
|
30
37
|
const MAX_TURN_ATTACHMENTS = 8;
|
|
31
38
|
const MAX_TURN_ATTACHMENT_TOTAL_BYTES = MAX_TURN_ATTACHMENT_BYTES * MAX_TURN_ATTACHMENTS;
|
|
32
39
|
|
|
33
|
-
async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, providerLaunch, signal } = {}) {
|
|
40
|
+
async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, providerLaunch, signal, onControlReady } = {}) {
|
|
34
41
|
throwIfAborted(signal);
|
|
35
42
|
const projectId = params.projectId || params.project?.projectId || params.project?.id || params.project?.url || 'overleaf-project';
|
|
36
43
|
const skillInvocation = normalizeSkillInvocation(params.skillInvocation);
|
|
@@ -162,6 +169,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
|
|
|
162
169
|
// While subagent workers are active, the parent idle watchdog resets
|
|
163
170
|
// instead of failing the run (spec P2-7 fix).
|
|
164
171
|
hasExternalActivity: subagentBroker ? () => subagentBroker.hasActiveWorkers() : undefined,
|
|
172
|
+
onControlReady,
|
|
165
173
|
signal
|
|
166
174
|
});
|
|
167
175
|
} catch (error) {
|
|
@@ -252,7 +260,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
|
|
|
252
260
|
if (change.type === 'write' && typeof change.previousContent === 'string') {
|
|
253
261
|
return {
|
|
254
262
|
...change,
|
|
255
|
-
diff: computeLineDiff(change.previousContent, change.content),
|
|
263
|
+
diff: computeLineDiff(change.previousContent, change.content, 3, params.locale === 'zh' ? 'zh' : 'en'),
|
|
256
264
|
patches: computeTextPatches(change.previousContent, change.content)
|
|
257
265
|
};
|
|
258
266
|
}
|
|
@@ -808,6 +816,7 @@ function runCodexAppServerProcess(input) {
|
|
|
808
816
|
let stderr = '';
|
|
809
817
|
let activeThreadId = '';
|
|
810
818
|
let activeTurnId = '';
|
|
819
|
+
let controlPublished = false;
|
|
811
820
|
const assistantMessages = new Map();
|
|
812
821
|
const assistantMessageOrder = [];
|
|
813
822
|
const readProgressController = createReadProgressController({
|
|
@@ -841,7 +850,15 @@ function runCodexAppServerProcess(input) {
|
|
|
841
850
|
fail(new Error(`Codex app-server produced no events for ${ms}ms (idle watchdog); the run was aborted to release the project lock.`));
|
|
842
851
|
});
|
|
843
852
|
const onAbort = () => {
|
|
844
|
-
|
|
853
|
+
const reason = getAbortReason(input.signal);
|
|
854
|
+
if (!activeThreadId || !activeTurnId) {
|
|
855
|
+
fail(reason);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
Promise.race([
|
|
859
|
+
interruptActiveTurn().catch(() => null),
|
|
860
|
+
new Promise(resolveDelay => setTimeout(resolveDelay, 1500))
|
|
861
|
+
]).finally(() => fail(reason));
|
|
845
862
|
};
|
|
846
863
|
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
847
864
|
|
|
@@ -910,6 +927,7 @@ function runCodexAppServerProcess(input) {
|
|
|
910
927
|
|
|
911
928
|
const turnResponse = await startTurnWithSummaryFallback(activeThreadId);
|
|
912
929
|
activeTurnId = turnResponse?.turn?.id || '';
|
|
930
|
+
publishActiveTurnControl();
|
|
913
931
|
readProgressController.flush();
|
|
914
932
|
}
|
|
915
933
|
|
|
@@ -935,6 +953,7 @@ function runCodexAppServerProcess(input) {
|
|
|
935
953
|
}
|
|
936
954
|
|
|
937
955
|
function request(method, params) {
|
|
956
|
+
idleWatchdog.reset();
|
|
938
957
|
const id = nextId++;
|
|
939
958
|
const message = { id, method, params };
|
|
940
959
|
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
@@ -986,6 +1005,11 @@ function runCodexAppServerProcess(input) {
|
|
|
986
1005
|
}
|
|
987
1006
|
|
|
988
1007
|
if (message.method) {
|
|
1008
|
+
if (message.method === 'turn/started') {
|
|
1009
|
+
activeThreadId = message.params?.thread?.id || message.params?.threadId || activeThreadId;
|
|
1010
|
+
activeTurnId = message.params?.turn?.id || message.params?.turnId || activeTurnId;
|
|
1011
|
+
publishActiveTurnControl();
|
|
1012
|
+
}
|
|
989
1013
|
recordAssistantMessage(message);
|
|
990
1014
|
// For `error` events surface the actual error text as the visible
|
|
991
1015
|
// title so the run timeline reads "Reconnecting... 2/5" instead of
|
|
@@ -1061,11 +1085,50 @@ function runCodexAppServerProcess(input) {
|
|
|
1061
1085
|
assistantMessages.set(itemId, text);
|
|
1062
1086
|
}
|
|
1063
1087
|
|
|
1088
|
+
function publishActiveTurnControl() {
|
|
1089
|
+
if (controlPublished || !activeThreadId || !activeTurnId) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
controlPublished = true;
|
|
1093
|
+
const control = Object.freeze({
|
|
1094
|
+
threadId: activeThreadId,
|
|
1095
|
+
turnId: activeTurnId,
|
|
1096
|
+
steer: ({ input: steerInput, clientUserMessageId } = {}) => request('turn/steer', {
|
|
1097
|
+
threadId: activeThreadId,
|
|
1098
|
+
expectedTurnId: activeTurnId,
|
|
1099
|
+
input: Array.isArray(steerInput) ? steerInput : [],
|
|
1100
|
+
clientUserMessageId: clientUserMessageId || undefined
|
|
1101
|
+
}),
|
|
1102
|
+
interrupt: () => interruptActiveTurn()
|
|
1103
|
+
});
|
|
1104
|
+
input.onControlReady?.(control);
|
|
1105
|
+
emitCodexEvent(input.emit, 'codex.turn.bound', 'Codex turn is ready for follow-up guidance', {
|
|
1106
|
+
threadId: activeThreadId,
|
|
1107
|
+
turnId: activeTurnId
|
|
1108
|
+
}, 'running');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function interruptActiveTurn() {
|
|
1112
|
+
if (!activeThreadId || !activeTurnId || settled) {
|
|
1113
|
+
return Promise.resolve({ interrupted: false });
|
|
1114
|
+
}
|
|
1115
|
+
return request('turn/interrupt', {
|
|
1116
|
+
threadId: activeThreadId,
|
|
1117
|
+
turnId: activeTurnId
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1064
1121
|
function succeed() {
|
|
1065
1122
|
if (settled) {
|
|
1066
1123
|
return;
|
|
1067
1124
|
}
|
|
1068
1125
|
settled = true;
|
|
1126
|
+
const turnEndedError = new Error('Codex turn ended before the pending app-server request completed.');
|
|
1127
|
+
turnEndedError.code = 'codex_turn_ended';
|
|
1128
|
+
for (const pendingRequest of pending.values()) {
|
|
1129
|
+
pendingRequest.reject(turnEndedError);
|
|
1130
|
+
}
|
|
1131
|
+
pending.clear();
|
|
1069
1132
|
cleanup();
|
|
1070
1133
|
child.kill('SIGTERM');
|
|
1071
1134
|
resolve({
|
|
@@ -1338,69 +1401,6 @@ function tokenizeShellCommand(command) {
|
|
|
1338
1401
|
return tokens;
|
|
1339
1402
|
}
|
|
1340
1403
|
|
|
1341
|
-
function createOptionalTimeout(value, onTimeout) {
|
|
1342
|
-
const timeoutMs = parseOptionalPositiveInteger(value);
|
|
1343
|
-
if (!timeoutMs) {
|
|
1344
|
-
return {
|
|
1345
|
-
cancel() {}
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
const timer = setTimeout(() => onTimeout(timeoutMs), timeoutMs);
|
|
1349
|
-
return {
|
|
1350
|
-
cancel() {
|
|
1351
|
-
clearTimeout(timer);
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
// Idle-style watchdog: fires only after the app-server has been silent for
|
|
1357
|
-
// `idleMs`. Callers must invoke .reset() on every signal of liveness
|
|
1358
|
-
// (incoming line, outgoing request). cancel() stops the timer on settle.
|
|
1359
|
-
// Returns no-op when idleMs is non-positive (defensive guard against bad env
|
|
1360
|
-
// var values).
|
|
1361
|
-
function createCodexIdleWatchdog(idleMs, onIdle) {
|
|
1362
|
-
if (!(idleMs > 0)) {
|
|
1363
|
-
return {
|
|
1364
|
-
reset() {},
|
|
1365
|
-
cancel() {}
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
let timer = setTimeout(() => onIdle(idleMs), idleMs);
|
|
1369
|
-
return {
|
|
1370
|
-
reset() {
|
|
1371
|
-
clearTimeout(timer);
|
|
1372
|
-
timer = setTimeout(() => onIdle(idleMs), idleMs);
|
|
1373
|
-
},
|
|
1374
|
-
cancel() {
|
|
1375
|
-
clearTimeout(timer);
|
|
1376
|
-
}
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
function parseOptionalPositiveInteger(value) {
|
|
1381
|
-
if (value === undefined || value === null || value === '') {
|
|
1382
|
-
return 0;
|
|
1383
|
-
}
|
|
1384
|
-
const number = Number(value);
|
|
1385
|
-
return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
function throwIfAborted(signal) {
|
|
1389
|
-
if (!signal?.aborted) {
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
throw getAbortReason(signal);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
function getAbortReason(signal) {
|
|
1396
|
-
if (signal?.reason instanceof Error) {
|
|
1397
|
-
return signal.reason;
|
|
1398
|
-
}
|
|
1399
|
-
const error = new Error('Codex run was cancelled by the user');
|
|
1400
|
-
error.code = 'codex_cancelled';
|
|
1401
|
-
return error;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
1404
|
function buildFinalAssistantMessage(messages = new Map(), order = []) {
|
|
1405
1405
|
const values = [];
|
|
1406
1406
|
const seenIds = new Set();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createOptionalTimeout(value, onTimeout) {
|
|
4
|
+
const timeoutMs = parseOptionalPositiveInteger(value);
|
|
5
|
+
if (!timeoutMs) return { cancel() {} };
|
|
6
|
+
const timer = setTimeout(() => onTimeout(timeoutMs), timeoutMs);
|
|
7
|
+
return { cancel: () => clearTimeout(timer) };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createCodexIdleWatchdog(idleMs, onIdle) {
|
|
11
|
+
if (!(idleMs > 0)) return { reset() {}, cancel() {} };
|
|
12
|
+
let timer = setTimeout(() => onIdle(idleMs), idleMs);
|
|
13
|
+
return {
|
|
14
|
+
reset() {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
timer = setTimeout(() => onIdle(idleMs), idleMs);
|
|
17
|
+
},
|
|
18
|
+
cancel: () => clearTimeout(timer)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseOptionalPositiveInteger(value) {
|
|
23
|
+
if (value === undefined || value === null || value === '') return 0;
|
|
24
|
+
const number = Number(value);
|
|
25
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getAbortReason(signal) {
|
|
29
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
30
|
+
const error = new Error('Codex run was cancelled by the user');
|
|
31
|
+
error.code = 'codex_cancelled';
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function throwIfAborted(signal) {
|
|
36
|
+
if (signal?.aborted) throw getAbortReason(signal);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
createCodexIdleWatchdog,
|
|
41
|
+
createOptionalTimeout,
|
|
42
|
+
getAbortReason,
|
|
43
|
+
parseOptionalPositiveInteger,
|
|
44
|
+
throwIfAborted
|
|
45
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
function computeLineDiff(oldText, newText, contextLines = 3) {
|
|
3
|
+
function computeLineDiff(oldText, newText, contextLines = 3, locale = 'en') {
|
|
4
4
|
const oldLines = splitLines(oldText);
|
|
5
5
|
const newLines = splitLines(newText);
|
|
6
6
|
|
|
@@ -12,23 +12,28 @@ function computeLineDiff(oldText, newText, contextLines = 3) {
|
|
|
12
12
|
const MAX_DIFF_PRODUCT = 4000000;
|
|
13
13
|
|
|
14
14
|
if (oldLines.length > MAX_LINES || newLines.length > MAX_LINES || oldLines.length * newLines.length > MAX_DIFF_PRODUCT) {
|
|
15
|
-
return buildTruncatedResult(oldLines.length, newLines.length);
|
|
15
|
+
return buildTruncatedResult(oldLines.length, newLines.length, locale);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const edits = myersDiff(oldLines, newLines);
|
|
19
19
|
if (edits === null) {
|
|
20
|
-
return buildTruncatedResult(oldLines.length, newLines.length);
|
|
20
|
+
return buildTruncatedResult(oldLines.length, newLines.length, locale);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
return buildHunks(oldLines, newLines, edits, contextLines);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function buildTruncatedResult(oldCount, newCount) {
|
|
26
|
+
function buildTruncatedResult(oldCount, newCount, locale = 'en') {
|
|
27
27
|
return [{
|
|
28
28
|
startA: 1,
|
|
29
29
|
startB: 1,
|
|
30
30
|
truncated: true,
|
|
31
|
-
lines: [{
|
|
31
|
+
lines: [{
|
|
32
|
+
type: 'context',
|
|
33
|
+
text: locale === 'zh'
|
|
34
|
+
? `文件改动较大(${oldCount} → ${newCount} 行),请在 Overleaf 中查看完整差异。`
|
|
35
|
+
: `Large file change (${oldCount} → ${newCount} lines). Review the full diff in Overleaf.`
|
|
36
|
+
}]
|
|
32
37
|
}];
|
|
33
38
|
}
|
|
34
39
|
|
package/native-host/src/index.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
const { decodeFrames, encodeMessage } = require('./nativeMessaging');
|
|
5
5
|
const { logDebug } = require('./debugLog');
|
|
6
6
|
const { handleRequest } = require('./taskRunner');
|
|
7
|
-
const { getActiveNativeWorkState } = require('./taskRunnerRuntime');
|
|
7
|
+
const { abortAllActiveOperations, getActiveNativeWorkState } = require('./taskRunnerRuntime');
|
|
8
8
|
const { handleUpdateRequest, isUpdateMethod } = require('./updateManager');
|
|
9
9
|
const { buildNativeRuntimeEnv, summarizeNativeEnvironment } = require('./nativeEnvironment');
|
|
10
10
|
|
|
11
11
|
let buffered = Buffer.alloc(0);
|
|
12
|
+
let stdoutUnavailable = false;
|
|
13
|
+
let nativeDisconnectHandled = false;
|
|
12
14
|
const runtimeEnv = buildNativeRuntimeEnv(process.env);
|
|
13
15
|
Object.assign(process.env, runtimeEnv);
|
|
14
16
|
logDebug('environment.ready', summarizeNativeEnvironment(runtimeEnv));
|
|
@@ -71,10 +73,24 @@ async function handleDecodedMessage(message) {
|
|
|
71
73
|
|
|
72
74
|
process.stdin.on('error', error => {
|
|
73
75
|
logDebug('stdin.error', { message: error.message });
|
|
74
|
-
|
|
76
|
+
handleNativeDisconnect('stdin_error', error);
|
|
77
|
+
});
|
|
78
|
+
process.stdin.on('end', () => handleNativeDisconnect('stdin_end'));
|
|
79
|
+
process.stdin.on('close', () => handleNativeDisconnect('stdin_close'));
|
|
80
|
+
|
|
81
|
+
process.stdout.on('error', error => {
|
|
82
|
+
if (isExpectedNativeDisconnect(error)) {
|
|
83
|
+
handleNativeDisconnect('stdout_disconnected', error);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
logDebug('stdout.error', { code: error.code, message: error.message, stack: error.stack });
|
|
75
87
|
});
|
|
76
88
|
|
|
77
89
|
process.on('uncaughtException', error => {
|
|
90
|
+
if (isExpectedNativeDisconnect(error)) {
|
|
91
|
+
handleNativeDisconnect('uncaught_native_disconnect', error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
78
94
|
logDebug('process.uncaught_exception', {
|
|
79
95
|
message: error.message,
|
|
80
96
|
stack: error.stack
|
|
@@ -91,6 +107,10 @@ process.on('unhandledRejection', reason => {
|
|
|
91
107
|
});
|
|
92
108
|
|
|
93
109
|
function writeResponse(response) {
|
|
110
|
+
if (stdoutUnavailable || process.stdout.destroyed || process.stdout.writableEnded) {
|
|
111
|
+
logDebug('stdout.write_skipped', { ok: response?.ok, code: response?.error?.code });
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
94
114
|
let frame;
|
|
95
115
|
try {
|
|
96
116
|
frame = encodeMessage(response);
|
|
@@ -110,7 +130,42 @@ function writeResponse(response) {
|
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
logDebug('stdout.write', { bytes: frame.length, ok: response?.ok, code: response?.error?.code });
|
|
113
|
-
|
|
133
|
+
try {
|
|
134
|
+
process.stdout.write(frame, error => {
|
|
135
|
+
if (!error) return;
|
|
136
|
+
if (isExpectedNativeDisconnect(error)) {
|
|
137
|
+
handleNativeDisconnect('stdout_write_disconnected', error);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
logDebug('stdout.write_failed', { code: error.code, message: error.message });
|
|
141
|
+
});
|
|
142
|
+
return true;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (isExpectedNativeDisconnect(error)) {
|
|
145
|
+
handleNativeDisconnect('stdout_write_disconnected', error);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isExpectedNativeDisconnect(error) {
|
|
153
|
+
return ['EPIPE', 'ERR_STREAM_DESTROYED', 'ERR_STREAM_WRITE_AFTER_END'].includes(String(error?.code || ''));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handleNativeDisconnect(source, error) {
|
|
157
|
+
if (nativeDisconnectHandled) return;
|
|
158
|
+
nativeDisconnectHandled = true;
|
|
159
|
+
stdoutUnavailable = true;
|
|
160
|
+
logDebug('native_transport.disconnected', {
|
|
161
|
+
source,
|
|
162
|
+
code: error?.code,
|
|
163
|
+
message: error?.message
|
|
164
|
+
});
|
|
165
|
+
abortAllActiveOperations('Native messaging transport disconnected.');
|
|
166
|
+
process.exitCode = 0;
|
|
167
|
+
const exitTimer = setTimeout(() => process.exit(0), 1500);
|
|
168
|
+
exitTimer.unref?.();
|
|
114
169
|
}
|
|
115
170
|
|
|
116
171
|
function buildOversizeResponseFallback(response, error) {
|
|
@@ -32,6 +32,8 @@ function installManagedDistribution(options = {}) {
|
|
|
32
32
|
if (!parseSemver(version)) {
|
|
33
33
|
throw updateError('managed_version_invalid', 'Managed installation requires a stable semantic version.');
|
|
34
34
|
}
|
|
35
|
+
const releaseRef = String(options.releaseRef || process.env.CODEX_OVERLEAF_RELEASE_REF || process.env.CODEX_OVERLEAF_REF || `v${version}`);
|
|
36
|
+
const releaseChannel = options.releaseChannel || (releaseRef === `v${version}` ? 'stable' : 'prerelease');
|
|
35
37
|
const platform = options.platform || process.platform;
|
|
36
38
|
const browser = options.browser || 'chrome';
|
|
37
39
|
const env = options.env || process.env;
|
|
@@ -52,7 +54,14 @@ function installManagedDistribution(options = {}) {
|
|
|
52
54
|
const unique = process.pid + '-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
|
53
55
|
const extensionStage = path.join(parent, '.extension.staging-' + unique);
|
|
54
56
|
const extensionRollback = path.join(parent, '.extension.rollback-' + unique);
|
|
55
|
-
buildManagedExtensionTree({
|
|
57
|
+
buildManagedExtensionTree({
|
|
58
|
+
packageRoot,
|
|
59
|
+
targetRoot: extensionStage,
|
|
60
|
+
version,
|
|
61
|
+
releaseRef,
|
|
62
|
+
releaseChannel,
|
|
63
|
+
allowedFiles: options.allowedFiles
|
|
64
|
+
});
|
|
56
65
|
|
|
57
66
|
const previousExtensionMarker = readManagedMarker(extensionRoot, EXTENSION_MARKER);
|
|
58
67
|
if (fs.existsSync(extensionRoot) && !isManagedMarker(previousExtensionMarker, 'extension')) {
|
|
@@ -75,7 +84,7 @@ function installManagedDistribution(options = {}) {
|
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
try {
|
|
78
|
-
installManagedNativeVersion({ packageRoot, nativeRoot, version });
|
|
87
|
+
installManagedNativeVersion({ packageRoot, nativeRoot, version, releaseRef, releaseChannel });
|
|
79
88
|
installManagedNativeRegistration({
|
|
80
89
|
browser,
|
|
81
90
|
env,
|
|
@@ -111,6 +120,8 @@ function buildManagedExtensionTree(options = {}) {
|
|
|
111
120
|
const packageRoot = path.resolve(options.packageRoot || path.resolve(__dirname, '../..'));
|
|
112
121
|
const targetRoot = path.resolve(String(options.targetRoot || ''));
|
|
113
122
|
const version = String(options.version || readJson(path.join(packageRoot, 'package.json')).version || '');
|
|
123
|
+
const releaseRef = String(options.releaseRef || `v${version}`);
|
|
124
|
+
const releaseChannel = options.releaseChannel || (releaseRef === `v${version}` ? 'stable' : 'prerelease');
|
|
114
125
|
if (!parseSemver(version)) {
|
|
115
126
|
throw new Error('Managed extension version is invalid.');
|
|
116
127
|
}
|
|
@@ -133,17 +144,20 @@ function buildManagedExtensionTree(options = {}) {
|
|
|
133
144
|
copyPackageTree(packageRoot, 'extension/styles', targetRoot, 'runtime/styles', { allowedFiles });
|
|
134
145
|
copyPackageTree(packageRoot, 'extension/vendor', targetRoot, 'runtime/vendor', { allowedFiles });
|
|
135
146
|
copyPackageFile(packageRoot, 'extension/runtime-manifest.json', targetRoot, 'runtime/runtime-manifest.json', allowedFiles);
|
|
147
|
+
rewriteManagedRuntimeAssetPaths(targetRoot);
|
|
136
148
|
fs.writeFileSync(path.join(targetRoot, EXTENSION_MARKER), JSON.stringify({
|
|
137
149
|
managedBy: MANAGED_BY,
|
|
138
150
|
kind: 'extension',
|
|
139
|
-
bootstrapProtocol:
|
|
151
|
+
bootstrapProtocol: 2,
|
|
140
152
|
version,
|
|
153
|
+
releaseRef,
|
|
154
|
+
releaseChannel,
|
|
141
155
|
installedAt: new Date().toISOString()
|
|
142
156
|
}, null, 2) + '\n', 'utf8');
|
|
143
157
|
return { targetRoot, version };
|
|
144
158
|
}
|
|
145
159
|
|
|
146
|
-
function installManagedNativeVersion({ packageRoot, nativeRoot, version }) {
|
|
160
|
+
function installManagedNativeVersion({ packageRoot, nativeRoot, version, releaseRef = `v${version}`, releaseChannel = 'stable' }) {
|
|
147
161
|
const marker = readManagedMarker(nativeRoot, NATIVE_MARKER);
|
|
148
162
|
if (fs.existsSync(nativeRoot) && !isManagedMarker(marker, 'native')) {
|
|
149
163
|
throw new Error('Refusing to replace an unmarked managed native root: ' + nativeRoot);
|
|
@@ -157,7 +171,11 @@ function installManagedNativeVersion({ packageRoot, nativeRoot, version }) {
|
|
|
157
171
|
for (const entry of buildRuntimeFileManifest({ packageRoot })) {
|
|
158
172
|
copyFile(entry.sourcePath, path.join(stage, entry.relativePath));
|
|
159
173
|
}
|
|
160
|
-
fs.writeFileSync(path.join(stage, '.codex-overleaf-version.json'), JSON.stringify({
|
|
174
|
+
fs.writeFileSync(path.join(stage, '.codex-overleaf-version.json'), JSON.stringify({
|
|
175
|
+
version,
|
|
176
|
+
releaseRef,
|
|
177
|
+
releaseChannel
|
|
178
|
+
}, null, 2) + '\n');
|
|
161
179
|
if (fs.existsSync(target)) {
|
|
162
180
|
safeRemove(target);
|
|
163
181
|
}
|
|
@@ -175,7 +193,7 @@ function installManagedNativeVersion({ packageRoot, nativeRoot, version }) {
|
|
|
175
193
|
fs.writeFileSync(path.join(nativeRoot, NATIVE_MARKER), JSON.stringify({
|
|
176
194
|
managedBy: MANAGED_BY,
|
|
177
195
|
kind: 'native',
|
|
178
|
-
bootstrapProtocol:
|
|
196
|
+
bootstrapProtocol: 2,
|
|
179
197
|
version,
|
|
180
198
|
installedAt: new Date().toISOString()
|
|
181
199
|
}, null, 2) + '\n', 'utf8');
|
|
@@ -330,6 +348,24 @@ function copyFile(source, target) {
|
|
|
330
348
|
fs.chmodSync(target, fs.statSync(source).mode & 0o777);
|
|
331
349
|
}
|
|
332
350
|
|
|
351
|
+
function rewriteManagedRuntimeAssetPaths(targetRoot) {
|
|
352
|
+
const cssPath = path.join(targetRoot, 'runtime/vendor/katex/katex.min.css');
|
|
353
|
+
if (!fs.existsSync(cssPath)) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const sourcePrefix = 'chrome-extension://__MSG_@@extension_id__/vendor/katex/fonts/';
|
|
357
|
+
const managedPrefix = 'chrome-extension://__MSG_@@extension_id__/runtime/vendor/katex/fonts/';
|
|
358
|
+
const css = fs.readFileSync(cssPath, 'utf8');
|
|
359
|
+
if (!css.includes(sourcePrefix)) {
|
|
360
|
+
throw new Error('Managed KaTeX CSS does not contain the expected source font prefix.');
|
|
361
|
+
}
|
|
362
|
+
const rewritten = css.split(sourcePrefix).join(managedPrefix);
|
|
363
|
+
if (rewritten.includes(sourcePrefix)) {
|
|
364
|
+
throw new Error('Managed KaTeX CSS retained a source-only font path.');
|
|
365
|
+
}
|
|
366
|
+
fs.writeFileSync(cssPath, rewritten, 'utf8');
|
|
367
|
+
}
|
|
368
|
+
|
|
333
369
|
function normalizeAllowedFiles(value) {
|
|
334
370
|
if (!value) return null;
|
|
335
371
|
return value instanceof Set ? value : new Set(value);
|
|
@@ -344,7 +380,9 @@ function readManagedMarker(root, markerName) {
|
|
|
344
380
|
}
|
|
345
381
|
|
|
346
382
|
function isManagedMarker(marker, kind) {
|
|
347
|
-
return marker?.managedBy === MANAGED_BY &&
|
|
383
|
+
return marker?.managedBy === MANAGED_BY &&
|
|
384
|
+
marker?.kind === kind &&
|
|
385
|
+
(marker?.bootstrapProtocol === 1 || marker?.bootstrapProtocol === 2);
|
|
348
386
|
}
|
|
349
387
|
|
|
350
388
|
function readVersionPointer(nativeRoot, name) {
|
|
@@ -407,8 +407,8 @@ function normalizeUpstreamResponseMode(value, allowAuto) {
|
|
|
407
407
|
|
|
408
408
|
function normalizeMaxOutputTokens(value) {
|
|
409
409
|
const number = Number(value);
|
|
410
|
-
if (!Number.isFinite(number) || number === 8192) return
|
|
411
|
-
return Math.min(
|
|
410
|
+
if (!Number.isFinite(number) || number === 8192 || number === 32768) return 65536;
|
|
411
|
+
return Math.min(65536, Math.max(256, Math.floor(number)));
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
function normalizeRequiredText(value, code, message, maxLength) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 120000;
|
|
4
|
+
const DEFAULT_TOTAL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
5
|
+
const MAX_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
6
|
+
const MAX_TOTAL_TIMEOUT_MS = 60 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
function createProviderStreamLifecycle({
|
|
9
|
+
controller,
|
|
10
|
+
idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
11
|
+
totalTimeoutMs = DEFAULT_TOTAL_TIMEOUT_MS,
|
|
12
|
+
onAbort = () => {}
|
|
13
|
+
} = {}) {
|
|
14
|
+
if (!controller?.signal || typeof controller.abort !== 'function') {
|
|
15
|
+
throw new TypeError('A provider stream lifecycle requires an AbortController.');
|
|
16
|
+
}
|
|
17
|
+
let idleTimeout = null;
|
|
18
|
+
let totalTimeout = null;
|
|
19
|
+
let abortReason = '';
|
|
20
|
+
let started = false;
|
|
21
|
+
let finished = false;
|
|
22
|
+
|
|
23
|
+
const clearTimers = () => {
|
|
24
|
+
clearTimeout(idleTimeout);
|
|
25
|
+
clearTimeout(totalTimeout);
|
|
26
|
+
idleTimeout = null;
|
|
27
|
+
totalTimeout = null;
|
|
28
|
+
};
|
|
29
|
+
const abort = reason => {
|
|
30
|
+
if (finished || controller.signal.aborted) return;
|
|
31
|
+
abortReason = String(reason || 'cancelled');
|
|
32
|
+
onAbort(abortReason);
|
|
33
|
+
clearTimers();
|
|
34
|
+
controller.abort();
|
|
35
|
+
};
|
|
36
|
+
const touch = () => {
|
|
37
|
+
if (finished || controller.signal.aborted) return;
|
|
38
|
+
clearTimeout(idleTimeout);
|
|
39
|
+
idleTimeout = setTimeout(() => abort('idle_timeout'), idleTimeoutMs);
|
|
40
|
+
idleTimeout.unref?.();
|
|
41
|
+
};
|
|
42
|
+
const start = () => {
|
|
43
|
+
if (started || finished || controller.signal.aborted) return;
|
|
44
|
+
started = true;
|
|
45
|
+
touch();
|
|
46
|
+
totalTimeout = setTimeout(() => abort('total_timeout'), totalTimeoutMs);
|
|
47
|
+
totalTimeout.unref?.();
|
|
48
|
+
};
|
|
49
|
+
const finish = () => {
|
|
50
|
+
if (finished) return;
|
|
51
|
+
finished = true;
|
|
52
|
+
clearTimers();
|
|
53
|
+
};
|
|
54
|
+
const recoverInterrupted = error => {
|
|
55
|
+
if (abortReason === 'idle_timeout' || abortReason === 'total_timeout') return abortReason;
|
|
56
|
+
if (abortReason) return '';
|
|
57
|
+
return isTransportInterruption(error) ? 'transport_interrupted' : '';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
abort,
|
|
62
|
+
dispose: finish,
|
|
63
|
+
finish,
|
|
64
|
+
recoverInterrupted,
|
|
65
|
+
start,
|
|
66
|
+
touch,
|
|
67
|
+
get abortReason() { return abortReason; }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveProviderIdleTimeoutMs(launch = {}) {
|
|
72
|
+
const configured = Number(launch.requestTimeoutMs);
|
|
73
|
+
if (!Number.isFinite(configured) || configured <= 0) return DEFAULT_IDLE_TIMEOUT_MS;
|
|
74
|
+
return Math.min(MAX_IDLE_TIMEOUT_MS, Math.max(DEFAULT_IDLE_TIMEOUT_MS, Math.floor(configured)));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveProviderTotalTimeoutMs(launch = {}, idleTimeoutMs = resolveProviderIdleTimeoutMs(launch)) {
|
|
78
|
+
const configured = Number(launch.totalRequestTimeoutMs);
|
|
79
|
+
const requested = Number.isFinite(configured) && configured > 0
|
|
80
|
+
? Math.floor(configured)
|
|
81
|
+
: DEFAULT_TOTAL_TIMEOUT_MS;
|
|
82
|
+
return Math.min(MAX_TOTAL_TIMEOUT_MS, Math.max(requested, idleTimeoutMs * 2));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isTransportInterruption(error) {
|
|
86
|
+
const code = String(error?.cause?.code || error?.code || '').toUpperCase();
|
|
87
|
+
if (['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT'].includes(code)) return true;
|
|
88
|
+
const message = String(error?.message || '').toLowerCase();
|
|
89
|
+
return error?.name === 'AbortError'
|
|
90
|
+
|| message.includes('fetch failed')
|
|
91
|
+
|| message.includes('terminated')
|
|
92
|
+
|| message.includes('socket')
|
|
93
|
+
|| message.includes('connection reset');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
createProviderStreamLifecycle,
|
|
98
|
+
resolveProviderIdleTimeoutMs,
|
|
99
|
+
resolveProviderTotalTimeoutMs
|
|
100
|
+
};
|