codex-overleaf-link 2.5.1 → 2.6.5
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 +149 -398
- package/README.zh-CN.md +153 -404
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +148 -3
- package/extension/src/backgroundSharedSessions.js +55 -2
- package/extension/src/backgroundUpdateCoordinator.js +128 -12
- package/extension/src/content/activeTurnControl.js +164 -7
- package/extension/src/content/composerAttachments.js +46 -10
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +1114 -321
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +147 -42
- package/extension/src/content/generated/content.bundle.js +5761 -1237
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/localSkillsPanel.js +6 -4
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +53 -14
- package/extension/src/content/modelPicker.js +29 -19
- package/extension/src/content/nativeCompatibilityController.js +52 -19
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +7 -7
- package/extension/src/content/panelMaintenance.js +10 -2
- package/extension/src/content/panelRenderer.js +55 -28
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +28 -19
- package/extension/src/content/recentProjects.js +30 -19
- package/extension/src/content/runActivitySummary.js +122 -14
- package/extension/src/content/runCompletionMeta.js +194 -6
- package/extension/src/content/runController.js +2 -1
- package/extension/src/content/runFailureNotice.js +8 -1
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +27 -13
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +73 -24
- package/extension/src/content/runReviewController.js +102 -37
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +124 -46
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sensitivePreflight.js +14 -1
- package/extension/src/content/sessionManager.js +49 -3
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +15 -2
- package/extension/src/content/settingsWorkbench.js +127 -89
- package/extension/src/content/sharedSessionViewSync.js +13 -4
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +301 -72
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +172 -113
- package/extension/src/pageBridge.js +124 -73
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +372 -18
- package/extension/src/shared/auditRecords.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +27 -7
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +57 -3
- package/extension/src/shared/i18n.js +315 -29
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/sessionState.js +63 -4
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/sharedSessionBridge.js +81 -13
- package/extension/src/shared/staleGuard.js +130 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +111 -2
- package/extension/src/shared/storageRunActions.js +62 -8
- package/extension/src/shared/undoOperations.js +126 -28
- package/extension/src/shared/updateRuntimeIdentity.js +16 -2
- package/extension/src/shared/writebackSettlement.js +21 -3
- package/extension/styles/panel.css +1293 -549
- package/native-host/src/bootstrapUpdate.js +52 -16
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +10 -1
- package/native-host/src/chatCompletionsBridge.js +29 -2
- package/native-host/src/codexPromptAssembly.js +86 -118
- package/native-host/src/codexSessionRunner.js +127 -17
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +28 -8
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +144 -21
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +5 -8
|
@@ -12,6 +12,7 @@ const Contract = require('./bootstrapMigrationContract');
|
|
|
12
12
|
const { updateError } = require('./updateTrust');
|
|
13
13
|
|
|
14
14
|
const BOOTSTRAP_DEADLINE_MS = 120000;
|
|
15
|
+
const MAX_AUTOMATIC_ROLLBACK_ATTEMPTS = 3;
|
|
15
16
|
const BOOTSTRAP_STATES = new Set(['bootstrap_pending', 'bootstrap_rolling_back']);
|
|
16
17
|
const backupRoot = context => path.join(context.updatesRoot, 'bootstrap-migrations');
|
|
17
18
|
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
@@ -157,7 +158,7 @@ function restoreBootstrap(context, journal) {
|
|
|
157
158
|
if (journal.bootstrapMigration && journal.bootstrapMigration.phase !== 'pending') {
|
|
158
159
|
throw updateError('update_bootstrap_record_missing', 'Bootstrap recovery material is missing.');
|
|
159
160
|
}
|
|
160
|
-
if (journal.bootstrapMigration) return
|
|
161
|
+
if (journal.bootstrapMigration) return untouchedBootstrap(context, journal);
|
|
161
162
|
}
|
|
162
163
|
return plan ? Files.restoreBootstrapMigration(plan) : null;
|
|
163
164
|
}
|
|
@@ -169,29 +170,64 @@ function bootstrapEvidence(context, journal, expected) {
|
|
|
169
170
|
|| journal.bootstrapMigration.phase === 'restored' && journal.bootstrapMigration.sourceUnchanged === true)) {
|
|
170
171
|
throw updateError('update_bootstrap_record_missing', 'Bootstrap recovery material is missing.');
|
|
171
172
|
}
|
|
172
|
-
return
|
|
173
|
+
return untouchedBootstrap(context, journal);
|
|
173
174
|
}
|
|
174
175
|
return plan ? Files.verifyBootstrapMigration(plan, { expected }) : null;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
// Without a file plan this transaction never wrote Bootstrap, so recovery only
|
|
179
|
+
// leaves it in place. Published source bytes keep the baseline digest; any other
|
|
180
|
+
// installed bytes (an earlier release candidate or local build) are recorded as
|
|
181
|
+
// observed. Later reads compare the same digest, so changes still block.
|
|
182
|
+
function untouchedBootstrap(context, journal) {
|
|
183
|
+
let knownSource = true;
|
|
184
|
+
const entries = Object.entries(Contract.BOOTSTRAP_MIGRATION_BASELINE).sort((a, b) => a[0].localeCompare(b[0]))
|
|
185
|
+
.map(([relative, expected]) => {
|
|
186
|
+
const actual = readBootstrapFileHash(context, relative);
|
|
187
|
+
if (actual !== expected) knownSource = false;
|
|
188
|
+
return [relative, actual];
|
|
189
|
+
});
|
|
190
|
+
return { transactionId: journal.id, migrationId: 'bootstrap-v3.1-from-2.5.0',
|
|
191
|
+
state: 'restored', expected: 'source', digest: hash(JSON.stringify(entries)), unmodified: true, knownSource };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readBootstrapFileHash(context, relative) {
|
|
195
|
+
const filename = path.join(context.extensionRoot, relative);
|
|
196
|
+
if (!fs.existsSync(filename)) return null;
|
|
197
|
+
if (!fs.lstatSync(filename).isFile()) {
|
|
198
|
+
throw updateError('update_bootstrap_source_mismatch', 'Bootstrap source bytes cannot be confirmed.');
|
|
199
|
+
}
|
|
200
|
+
return hash(fs.readFileSync(filename));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Runs before staging and in the update check. A signed migration starts only
|
|
204
|
+
// from the exact source bytes it names; other installations need a full
|
|
205
|
+
// reinstall instead of a transaction that cannot finish. This reads only the
|
|
206
|
+
// descriptor's file list, so later migration contracts use the same check.
|
|
207
|
+
function assertMigrationSource(context, manifest, sourceVersion) {
|
|
208
|
+
const descriptor = manifest?.bootstrapMigration;
|
|
209
|
+
if (!descriptor || !Array.isArray(descriptor.sourceVersions) || !descriptor.sourceVersions.includes(sourceVersion)
|
|
210
|
+
|| !Array.isArray(descriptor.files)) return;
|
|
211
|
+
for (const file of descriptor.files) {
|
|
212
|
+
const relative = String(file?.path || '');
|
|
213
|
+
if (!/^bootstrap\/[A-Za-z0-9._-]+$/.test(relative)) {
|
|
214
|
+
throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap migration file identity is invalid.');
|
|
215
|
+
}
|
|
216
|
+
if (readBootstrapFileHash(context, relative) !== (file.sourceSha256 ?? null)) {
|
|
217
|
+
throw updateError('update_bootstrap_source_mismatch',
|
|
218
|
+
'The installed startup files do not match the published source of this update.');
|
|
183
219
|
}
|
|
184
220
|
}
|
|
185
|
-
const entries = Object.entries(Contract.BOOTSTRAP_MIGRATION_BASELINE).sort((a, b) => a[0].localeCompare(b[0]));
|
|
186
|
-
return { transactionId: journal.id, migrationId: 'bootstrap-v3.1-from-2.5.0',
|
|
187
|
-
state: 'restored', expected: 'source', digest: hash(JSON.stringify(entries)), unmodified: true };
|
|
188
221
|
}
|
|
189
222
|
|
|
190
223
|
function bootstrapExpired(journal, { recoveryRequested = false, context } = {}) {
|
|
191
224
|
const migration = journal?.bootstrapMigration;
|
|
192
225
|
if (!migration || !['bootstrap_pending', 'bootstrap_rolling_back', 'awaiting_health'].includes(journal.state)) return false;
|
|
193
|
-
|
|
194
|
-
|
|
226
|
+
// Automatic retries are bounded; an explicit update.rollback still runs.
|
|
227
|
+
if (journal.state === 'bootstrap_rolling_back') {
|
|
228
|
+
return Number(journal.rollbackFailure?.attempts || 0) < MAX_AUTOMATIC_ROLLBACK_ATTEMPTS;
|
|
229
|
+
}
|
|
230
|
+
return (migration.deadline && Date.now() >= migration.deadline)
|
|
195
231
|
|| (recoveryRequested && migration.phase !== 'pending' && !watchdogAlive(migration, context, journal));
|
|
196
232
|
}
|
|
197
233
|
|
|
@@ -228,6 +264,6 @@ function startWatchdog(context, journal, token) {
|
|
|
228
264
|
|
|
229
265
|
function readJson(filename) { try { return JSON.parse(fs.readFileSync(filename, 'utf8')); } catch (_) { return null; } }
|
|
230
266
|
|
|
231
|
-
module.exports = { BOOTSTRAP_DEADLINE_MS, BOOTSTRAP_STATES,
|
|
232
|
-
advanceBootstrapMigration, assertBootstrapConfirmation,
|
|
233
|
-
bootstrapExpired, watchdogAlive };
|
|
267
|
+
module.exports = { BOOTSTRAP_DEADLINE_MS, BOOTSTRAP_STATES, MAX_AUTOMATIC_ROLLBACK_ATTEMPTS, migrationHint,
|
|
268
|
+
bindBootstrapMigration, advanceBootstrapMigration, assertBootstrapConfirmation, assertMigrationSource,
|
|
269
|
+
restoreBootstrap, bootstrapEvidence, bootstrapExpired, watchdogAlive };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
|
+
const { providerError } = require('./providerProfile');
|
|
4
5
|
|
|
5
6
|
const {
|
|
6
7
|
applyReasoningControl,
|
|
@@ -58,7 +59,15 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
|
|
|
58
59
|
}
|
|
59
60
|
if (!Array.isArray(input)) return;
|
|
60
61
|
let pendingToolCalls = [];
|
|
62
|
+
let pendingToolOutputContent = [];
|
|
61
63
|
let pendingReasoning = '';
|
|
64
|
+
const flushToolOutputContent = () => {
|
|
65
|
+
if (!pendingToolOutputContent.length) return;
|
|
66
|
+
// Chat tool messages accept text only. Attach images after the complete
|
|
67
|
+
// tool-result group so a user message cannot split parallel tool results.
|
|
68
|
+
messages.push({ role: 'user', content: pendingToolOutputContent });
|
|
69
|
+
pendingToolOutputContent = [];
|
|
70
|
+
};
|
|
62
71
|
const flushToolCalls = () => {
|
|
63
72
|
if (!pendingToolCalls.length) return;
|
|
64
73
|
const assistant = {
|
|
@@ -73,6 +82,9 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
|
|
|
73
82
|
};
|
|
74
83
|
for (const item of input) {
|
|
75
84
|
if (!item || typeof item !== 'object') continue;
|
|
85
|
+
const isToolOutput = item.type === 'function_call_output' || item.type === 'custom_tool_call_output'
|
|
86
|
+
|| item.type === 'computer_call_output' || item.type === 'tool_search_output';
|
|
87
|
+
if (!isToolOutput && item.type !== 'reasoning') flushToolOutputContent();
|
|
76
88
|
if (item.type === 'reasoning') {
|
|
77
89
|
flushToolCalls();
|
|
78
90
|
pendingReasoning = extractReasoningText(item);
|
|
@@ -83,12 +95,22 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
|
|
|
83
95
|
continue;
|
|
84
96
|
}
|
|
85
97
|
flushToolCalls();
|
|
86
|
-
if (
|
|
98
|
+
if (isToolOutput) {
|
|
99
|
+
const callId = normalizeText(item.call_id) || normalizeText(item.id);
|
|
100
|
+
const imageContent = translateImageToolOutput(item.output);
|
|
87
101
|
messages.push({
|
|
88
102
|
role: 'tool',
|
|
89
|
-
tool_call_id:
|
|
90
|
-
content:
|
|
103
|
+
tool_call_id: callId,
|
|
104
|
+
content: imageContent
|
|
105
|
+
? 'The tool output, including images, is attached after the tool results.'
|
|
106
|
+
: normalizeToolOutput(item.output)
|
|
91
107
|
});
|
|
108
|
+
if (imageContent) {
|
|
109
|
+
pendingToolOutputContent.push(
|
|
110
|
+
{ type: 'text', text: `Output from tool call ${callId}:` },
|
|
111
|
+
...imageContent
|
|
112
|
+
);
|
|
113
|
+
}
|
|
92
114
|
continue;
|
|
93
115
|
}
|
|
94
116
|
if (item.type === 'message' || item.role) {
|
|
@@ -105,6 +127,38 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
|
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
129
|
flushToolCalls();
|
|
130
|
+
flushToolOutputContent();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function translateImageToolOutput(output) {
|
|
134
|
+
const parts = Array.isArray(output) ? output : [output];
|
|
135
|
+
if (!parts.some(isImagePart)) return null;
|
|
136
|
+
return parts.map(part => {
|
|
137
|
+
if (isImagePart(part)) {
|
|
138
|
+
const image = translateImagePart(part);
|
|
139
|
+
if (!image) {
|
|
140
|
+
throw providerError('provider_request_invalid', 'A tool image requires an image URL or data URL for Chat Completions.');
|
|
141
|
+
}
|
|
142
|
+
return image;
|
|
143
|
+
}
|
|
144
|
+
const text = ['input_text', 'output_text', 'text'].includes(part?.type)
|
|
145
|
+
? normalizeText(part.text)
|
|
146
|
+
: normalizeToolOutput(part);
|
|
147
|
+
return { type: 'text', text };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isImagePart(part) {
|
|
152
|
+
return part?.type === 'input_image' || part?.type === 'image_url' || part?.type === 'computer_screenshot';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function translateImagePart(part) {
|
|
156
|
+
const url = normalizeText(part.image_url?.url || part.image_url || part.url);
|
|
157
|
+
if (!url) return null;
|
|
158
|
+
const detail = normalizeText(part.image_url?.detail || part.detail);
|
|
159
|
+
const image = { type: 'image_url', image_url: { url } };
|
|
160
|
+
if (['auto', 'low', 'high'].includes(detail)) image.image_url.detail = detail;
|
|
161
|
+
return image;
|
|
108
162
|
}
|
|
109
163
|
|
|
110
164
|
function extractReasoningText(item) {
|
|
@@ -142,10 +196,10 @@ function translateMessageContent(content) {
|
|
|
142
196
|
if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') {
|
|
143
197
|
parts.push({ type: 'text', text: normalizeText(part.text) });
|
|
144
198
|
} else if (part.type === 'input_image' || part.type === 'image_url') {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
199
|
+
const image = translateImagePart(part);
|
|
200
|
+
if (image) {
|
|
147
201
|
hasStructuredPart = true;
|
|
148
|
-
parts.push(
|
|
202
|
+
parts.push(image);
|
|
149
203
|
}
|
|
150
204
|
}
|
|
151
205
|
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const { requiresReasoningContentReplay } = require('./providerReasoning');
|
|
5
|
+
const { isContextLimitError } = require('./providerContextError');
|
|
5
6
|
const MAX_STREAM_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
6
7
|
|
|
7
8
|
function convertChatResponse(chat = {}, context = {}) {
|
|
9
|
+
if (chat.error) throwUpstreamError(chat.error);
|
|
8
10
|
const choice = Array.isArray(chat.choices) ? chat.choices[0] || {} : {};
|
|
9
11
|
const message = choice.message || {};
|
|
10
12
|
assertNoLeakedToolControlTokens(message);
|
|
@@ -131,7 +133,7 @@ function writeConvertedResponseAsSse(res, converted, context = {}, onComplete =
|
|
|
131
133
|
|
|
132
134
|
function processChatChunk(state, res, chunk) {
|
|
133
135
|
if (!chunk || typeof chunk !== 'object') return;
|
|
134
|
-
if (chunk.error)
|
|
136
|
+
if (chunk.error) throwUpstreamError(chunk.error);
|
|
135
137
|
if (chunk.id && !state.upstreamId) {
|
|
136
138
|
state.upstreamId = chunk.id;
|
|
137
139
|
}
|
|
@@ -491,6 +493,13 @@ function mapChatFinishReason(value) {
|
|
|
491
493
|
return { status: 'completed', incompleteReason: null };
|
|
492
494
|
}
|
|
493
495
|
|
|
496
|
+
function throwUpstreamError(detail) {
|
|
497
|
+
throw responseError(
|
|
498
|
+
normalizeText(detail?.message) || normalizeText(detail) || 'Upstream provider returned an error.',
|
|
499
|
+
isContextLimitError(detail) ? 'context_length_exceeded' : 'provider_upstream_error'
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
494
503
|
function responseError(message, code = 'provider_response_invalid') {
|
|
495
504
|
const error = new Error(message);
|
|
496
505
|
error.code = code;
|
|
@@ -13,6 +13,7 @@ const { classifyResponsesRoute } = require('./providerBridgeRoutes');
|
|
|
13
13
|
const { providerError } = require('./providerProfile');
|
|
14
14
|
const { fetchProvider, readProviderText, readProviderJson } = require('./providerHttp');
|
|
15
15
|
const { sanitizeProviderMessage } = require('./providerRedaction');
|
|
16
|
+
const { isContextLimitError } = require('./providerContextError');
|
|
16
17
|
const { logDebug } = require('./debugLog');
|
|
17
18
|
const {
|
|
18
19
|
createProviderStreamLifecycle,
|
|
@@ -174,7 +175,7 @@ async function handleRequest({
|
|
|
174
175
|
}, launch);
|
|
175
176
|
lifecycle.touch();
|
|
176
177
|
if (!upstream.ok) {
|
|
177
|
-
await forwardUpstreamError(upstream, res, launch.apiKey, lifecycle.touch);
|
|
178
|
+
await forwardUpstreamError(upstream, res, launch.apiKey, lifecycle.touch, requestBody.stream !== false);
|
|
178
179
|
return;
|
|
179
180
|
}
|
|
180
181
|
const context = {
|
|
@@ -255,6 +256,10 @@ async function handleRequest({
|
|
|
255
256
|
if (controller.signal.aborted) {
|
|
256
257
|
throw providerError('provider_connection_timeout', 'The provider request was cancelled or timed out.');
|
|
257
258
|
}
|
|
259
|
+
if (error?.code === 'context_length_exceeded' && !res.headersSent) {
|
|
260
|
+
sendContextLimitError(res, sanitizeProviderMessage(error.message, [launch.apiKey]), requestBody.stream !== false);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
258
263
|
throw error;
|
|
259
264
|
} finally {
|
|
260
265
|
lifecycle.dispose();
|
|
@@ -382,6 +387,7 @@ function normalizeBridgeErrorCode(value) {
|
|
|
382
387
|
'provider_reasoning_continuation_exhausted',
|
|
383
388
|
'provider_stream_tool_parse_failed',
|
|
384
389
|
'provider_protocol_incompatible',
|
|
390
|
+
'context_length_exceeded',
|
|
385
391
|
'provider_upstream_error'
|
|
386
392
|
].includes(value) ? value : 'provider_bridge_failed';
|
|
387
393
|
}
|
|
@@ -392,16 +398,37 @@ function bridgeErrorMessage(code) {
|
|
|
392
398
|
: 'The local provider bridge failed.';
|
|
393
399
|
}
|
|
394
400
|
|
|
395
|
-
async function forwardUpstreamError(upstream, res, apiKey, onActivity) {
|
|
401
|
+
async function forwardUpstreamError(upstream, res, apiKey, onActivity, clientStream) {
|
|
396
402
|
const text = (await readProviderText(upstream, onActivity)).slice(0, MAX_ERROR_BYTES);
|
|
397
403
|
let message = `Provider returned HTTP ${upstream.status}.`;
|
|
404
|
+
let error;
|
|
398
405
|
try {
|
|
399
406
|
const parsed = JSON.parse(text);
|
|
407
|
+
error = parsed?.error || parsed;
|
|
400
408
|
message = parsed?.error?.message || parsed?.message || message;
|
|
401
409
|
} catch (_error) {}
|
|
410
|
+
if ([400, 413, 422].includes(upstream.status) && isContextLimitError(error || text)) {
|
|
411
|
+
sendContextLimitError(res, sanitizeProviderMessage(error ? message : text, [apiKey]), clientStream);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
402
414
|
sendJsonError(res, upstream.status, 'provider_upstream_error', sanitizeProviderMessage(message, [apiKey]));
|
|
403
415
|
}
|
|
404
416
|
|
|
417
|
+
function sendContextLimitError(res, message, stream) {
|
|
418
|
+
const error = { type: 'invalid_request_error', code: 'context_length_exceeded', message };
|
|
419
|
+
if (!stream) {
|
|
420
|
+
sendJson(res, 400, { error });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// Non-2xx JSON is a generic HTTP failure to the installed Codex client.
|
|
424
|
+
// Its typed context recovery consumes a Responses failure event instead.
|
|
425
|
+
res.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-store' });
|
|
426
|
+
res.end(`event: response.failed\ndata: ${JSON.stringify({
|
|
427
|
+
type: 'response.failed', sequence_number: 0,
|
|
428
|
+
response: { id: `resp_${crypto.randomUUID()}`, object: 'response', status: 'failed', error }
|
|
429
|
+
})}\n\n`);
|
|
430
|
+
}
|
|
431
|
+
|
|
405
432
|
function rememberHistory(history, responseId, messages, metadata = {}) {
|
|
406
433
|
history.delete(responseId);
|
|
407
434
|
history.set(responseId, { messages, ...metadata });
|
|
@@ -2,84 +2,87 @@
|
|
|
2
2
|
|
|
3
3
|
const { truncateText } = require('./debugLog');
|
|
4
4
|
const { formatOverleafWorkspaceRules } = require('./overleafWorkspaceRules');
|
|
5
|
+
const { buildReadProgressRules } = require('./readProgressGuard');
|
|
5
6
|
|
|
6
7
|
const PROJECT_CUSTOM_INSTRUCTIONS_MAX_CHARS = 12000;
|
|
7
8
|
const NATIVE_MESSAGE_PROMPT_MAX_CHARS = 1024 * 1024;
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
/** Stable parent-thread guidance; the active mode and task belong to the turn. */
|
|
11
|
+
function buildCodexDeveloperInstructions(options = {}) {
|
|
12
|
+
const params = options.params && typeof options.params === 'object' ? options.params : options;
|
|
13
|
+
const locale = (options.locale ?? params.locale) === 'zh' ? 'Chinese (zh)' : 'English (en)';
|
|
14
|
+
const skillInvocation = options.skillInvocation || params.skillInvocation;
|
|
15
|
+
return [
|
|
16
|
+
'You are Codex working inside the Codex Overleaf Link panel, helping an academic author with their LaTeX project in Overleaf. The user reads your answer in a narrow (~380px) side panel.',
|
|
17
|
+
[
|
|
18
|
+
'Reply language and style:',
|
|
19
|
+
`- Reply in the language of the user's latest request. If unclear, use the UI language: ${locale}. Keep LaTeX source, file paths, and identifiers unchanged.`,
|
|
20
|
+
'- Lead with the answer or outcome. Use short paragraphs and lists; avoid wide tables and long code dumps. Do not restate the request.',
|
|
21
|
+
'- Complete the requested analysis or edit within this turn. Do not end with only a plan or a promise to act later.',
|
|
22
|
+
'- For edits, summarize what changed per file with citations. The panel already shows written files, diffs, and Undo; do not paste whole diffs.'
|
|
23
|
+
].join('\n'),
|
|
24
|
+
...(isSkillInstallerInvocation(skillInvocation) ? [] : [formatOverleafWorkspaceRules()]),
|
|
25
|
+
['Project location citation rules:', formatProjectLocationCitationRules()].join('\n'),
|
|
26
|
+
['Answer formatting:', formatMathOutputRules()].join('\n'),
|
|
27
|
+
[
|
|
28
|
+
'Academic integrity:',
|
|
29
|
+
'- Never invent citations, BibTeX entries, data, results, or quotations.',
|
|
30
|
+
'- When a claim needs a source you cannot verify in the project, say so and suggest what kind of source is needed instead of adding one.'
|
|
31
|
+
].join('\n'),
|
|
32
|
+
[
|
|
33
|
+
'Editing discipline for writable turns:',
|
|
34
|
+
'- Change only what the request needs. Keep unrelated text, whitespace, and formatting untouched; small diffs keep Overleaf tracked changes and Undo reviewable.',
|
|
35
|
+
'- Keep \\label, \\ref, and \\cite keys and file names stable unless asked to change them.',
|
|
36
|
+
'- Keep the document compilable, with balanced braces and environments.'
|
|
37
|
+
].join('\n'),
|
|
38
|
+
[
|
|
39
|
+
'Modes:',
|
|
40
|
+
'- Each turn states its active mode. These modes govern project work; explicit skill-install turns follow their separate installation scope.',
|
|
41
|
+
'- Ask is read-only: inspect and explain. When suggesting edits, show short LaTeX snippets with citations and mention that Auto mode can apply them.',
|
|
42
|
+
'- Auto may edit the local workspace. The browser bridge handles authorization, deletion approval, and syncing to Overleaf.'
|
|
43
|
+
].join('\n'),
|
|
44
|
+
buildReadProgressRules()
|
|
45
|
+
].join('\n\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build dynamic per-turn context and the current user request. */
|
|
22
49
|
function buildCodexTurnPrompt(options = {}) {
|
|
23
50
|
const context = normalizePromptOptions(options);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'',
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'',
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'',
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'Selected Codex skill:',
|
|
60
|
-
formatSkillInvocationContext(context),
|
|
61
|
-
'',
|
|
62
|
-
'Attachments for this turn:',
|
|
63
|
-
formatTurnAttachmentsContext(context.attachments),
|
|
64
|
-
'',
|
|
65
|
-
'Mode for this turn:',
|
|
66
|
-
`- ${context.mode}`,
|
|
67
|
-
'- ask: inspect and explain only; do not edit files.',
|
|
68
|
-
'- auto: edit the local workspace directly when the request calls for changes. The browser bridge handles authorization, deletion approval, and syncing back to Overleaf.',
|
|
69
|
-
'',
|
|
70
|
-
'Write expectation for this turn:',
|
|
71
|
-
formatWriteExpectation({
|
|
72
|
-
mode: context.mode,
|
|
73
|
-
task: context.userTask,
|
|
74
|
-
skillInvocation: context.skillInvocation
|
|
75
|
-
})
|
|
76
|
-
].join('\n');
|
|
77
|
-
|
|
78
|
-
const userPrompt = [
|
|
79
|
-
'Current user request:',
|
|
80
|
-
context.userTask || '(empty request)'
|
|
81
|
-
].join('\n');
|
|
82
|
-
|
|
51
|
+
const sections = [
|
|
52
|
+
`Mode for this turn: ${context.mode}`,
|
|
53
|
+
['Write expectation for this turn:', formatWriteExpectation(context)].join('\n')
|
|
54
|
+
];
|
|
55
|
+
if (!isSkillInstallerInvocation(context.skillInvocation)) {
|
|
56
|
+
sections.push([
|
|
57
|
+
'Current Overleaf workspace:',
|
|
58
|
+
...(context.projectKey || context.projectId ? [`- Project: ${context.projectKey || context.projectId}`] : []),
|
|
59
|
+
...formatWorkspaceSnapshotContext(context.unreadFiles)
|
|
60
|
+
].join('\n'));
|
|
61
|
+
}
|
|
62
|
+
if (!context.threadId && Array.isArray(context.session.history) && context.session.history.length) {
|
|
63
|
+
sections.push(['Recent turns in this UI session:', formatSessionHistory(context.session.history)].join('\n'));
|
|
64
|
+
}
|
|
65
|
+
if (context.focusFiles.length) {
|
|
66
|
+
sections.push(['Focus files:', formatFocusFiles(context.focusFiles)].join('\n'));
|
|
67
|
+
}
|
|
68
|
+
if (String(context.compileLog || '').trim()) {
|
|
69
|
+
sections.push(['Compilation context (@compile-log):', formatCompileLogContext(context)].join('\n'));
|
|
70
|
+
}
|
|
71
|
+
if (String(context.customInstructions || '').trim()) {
|
|
72
|
+
sections.push(['Project custom instructions:', formatCustomInstructionsContext(context.customInstructions)].join('\n'));
|
|
73
|
+
}
|
|
74
|
+
if (context.selectedSkillIds.length && (context.projectLocalSkills.skills?.length || context.projectLocalSkills.missing?.length)) {
|
|
75
|
+
sections.push(['Project local skills:', formatProjectLocalSkillsContext(context)].join('\n'));
|
|
76
|
+
}
|
|
77
|
+
if (normalizeSkillInvocation(context.skillInvocation) || normalizeSkillInvocation(context.codexSkillInvocationContext?.invocation)
|
|
78
|
+
|| context.codexSkillInvocationContext?.ignored?.length || context.codexSkillInvocationContext?.missing?.length) {
|
|
79
|
+
sections.push(['Selected Codex skill:', formatSkillInvocationContext(context)].join('\n'));
|
|
80
|
+
}
|
|
81
|
+
if (context.attachments.length) {
|
|
82
|
+
sections.push(['Attachments for this turn:', formatTurnAttachmentsContext(context.attachments)].join('\n'));
|
|
83
|
+
}
|
|
84
|
+
const systemPrompt = sections.join('\n\n');
|
|
85
|
+
const userPrompt = ['Current user request:', context.userTask || '(empty request)'].join('\n');
|
|
83
86
|
return { systemPrompt, userPrompt };
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -87,10 +90,12 @@ function buildCodexTurnPrompt(options = {}) {
|
|
|
87
90
|
* Validate prompt size against native messaging limits.
|
|
88
91
|
* @param {string} systemPrompt
|
|
89
92
|
* @param {string} userPrompt
|
|
93
|
+
* @param {string} developerInstructions
|
|
90
94
|
* @returns {{ ok: boolean, totalChars: number, exceedsLimit: boolean }}
|
|
91
95
|
*/
|
|
92
|
-
function validatePromptSize(systemPrompt, userPrompt) {
|
|
93
|
-
const totalChars = String(systemPrompt || '').length + String(userPrompt || '').length
|
|
96
|
+
function validatePromptSize(systemPrompt, userPrompt, developerInstructions = '') {
|
|
97
|
+
const totalChars = String(systemPrompt || '').length + String(userPrompt || '').length
|
|
98
|
+
+ String(developerInstructions || '').length;
|
|
94
99
|
const exceedsLimit = totalChars > NATIVE_MESSAGE_PROMPT_MAX_CHARS;
|
|
95
100
|
return {
|
|
96
101
|
ok: !exceedsLimit,
|
|
@@ -127,6 +132,7 @@ function normalizePromptOptions(options = {}) {
|
|
|
127
132
|
return {
|
|
128
133
|
params,
|
|
129
134
|
session,
|
|
135
|
+
threadId: String(options.threadId ?? params.threadId ?? '').trim(),
|
|
130
136
|
projectKey: mirror.projectKey || options.projectKey || params.projectId || params.project?.id || params.project?.projectId,
|
|
131
137
|
projectId: options.projectId || params.projectId || params.project?.id || params.project?.projectId,
|
|
132
138
|
workspacePath: mirror.workspacePath || options.workspacePath || '',
|
|
@@ -156,8 +162,7 @@ function normalizePromptOptions(options = {}) {
|
|
|
156
162
|
|
|
157
163
|
function formatWorkspaceSnapshotContext(unreadFiles) {
|
|
158
164
|
if (!unreadFiles.length) return [
|
|
159
|
-
'- The local workspace was synced from Overleaf
|
|
160
|
-
'- If the recent session history conflicts with the files in the workspace, trust the files.'
|
|
165
|
+
'- The local workspace was synced from Overleaf before this turn; trust the files over conflicting session history.'
|
|
161
166
|
];
|
|
162
167
|
return [
|
|
163
168
|
'- The readable project files were synced from Overleaf before this turn.',
|
|
@@ -196,7 +201,7 @@ function formatTurnAttachmentsContext(attachments = []) {
|
|
|
196
201
|
].join('\n');
|
|
197
202
|
}
|
|
198
203
|
|
|
199
|
-
function formatWriteExpectation({ mode = 'auto',
|
|
204
|
+
function formatWriteExpectation({ mode = 'auto', skillInvocation = null } = {}) {
|
|
200
205
|
if (isSkillInstallerInvocation(skillInvocation)) {
|
|
201
206
|
return [
|
|
202
207
|
'- This is a skill-install turn.',
|
|
@@ -208,29 +213,7 @@ function formatWriteExpectation({ mode = 'auto', task = '', skillInvocation = nu
|
|
|
208
213
|
if (mode === 'ask') {
|
|
209
214
|
return '- This is read-only. Inspect and explain; do not edit files.';
|
|
210
215
|
}
|
|
211
|
-
if
|
|
212
|
-
const lines = [
|
|
213
|
-
'- The request asks for file changes. You must edit the local workspace when you find concrete fixes.',
|
|
214
|
-
'- Do not stop at a suggestion list or say you will not modify files unless no safe concrete edit exists.',
|
|
215
|
-
'- If you intentionally leave files unchanged, explain the specific blocker in the final answer.'
|
|
216
|
-
];
|
|
217
|
-
if (requestTargetsAbstract(task)) {
|
|
218
|
-
lines.push('- The request targets the abstract/summary. Locate the LaTeX abstract environment or summary paragraph and edit that local project file directly.');
|
|
219
|
-
}
|
|
220
|
-
return lines.join('\n');
|
|
221
|
-
}
|
|
222
|
-
return [
|
|
223
|
-
'- This mode can write. If the request asks for corrections, revisions, fixes, polishing, updates, or implementation, edit the local workspace directly.',
|
|
224
|
-
'- If the request is purely an inspection question, report findings without inventing unnecessary edits.'
|
|
225
|
-
].join('\n');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function requestImpliesFileChanges(task = '') {
|
|
229
|
-
return /修正|修复|修改|改[一-龥]*|完善|补全|补充|润色|重写|改写|整理|调整|应用|写入|fix|correct|repair|revise|edit|update|rewrite|polish|improve|apply/i.test(String(task || ''));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function requestTargetsAbstract(task = '') {
|
|
233
|
-
return /摘要|abstract|summary/i.test(String(task || ''));
|
|
216
|
+
return '- Decide from the request whether it asks for changes. If it asks you to fix, revise, polish, rewrite, apply or otherwise change the project, edit the workspace files directly instead of only suggesting; if it asks a question, answer it without editing. If you intentionally leave files unchanged for a change request, explain the blocker.';
|
|
234
217
|
}
|
|
235
218
|
|
|
236
219
|
function normalizeFocusFiles(value) {
|
|
@@ -271,8 +254,8 @@ function normalizeProjectPath(value) {
|
|
|
271
254
|
|
|
272
255
|
function formatProjectLocationCitationRules() {
|
|
273
256
|
return [
|
|
274
|
-
'- In any user-visible output, cite project locations using only Overleaf project-relative paths
|
|
275
|
-
'- Use path:LINE[:COLUMN] (for example, path/to/file.tex:12:3).',
|
|
257
|
+
'- In any user-visible output, cite project locations using only Overleaf project-relative paths of files in this workspace.',
|
|
258
|
+
'- Use path:LINE[:COLUMN] for a position and path:START-END for a span (for example, path/to/file.tex:12:3 or main.tex:12-20).',
|
|
276
259
|
'- Do not wrap project location citations in backticks or inline code; write main.tex:28 as normal text so the browser can turn it into a jump button.',
|
|
277
260
|
'- Do not cite local absolute paths, temporary workspace paths, file:// URLs, or markdown links to local files.',
|
|
278
261
|
'- If a line number is uncertain, cite only the project-relative file path.'
|
|
@@ -289,14 +272,6 @@ function formatMathOutputRules() {
|
|
|
289
272
|
].join('\n');
|
|
290
273
|
}
|
|
291
274
|
|
|
292
|
-
function formatFocusedProjectFileInventory(files) {
|
|
293
|
-
const focusFiles = normalizeFocusFiles(files);
|
|
294
|
-
if (!focusFiles.length) {
|
|
295
|
-
return '- none selected.';
|
|
296
|
-
}
|
|
297
|
-
return focusFiles.map(filePath => `- ${filePath}`).join('\n');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
275
|
function formatFocusFiles(files) {
|
|
301
276
|
if (!files.length) {
|
|
302
277
|
return '- none; use the whole project when needed.';
|
|
@@ -396,13 +371,6 @@ function formatProjectLocalSkillsContext(context = {}) {
|
|
|
396
371
|
return sections.length ? sections.join('\n\n') : '- none loaded.';
|
|
397
372
|
}
|
|
398
373
|
|
|
399
|
-
function formatCodexSkillLoadingContext(context = {}) {
|
|
400
|
-
return [
|
|
401
|
-
`- Codex local skills: ${context.loadCodexLocalSkills === false ? 'disabled' : 'enabled'}`,
|
|
402
|
-
`- Codex Overleaf skills: ${context.loadCodexOverleafSkills === false ? 'disabled' : 'enabled'}`
|
|
403
|
-
].join('\n');
|
|
404
|
-
}
|
|
405
|
-
|
|
406
374
|
function formatSkillInvocationContext(context = {}) {
|
|
407
375
|
const invocation = normalizeSkillInvocation(context.skillInvocation);
|
|
408
376
|
const codexSkillInvocationContext = context.codexSkillInvocationContext || {};
|
|
@@ -514,4 +482,4 @@ function formatSessionHistory(history) {
|
|
|
514
482
|
}).join('\n');
|
|
515
483
|
}
|
|
516
484
|
|
|
517
|
-
module.exports = { buildCodexTurnPrompt, validatePromptSize };
|
|
485
|
+
module.exports = { buildCodexDeveloperInstructions, buildCodexTurnPrompt, validatePromptSize };
|