codex-overleaf-link 2.5.0 → 2.6.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 +155 -392
- package/README.zh-CN.md +154 -393
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- 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/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
|
|
|
4
4
|
const http = require('node:http');
|
|
5
5
|
const { classifyResponsesRoute } = require('./providerBridgeRoutes');
|
|
6
6
|
const { providerError } = require('./providerProfile');
|
|
7
|
+
const { fetchProvider, readProviderText } = require('./providerHttp');
|
|
7
8
|
const { sanitizeProviderMessage } = require('./providerRedaction');
|
|
8
9
|
const { logDebug } = require('./debugLog');
|
|
9
10
|
const {
|
|
@@ -19,6 +20,7 @@ const MAX_HISTORY_ITEMS = 4096;
|
|
|
19
20
|
const MAX_HISTORY_BYTES = 32 * 1024 * 1024;
|
|
20
21
|
const MAX_DUPLICATE_CONTINUATIONS = 2;
|
|
21
22
|
const MAX_STREAM_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
23
|
+
const MAX_COMPACT_RESPONSE_BYTES = 32 * 1024 * 1024;
|
|
22
24
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
23
25
|
const CONTINUATION_MARKER = '[codex-overleaf-provider-reasoning-continuation]';
|
|
24
26
|
|
|
@@ -102,7 +104,7 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
|
|
|
102
104
|
const prepared = route === 'responses'
|
|
103
105
|
? prepareResponsesRequest({ requestBody, launch, history, previous, originalPreviousResponseId })
|
|
104
106
|
: {
|
|
105
|
-
body: { ...requestBody },
|
|
107
|
+
body: { ...(launch.bodyOverrides || {}), ...requestBody, previous_response_id: requestBody.previous_response_id },
|
|
106
108
|
currentInput: normalizeInputItems(requestBody.input),
|
|
107
109
|
replayed: false
|
|
108
110
|
};
|
|
@@ -140,19 +142,19 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
|
|
|
140
142
|
res.on('close', onClientClose);
|
|
141
143
|
lifecycle.start();
|
|
142
144
|
try {
|
|
143
|
-
const upstream = await
|
|
145
|
+
const upstream = await fetchProvider(buildResponsesUrl(launch.baseUrl, launch, route), {
|
|
144
146
|
method: 'POST',
|
|
145
147
|
headers: buildUpstreamHeaders(launch),
|
|
146
148
|
body: JSON.stringify(prepared.body),
|
|
147
149
|
signal: controller.signal
|
|
148
|
-
});
|
|
150
|
+
}, launch);
|
|
149
151
|
lifecycle.touch();
|
|
150
152
|
if (!upstream.ok) {
|
|
151
|
-
await forwardUpstreamError(upstream, res, launch.apiKey);
|
|
153
|
+
await forwardUpstreamError(upstream, res, launch.apiKey, lifecycle.touch);
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
156
|
if (route === 'compact') {
|
|
155
|
-
await
|
|
157
|
+
await forwardCompactResponse(upstream, res, lifecycle.touch, launch.apiKey);
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -165,11 +167,18 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
|
|
|
165
167
|
'The provider returned a non-streaming payload for a streaming Responses request.'
|
|
166
168
|
);
|
|
167
169
|
}
|
|
168
|
-
response = await forwardResponsesSse(upstream, res, lifecycle.touch);
|
|
170
|
+
response = await forwardResponsesSse(upstream, res, lifecycle.touch, launch.apiKey);
|
|
169
171
|
} else {
|
|
170
|
-
|
|
172
|
+
const text = await readProviderText(upstream, lifecycle.touch);
|
|
173
|
+
try {
|
|
174
|
+
response = JSON.parse(text);
|
|
175
|
+
} catch (_error) {
|
|
176
|
+
throw providerError('provider_response_invalid', 'The provider returned an invalid Responses JSON response.');
|
|
177
|
+
}
|
|
171
178
|
lifecycle.touch();
|
|
172
|
-
|
|
179
|
+
sanitizeProtocolErrors(response, launch.apiKey, hasDuplicateJsonKeys(text));
|
|
180
|
+
if (requestBody.stream !== false) writeBufferedResponseAsSse(res, response);
|
|
181
|
+
else sendJson(res, 200, response);
|
|
173
182
|
}
|
|
174
183
|
if (response?.id) {
|
|
175
184
|
rememberResponse(history, {
|
|
@@ -228,7 +237,10 @@ async function handleRequest({ req, res, launch, clientToken, activeRequests, hi
|
|
|
228
237
|
function prepareResponsesRequest({ requestBody, launch, history, previous, originalPreviousResponseId }) {
|
|
229
238
|
const currentInput = normalizeInputItems(requestBody.input);
|
|
230
239
|
const body = {
|
|
240
|
+
...(launch.bodyOverrides || {}),
|
|
231
241
|
...requestBody,
|
|
242
|
+
previous_response_id: requestBody.previous_response_id,
|
|
243
|
+
stream: requestBody.stream !== false && launch.upstreamResponseMode !== 'buffered',
|
|
232
244
|
max_output_tokens: resolveMaxOutputTokens(launch.maxOutputTokens, requestBody.max_output_tokens)
|
|
233
245
|
};
|
|
234
246
|
if (!previous?.continuationRequired) {
|
|
@@ -405,7 +417,75 @@ function resolveMaxOutputTokens(configured, requested) {
|
|
|
405
417
|
return Math.max(1, Math.floor(value));
|
|
406
418
|
}
|
|
407
419
|
|
|
408
|
-
|
|
420
|
+
function writeBufferedResponseAsSse(res, response) {
|
|
421
|
+
if (!response || typeof response !== 'object' || Array.isArray(response)) {
|
|
422
|
+
throw providerError('provider_response_invalid', 'The provider returned an invalid Responses JSON response.');
|
|
423
|
+
}
|
|
424
|
+
if (!response.error && !['completed', 'incomplete', 'failed'].includes(response.status)) {
|
|
425
|
+
throw providerError('provider_response_invalid', 'The buffered Responses request did not return a terminal response.');
|
|
426
|
+
}
|
|
427
|
+
res.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-store' });
|
|
428
|
+
let sequence = 0;
|
|
429
|
+
const emit = (type, detail) => {
|
|
430
|
+
const frame = `event: ${type}\ndata: ${JSON.stringify({ type, sequence_number: sequence++, ...detail })}\n\n`;
|
|
431
|
+
if (Buffer.byteLength(frame, 'utf8') > MAX_STREAM_BUFFER_BYTES) {
|
|
432
|
+
throw providerError('provider_stream_frame_too_large', 'Provider stream frame exceeded the local bridge limit.');
|
|
433
|
+
}
|
|
434
|
+
res.write(frame);
|
|
435
|
+
};
|
|
436
|
+
if (response.error && !Array.isArray(response.output)) {
|
|
437
|
+
emit('error', { ...response, type: 'error' });
|
|
438
|
+
res.end();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const pending = { ...response, status: 'in_progress', output: [], usage: null, error: null, incomplete_details: null };
|
|
442
|
+
emit('response.created', { response: pending });
|
|
443
|
+
emit('response.in_progress', { response: pending });
|
|
444
|
+
for (const [output_index, item] of (response.output || []).entries()) {
|
|
445
|
+
const identity = { item_id: item.id, output_index };
|
|
446
|
+
const added = { ...item, status: 'in_progress' };
|
|
447
|
+
if (item.type === 'message') added.content = [];
|
|
448
|
+
if (item.type === 'reasoning') added.summary = [];
|
|
449
|
+
if (item.type === 'function_call') added.arguments = '';
|
|
450
|
+
if (item.type === 'custom_tool_call') added.input = '';
|
|
451
|
+
emit('response.output_item.added', { output_index, item: added });
|
|
452
|
+
for (const [content_index, part] of (item.content || []).entries()) {
|
|
453
|
+
const detail = { ...identity, content_index };
|
|
454
|
+
const emptyPart = part.type === 'refusal' ? { ...part, refusal: '' }
|
|
455
|
+
: { ...part, ...(typeof part.text === 'string' ? { text: '' } : {}) };
|
|
456
|
+
emit('response.content_part.added', { ...detail, part: emptyPart });
|
|
457
|
+
if (part.type === 'output_text') {
|
|
458
|
+
if (part.text) emit('response.output_text.delta', { ...detail, delta: part.text, logprobs: [] });
|
|
459
|
+
emit('response.output_text.done', { ...detail, text: part.text || '', logprobs: [] });
|
|
460
|
+
} else if (part.type === 'refusal') {
|
|
461
|
+
if (part.refusal) emit('response.refusal.delta', { ...detail, delta: part.refusal });
|
|
462
|
+
emit('response.refusal.done', { ...detail, refusal: part.refusal || '' });
|
|
463
|
+
}
|
|
464
|
+
emit('response.content_part.done', { ...detail, part });
|
|
465
|
+
}
|
|
466
|
+
for (const [summary_index, part] of (item.summary || []).entries()) {
|
|
467
|
+
const detail = { ...identity, summary_index };
|
|
468
|
+
emit('response.reasoning_summary_part.added', { ...detail, part: { ...part, text: '' } });
|
|
469
|
+
if (part.text) emit('response.reasoning_summary_text.delta', { ...detail, delta: part.text });
|
|
470
|
+
emit('response.reasoning_summary_text.done', { ...detail, text: part.text || '' });
|
|
471
|
+
emit('response.reasoning_summary_part.done', { ...detail, part });
|
|
472
|
+
}
|
|
473
|
+
if (item.type === 'function_call' || item.type === 'custom_tool_call') {
|
|
474
|
+
const custom = item.type === 'custom_tool_call';
|
|
475
|
+
const value = custom ? item.input : item.arguments;
|
|
476
|
+
const event = custom ? 'response.custom_tool_call_input' : 'response.function_call_arguments';
|
|
477
|
+
if (value) emit(event + '.delta', { ...identity, delta: value });
|
|
478
|
+
emit(event + '.done', { ...identity, [custom ? 'input' : 'arguments']: value || (custom ? '' : '{}') });
|
|
479
|
+
}
|
|
480
|
+
emit('response.output_item.done', { output_index, item });
|
|
481
|
+
}
|
|
482
|
+
const status = response.error || response.status === 'failed' ? 'failed'
|
|
483
|
+
: response.status === 'incomplete' ? 'incomplete' : 'completed';
|
|
484
|
+
emit('response.' + status, { response });
|
|
485
|
+
res.end();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function forwardResponsesSse(upstream, res, onActivity, apiKey) {
|
|
409
489
|
res.writeHead(200, {
|
|
410
490
|
'content-type': 'text/event-stream; charset=utf-8',
|
|
411
491
|
'cache-control': 'no-cache, no-store',
|
|
@@ -415,67 +495,199 @@ async function forwardResponsesSse(upstream, res, onActivity) {
|
|
|
415
495
|
throw providerError('provider_response_invalid', 'The provider returned an empty Responses stream.');
|
|
416
496
|
}
|
|
417
497
|
const reader = upstream.body.getReader();
|
|
418
|
-
const decoder = new TextDecoder();
|
|
419
|
-
const observer = { buffer: '', terminalResponse: null };
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
498
|
+
const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
499
|
+
const observer = { buffer: '', position: 0, lineStart: 0, terminalResponse: null };
|
|
500
|
+
const forward = frame => {
|
|
501
|
+
const safeFrame = inspectSseFrame(observer, frame, apiKey);
|
|
502
|
+
if (!res.writableEnded) res.write(safeFrame);
|
|
503
|
+
};
|
|
504
|
+
let complete = false;
|
|
505
|
+
try {
|
|
506
|
+
while (true) {
|
|
507
|
+
const { done, value } = await reader.read();
|
|
508
|
+
if (done) break;
|
|
509
|
+
onActivity();
|
|
510
|
+
forwardSseText(observer, decoder.decode(value, { stream: true }), forward);
|
|
511
|
+
}
|
|
512
|
+
forwardSseText(observer, decoder.decode(), forward, true);
|
|
513
|
+
complete = true;
|
|
514
|
+
} finally {
|
|
515
|
+
if (!complete) await reader.cancel().catch(() => {});
|
|
516
|
+
reader.releaseLock();
|
|
517
|
+
}
|
|
428
518
|
if (!res.writableEnded) res.end();
|
|
429
519
|
return observer.terminalResponse;
|
|
430
520
|
}
|
|
431
521
|
|
|
432
|
-
function
|
|
522
|
+
function forwardSseText(observer, text, forward, flush = false) {
|
|
433
523
|
observer.buffer += text;
|
|
524
|
+
// Hold each event until its diagnostics can be inspected. Line endings and
|
|
525
|
+
// successful frames remain intact, including CRLF split across HTTP chunks.
|
|
526
|
+
while (observer.position < observer.buffer.length) {
|
|
527
|
+
const position = observer.position;
|
|
528
|
+
const char = observer.buffer[position];
|
|
529
|
+
if (char !== '\r' && char !== '\n') { observer.position += 1; continue; }
|
|
530
|
+
if (char === '\r' && position + 1 === observer.buffer.length && !flush) break;
|
|
531
|
+
const end = position + (char === '\r' && observer.buffer[position + 1] === '\n' ? 2 : 1);
|
|
532
|
+
if (position === observer.lineStart) {
|
|
533
|
+
forward(observer.buffer.slice(0, end));
|
|
534
|
+
observer.buffer = observer.buffer.slice(end);
|
|
535
|
+
observer.position = observer.lineStart = 0;
|
|
536
|
+
} else {
|
|
537
|
+
observer.position = observer.lineStart = end;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
434
540
|
if (Buffer.byteLength(observer.buffer, 'utf8') > MAX_STREAM_BUFFER_BYTES) {
|
|
435
541
|
throw providerError('provider_stream_frame_too_large', 'Provider stream frame exceeded the local bridge limit.');
|
|
436
542
|
}
|
|
437
|
-
let match;
|
|
438
|
-
while ((match = /\r?\n\r?\n/.exec(observer.buffer))) {
|
|
439
|
-
const block = observer.buffer.slice(0, match.index);
|
|
440
|
-
observer.buffer = observer.buffer.slice(match.index + match[0].length);
|
|
441
|
-
observeSseBlock(observer, block);
|
|
442
|
-
}
|
|
443
543
|
if (flush && observer.buffer.trim()) {
|
|
444
|
-
|
|
544
|
+
forward(observer.buffer);
|
|
445
545
|
observer.buffer = '';
|
|
446
546
|
}
|
|
447
547
|
}
|
|
448
548
|
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
549
|
+
function inspectSseFrame(observer, frame, apiKey) {
|
|
550
|
+
if (Buffer.byteLength(frame, 'utf8') > MAX_STREAM_BUFFER_BYTES) {
|
|
551
|
+
throw providerError('provider_stream_frame_too_large', 'Provider stream frame exceeded the local bridge limit.');
|
|
552
|
+
}
|
|
553
|
+
const parts = frame.split(/(\r\n|\r|\n)/);
|
|
554
|
+
const dataIndexes = [];
|
|
555
|
+
let event = '';
|
|
556
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
557
|
+
const line = parts[index].replace(/^\uFEFF/, '');
|
|
558
|
+
if (/^data(?::|$)/.test(line)) dataIndexes.push(index);
|
|
559
|
+
if (line.startsWith('event:')) event = line.slice(6).replace(/^ /, '');
|
|
560
|
+
}
|
|
561
|
+
const failedEvent = ['error', 'response.failed', 'response.error'].includes(event);
|
|
562
|
+
if (!dataIndexes.length && !failedEvent) return frame;
|
|
563
|
+
const data = dataIndexes.map(index => parts[index].replace(/^\uFEFF/, '').replace(/^data:? ?/, '')).join('\n');
|
|
564
|
+
if ((!data || data === '[DONE]') && !failedEvent) return frame;
|
|
565
|
+
let payload;
|
|
456
566
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
567
|
+
payload = JSON.parse(data);
|
|
568
|
+
} catch (_error) {
|
|
569
|
+
throw providerError('provider_response_invalid', 'The provider returned an invalid Responses stream event.');
|
|
570
|
+
}
|
|
571
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
572
|
+
throw providerError('provider_response_invalid', 'The provider returned an invalid Responses stream event.');
|
|
573
|
+
}
|
|
574
|
+
const changed = sanitizeProtocolErrors(payload, apiKey, failedEvent || hasDuplicateJsonKeys(data));
|
|
575
|
+
if (['response.completed', 'response.incomplete', 'response.failed'].includes(payload.type || event) && payload.response) {
|
|
576
|
+
observer.terminalResponse = payload.response;
|
|
577
|
+
}
|
|
578
|
+
if (!changed) return frame;
|
|
579
|
+
parts[dataIndexes[0]] = `data: ${JSON.stringify(payload)}`;
|
|
580
|
+
for (const index of dataIndexes.slice(1)) parts[index] = parts[index + 1] = '';
|
|
581
|
+
return parts.join('');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function sanitizeProtocolErrors(payload, apiKey, failedEvent = false) {
|
|
585
|
+
let changed = false;
|
|
586
|
+
const clean = value => {
|
|
587
|
+
if (typeof value === 'string' && value) {
|
|
588
|
+
const safe = sanitizeProviderMessage(value, [apiKey], MAX_ERROR_BYTES);
|
|
589
|
+
changed ||= safe !== value;
|
|
590
|
+
return safe;
|
|
460
591
|
}
|
|
461
|
-
|
|
592
|
+
if (value && typeof value === 'object') {
|
|
593
|
+
for (const key of Object.keys(value)) value[key] = clean(value[key]);
|
|
594
|
+
}
|
|
595
|
+
return value;
|
|
596
|
+
};
|
|
597
|
+
const visit = (value, inheritedFailure) => {
|
|
598
|
+
if (!value || typeof value !== 'object') return;
|
|
599
|
+
if (value.error != null) { changed = true; value.error = clean(value.error); }
|
|
600
|
+
const failed = inheritedFailure || ['error', 'response.failed', 'response.error'].includes(value.type)
|
|
601
|
+
|| ['failed', 'error'].includes(value.status);
|
|
602
|
+
if (failed) {
|
|
603
|
+
// Re-encode failures even when the surviving values need no replacement:
|
|
604
|
+
// duplicate JSON keys must not retain an earlier raw diagnostic value.
|
|
605
|
+
changed = true;
|
|
606
|
+
for (const key of ['message', 'code', 'param', 'detail', 'details', 'reason', 'description']) {
|
|
607
|
+
if (value[key] != null) value[key] = clean(value[key]);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Response envelopes carry diagnostics; model output and tool arguments
|
|
611
|
+
// retain their original protocol semantics, even if they resemble secrets.
|
|
612
|
+
visit(value.response, failed);
|
|
613
|
+
};
|
|
614
|
+
visit(payload, failedEvent);
|
|
615
|
+
return changed;
|
|
462
616
|
}
|
|
463
617
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
618
|
+
function hasDuplicateJsonKeys(text) {
|
|
619
|
+
// Called only after JSON.parse succeeds. Track keys per object and skip
|
|
620
|
+
// entire string values, including JSON-looking tool arguments. A duplicate
|
|
621
|
+
// can hide both an error value and an earlier failed status, so its protocol
|
|
622
|
+
// diagnostics require cleaning even when the surviving status is completed.
|
|
623
|
+
const stack = [];
|
|
624
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
625
|
+
const char = text[index];
|
|
626
|
+
if (char === '{') stack.push({ keys: new Set(), expectsKey: true });
|
|
627
|
+
else if (char === '[') stack.push(null);
|
|
628
|
+
else if (char === '}' || char === ']') stack.pop();
|
|
629
|
+
else if (char === ',' && stack.at(-1)) stack.at(-1).expectsKey = true;
|
|
630
|
+
else if (char === '"') {
|
|
631
|
+
let end = index;
|
|
632
|
+
while (true) {
|
|
633
|
+
end = text.indexOf('"', end + 1);
|
|
634
|
+
let backslashes = 0;
|
|
635
|
+
for (let at = end - 1; text[at] === '\\'; at -= 1) backslashes += 1;
|
|
636
|
+
if (backslashes % 2 === 0) break;
|
|
637
|
+
}
|
|
638
|
+
const object = stack.at(-1);
|
|
639
|
+
if (object?.expectsKey) {
|
|
640
|
+
const key = JSON.parse(text.slice(index, end + 1));
|
|
641
|
+
if (object.keys.has(key)) return true;
|
|
642
|
+
object.keys.add(key);
|
|
643
|
+
object.expectsKey = false;
|
|
644
|
+
}
|
|
645
|
+
index = end;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function forwardCompactResponse(upstream, res, onActivity, apiKey) {
|
|
467
652
|
if (!upstream.body) {
|
|
468
|
-
|
|
469
|
-
return;
|
|
653
|
+
throw providerError('provider_response_invalid', 'The provider returned an empty compact response.');
|
|
470
654
|
}
|
|
471
655
|
const reader = upstream.body.getReader();
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
656
|
+
const chunks = [];
|
|
657
|
+
let bytes = 0;
|
|
658
|
+
let complete = false;
|
|
659
|
+
try {
|
|
660
|
+
while (true) {
|
|
661
|
+
const { done, value } = await reader.read();
|
|
662
|
+
if (done) break;
|
|
663
|
+
onActivity();
|
|
664
|
+
bytes += value.byteLength;
|
|
665
|
+
if (bytes > MAX_COMPACT_RESPONSE_BYTES) {
|
|
666
|
+
throw providerError('provider_response_too_large', 'The compact response exceeded the 32 MiB local bridge limit.');
|
|
667
|
+
}
|
|
668
|
+
chunks.push(Buffer.from(value));
|
|
669
|
+
}
|
|
670
|
+
complete = true;
|
|
671
|
+
} finally {
|
|
672
|
+
if (!complete) await reader.cancel().catch(() => {});
|
|
673
|
+
reader.releaseLock();
|
|
477
674
|
}
|
|
478
|
-
|
|
675
|
+
const body = Buffer.concat(chunks, bytes);
|
|
676
|
+
let text;
|
|
677
|
+
let payload;
|
|
678
|
+
try {
|
|
679
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(body);
|
|
680
|
+
payload = JSON.parse(text);
|
|
681
|
+
} catch (_error) {
|
|
682
|
+
throw providerError('provider_response_invalid', 'The provider returned an invalid compact JSON response.');
|
|
683
|
+
}
|
|
684
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
685
|
+
throw providerError('provider_response_invalid', 'The provider returned an invalid compact JSON response.');
|
|
686
|
+
}
|
|
687
|
+
const changed = sanitizeProtocolErrors(payload, apiKey, hasDuplicateJsonKeys(text));
|
|
688
|
+
const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8';
|
|
689
|
+
res.writeHead(upstream.status, { 'content-type': contentType, 'cache-control': 'no-store' });
|
|
690
|
+
if (!res.writableEnded) res.end(changed ? JSON.stringify(payload) : body);
|
|
479
691
|
}
|
|
480
692
|
|
|
481
693
|
function buildResponsesUrl(baseUrl, launch = {}, route = 'responses') {
|
|
@@ -521,8 +733,8 @@ function buildUpstreamHeaders(launch = {}) {
|
|
|
521
733
|
return headers;
|
|
522
734
|
}
|
|
523
735
|
|
|
524
|
-
async function forwardUpstreamError(upstream, res, apiKey) {
|
|
525
|
-
const text = (await upstream
|
|
736
|
+
async function forwardUpstreamError(upstream, res, apiKey, onActivity) {
|
|
737
|
+
const text = (await readProviderText(upstream, onActivity)).slice(0, MAX_ERROR_BYTES);
|
|
526
738
|
let message = `Provider returned HTTP ${upstream.status}.`;
|
|
527
739
|
try {
|
|
528
740
|
const parsed = JSON.parse(text);
|
|
@@ -555,12 +767,14 @@ function normalizeBridgeErrorCode(value) {
|
|
|
555
767
|
return [
|
|
556
768
|
'provider_connection_timeout',
|
|
557
769
|
'provider_response_invalid',
|
|
770
|
+
'provider_response_too_large',
|
|
558
771
|
'provider_protocol_incompatible',
|
|
559
772
|
'provider_request_invalid',
|
|
560
773
|
'provider_request_too_large',
|
|
561
774
|
'provider_responses_continuation_repeated',
|
|
562
775
|
'provider_responses_continuation_unrecoverable',
|
|
563
|
-
'provider_continuation_history_limit'
|
|
776
|
+
'provider_continuation_history_limit',
|
|
777
|
+
'provider_upstream_error'
|
|
564
778
|
].includes(value) ? value : 'provider_bridge_failed';
|
|
565
779
|
}
|
|
566
780
|
|
|
@@ -18,16 +18,36 @@ function buildRuntimeFileManifest(options = {}) {
|
|
|
18
18
|
fileEntry(packageRoot, 'package.json'),
|
|
19
19
|
...directoryEntries(packageRoot, 'native-host/src'),
|
|
20
20
|
...directoryEntries(packageRoot, 'extension/src/shared'),
|
|
21
|
+
...bootstrapPayloadEntries(packageRoot),
|
|
21
22
|
fileEntry(packageRoot, 'scripts/codex-json-agent.mjs'),
|
|
22
23
|
fileEntry(packageRoot, 'scripts/install-native-host.mjs'),
|
|
23
24
|
fileEntry(packageRoot, 'scripts/uninstall-native-host.mjs')
|
|
24
25
|
];
|
|
25
26
|
|
|
26
27
|
return files
|
|
27
|
-
.filter((entry) => !allowedFiles || allowedFiles.has(entry.relativePath))
|
|
28
|
+
.filter((entry) => !allowedFiles || allowedFiles.has(entry.sourceRelativePath || entry.relativePath))
|
|
28
29
|
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Source packages hold one authoritative Bootstrap tree. Materialize its copy
|
|
33
|
+
// under the native-host namespace accepted by the already-published updater.
|
|
34
|
+
// Standalone Native tarballs already contain that mapped copy.
|
|
35
|
+
function bootstrapPayloadEntries(packageRoot) {
|
|
36
|
+
const sourceDir = 'extension/bootstrap';
|
|
37
|
+
const payloadDir = 'native-host/bootstrap-payload';
|
|
38
|
+
if (!fs.existsSync(path.join(packageRoot, sourceDir))) {
|
|
39
|
+
return fs.existsSync(path.join(packageRoot, payloadDir)) ? directoryEntries(packageRoot, payloadDir) : [];
|
|
40
|
+
}
|
|
41
|
+
return directoryEntries(packageRoot, sourceDir).map(entry => {
|
|
42
|
+
const filename = path.posix.relative(sourceDir, entry.relativePath);
|
|
43
|
+
return {
|
|
44
|
+
...entry,
|
|
45
|
+
sourceRelativePath: entry.relativePath,
|
|
46
|
+
relativePath: path.posix.join(payloadDir, filename === 'manifest.template.json' ? filename : 'bootstrap/' + filename)
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
function assertSafeManagedRuntimeRoot(runtimeRoot, options = {}) {
|
|
32
52
|
if (!runtimeRoot || typeof runtimeRoot !== 'string') {
|
|
33
53
|
throw new Error('Runtime root is required');
|
|
@@ -29,13 +29,40 @@ function prepareSelectionScope(value, workspacePath, projectId) {
|
|
|
29
29
|
if (error.code === 'selected_context_unresolved') throw error;
|
|
30
30
|
throw invalid('The selected file could not be read. Select it again after the project is synchronized.');
|
|
31
31
|
}
|
|
32
|
-
const digest =
|
|
33
|
-
if (digest
|
|
34
|
-
|
|
32
|
+
const digest = hash(before);
|
|
33
|
+
if (digest === selection.documentHash && before.slice(selection.from, selection.to) === selection.text) {
|
|
34
|
+
return { selection, before };
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
const proof = selection.rebaseEvidence;
|
|
37
|
+
const candidates = new Map();
|
|
38
|
+
const add = (from, to) => {
|
|
39
|
+
if (Number.isSafeInteger(from) && from >= 0 && to <= before.length
|
|
40
|
+
&& before.slice(from, to) === selection.text) candidates.set(from + ':' + to, { from, to });
|
|
41
|
+
};
|
|
42
|
+
if (proof) {
|
|
43
|
+
// Full flanks define positions; never search for a possibly repeated passage.
|
|
44
|
+
if (selection.from <= before.length && hash(before.slice(0, selection.from)) === proof.prefixHash) {
|
|
45
|
+
add(selection.from, selection.from + selection.text.length);
|
|
46
|
+
}
|
|
47
|
+
const suffixLength = proof.documentLength - selection.to;
|
|
48
|
+
const to = before.length - suffixLength;
|
|
49
|
+
if (to >= 0 && hash(before.slice(to)) === proof.suffixHash) add(to - selection.text.length, to);
|
|
50
|
+
}
|
|
51
|
+
if (candidates.size !== 1) {
|
|
52
|
+
throw invalid('The selected passage could not be safely mapped to the changed document. Select it again before editing.');
|
|
53
|
+
}
|
|
54
|
+
const { from, to } = [...candidates.values()][0];
|
|
55
|
+
const rebased = SelectionContext.normalize({ ...selection, from, to, documentHash: digest,
|
|
56
|
+
lineStart: before.slice(0, from).split('\n').length,
|
|
57
|
+
lineEnd: before.slice(0, Math.max(from, to - 1)).split('\n').length,
|
|
58
|
+
rebaseEvidence: { v: 1, documentLength: before.length,
|
|
59
|
+
prefixHash: hash(before.slice(0, from)), suffixHash: hash(before.slice(to)) } });
|
|
60
|
+
return { selection: rebased, before, rebased: true,
|
|
61
|
+
original: { from: selection.from, to: selection.to, documentHash: selection.documentHash } };
|
|
37
62
|
}
|
|
38
63
|
|
|
64
|
+
function hash(value) { return crypto.createHash('sha256').update(value, 'utf8').digest('hex'); }
|
|
65
|
+
|
|
39
66
|
function filterSelectionChanges(changes, scope) {
|
|
40
67
|
const items = Array.isArray(changes) ? changes : [];
|
|
41
68
|
if (scope?.selection?.mode !== 'edit') return { changes: items, unsupportedChanges: [] };
|
|
@@ -46,11 +73,12 @@ function filterSelectionChanges(changes, scope) {
|
|
|
46
73
|
&& change.previousContent === before && typeof change.content === 'string'
|
|
47
74
|
&& change.content.length >= prefix.length + suffix.length
|
|
48
75
|
&& change.content.startsWith(prefix) && change.content.endsWith(suffix);
|
|
49
|
-
|
|
76
|
+
const selectedPathCount = items.filter(change => change?.path === selection.path).length;
|
|
77
|
+
const allowed = change => selectedPathCount === 1 && withinSelection(change);
|
|
50
78
|
return {
|
|
51
|
-
changes:
|
|
52
|
-
unsupportedChanges: items.map(change => ({
|
|
53
|
-
type: 'unsupported-local-file', path: change
|
|
79
|
+
changes: items.filter(allowed),
|
|
80
|
+
unsupportedChanges: items.filter(change => !allowed(change)).map(change => ({
|
|
81
|
+
type: 'unsupported-local-file', path: change?.path || '', reason: 'selection_scope_violation'
|
|
54
82
|
}))
|
|
55
83
|
};
|
|
56
84
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Project the broker's FIFO-with-skip scheduler in worker-budget-sized waves.
|
|
4
|
+
// Running workers reserve their current wave; queued files are already canonical.
|
|
5
|
+
// This is admission accounting, not a timer for the parent turn or OS cleanup.
|
|
6
|
+
function estimateWorkerWaves(runningFiles, queuedFiles, maxWorkers) {
|
|
7
|
+
const pending = queuedFiles.slice();
|
|
8
|
+
let active = runningFiles.slice();
|
|
9
|
+
let waves = 0;
|
|
10
|
+
while (active.length || pending.length) {
|
|
11
|
+
const occupied = new Set(active.flat());
|
|
12
|
+
while (active.length < maxWorkers) {
|
|
13
|
+
const index = pending.findIndex(files => !files.some(file => occupied.has(file)));
|
|
14
|
+
if (index === -1) break;
|
|
15
|
+
const [files] = pending.splice(index, 1);
|
|
16
|
+
active.push(files);
|
|
17
|
+
for (const file of files) occupied.add(file);
|
|
18
|
+
}
|
|
19
|
+
if (!active.length) return Infinity;
|
|
20
|
+
waves += 1;
|
|
21
|
+
active = [];
|
|
22
|
+
}
|
|
23
|
+
return waves;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { estimateWorkerWaves };
|