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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Coordination for the one compatible 2.5.0 -> 2.5.1 Bootstrap revision.
|
|
4
|
+
// All mutations run under updateManager's existing cross-process lock.
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const vm = require('node:vm');
|
|
9
|
+
const { spawn } = require('node:child_process');
|
|
10
|
+
const Files = require('./bootstrapMigration');
|
|
11
|
+
const Contract = require('./bootstrapMigrationContract');
|
|
12
|
+
const { updateError } = require('./updateTrust');
|
|
13
|
+
|
|
14
|
+
const BOOTSTRAP_DEADLINE_MS = 120000;
|
|
15
|
+
const MAX_AUTOMATIC_ROLLBACK_ATTEMPTS = 3;
|
|
16
|
+
const BOOTSTRAP_STATES = new Set(['bootstrap_pending', 'bootstrap_rolling_back']);
|
|
17
|
+
const backupRoot = context => path.join(context.updatesRoot, 'bootstrap-migrations');
|
|
18
|
+
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
19
|
+
|
|
20
|
+
function migrationHint(context, journal) {
|
|
21
|
+
if (!journal || !['awaiting_health', ...BOOTSTRAP_STATES].includes(journal.state)) return null;
|
|
22
|
+
if (journal.bootstrapMigration) return journal.bootstrapMigration;
|
|
23
|
+
if (journal.sourceVersion !== '2.5.0' || journal.targetVersion !== '2.5.1') return null;
|
|
24
|
+
// This exact release pair requires migration even when discovery data was
|
|
25
|
+
// lost. Missing evidence must not downgrade an update into a false success.
|
|
26
|
+
return { phase: 'pending', targetRevision: '3.1' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function bindBootstrapMigration(context, journal, deps) {
|
|
30
|
+
if (journal?.state !== 'awaiting_health' || journal.bootstrapMigration || !migrationHint(context, journal)) return journal;
|
|
31
|
+
const candidate = readJson(path.join(context.updatesRoot, 'candidate.json'));
|
|
32
|
+
let verificationError = '';
|
|
33
|
+
try { deps.assertAuthorization(journal); verifiedDescriptor(journal, candidate, deps); }
|
|
34
|
+
catch (error) { verificationError = error.code || 'update_bootstrap_manifest_mismatch'; }
|
|
35
|
+
const next = { ...journal, state: 'bootstrap_pending', bootstrapMigration: {
|
|
36
|
+
phase: 'pending', targetRevision: '3.1', reloadCount: 0, verificationError,
|
|
37
|
+
manifestBase64: candidate?.manifestBase64 || '', signatureBase64: candidate?.signatureBase64 || ''
|
|
38
|
+
}, updatedAt: new Date().toISOString() };
|
|
39
|
+
deps.writeJournal(next);
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function verifiedDescriptor(journal, evidence, deps) {
|
|
44
|
+
const bytes = Buffer.from(evidence?.manifestBase64 || '', 'base64');
|
|
45
|
+
if (!journal.manifestSha256 || hash(bytes) !== journal.manifestSha256) {
|
|
46
|
+
throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap migration does not match the applied release.');
|
|
47
|
+
}
|
|
48
|
+
const manifest = deps.verifyManifest(bytes, Buffer.from(evidence?.signatureBase64 || '', 'base64'));
|
|
49
|
+
if (manifest.version !== journal.targetVersion || manifest.updateBundle?.sha256 !== journal.bundleSha256
|
|
50
|
+
|| manifest.updateBundle?.size !== journal.bundleSize) {
|
|
51
|
+
throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap migration does not match the update archive.');
|
|
52
|
+
}
|
|
53
|
+
const descriptor = Contract.validateBootstrapMigrationDescriptor(manifest.bootstrapMigration, { targetVersion: journal.targetVersion });
|
|
54
|
+
if (!descriptor.sourceVersions.includes(journal.sourceVersion)) {
|
|
55
|
+
throw updateError('update_bootstrap_source_version_mismatch', 'This installation has no compatible Bootstrap migration.');
|
|
56
|
+
}
|
|
57
|
+
return descriptor;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function payloadRootFor(context, journal) {
|
|
61
|
+
return path.join(context.nativeRoot, 'versions', journal.targetVersion, 'native-host', 'bootstrap-payload');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadPlan(context, journal) {
|
|
65
|
+
const root = backupRoot(context);
|
|
66
|
+
if (!fs.existsSync(path.join(root, journal.id, 'state.json'))) return null;
|
|
67
|
+
const plan = Files.loadBootstrapMigration({ backupRoot: root, transactionId: journal.id });
|
|
68
|
+
if (plan.extensionRoot !== context.extensionRoot || plan.payloadRoot !== payloadRootFor(context, journal)
|
|
69
|
+
|| plan.sourceVersion !== journal.sourceVersion || plan.targetVersion !== journal.targetVersion) {
|
|
70
|
+
throw updateError('update_bootstrap_transaction_mismatch', 'Bootstrap recovery belongs to a different installation.');
|
|
71
|
+
}
|
|
72
|
+
return plan;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function advanceBootstrapMigration(context, journal, params, deps, options = {}) {
|
|
76
|
+
if (!journal || !migrationHint(context, journal)) return { state: 'not_required' };
|
|
77
|
+
if (params.transactionId !== journal.id) throw updateError('update_transaction_mismatch', 'Bootstrap update transaction changed.');
|
|
78
|
+
journal = bindBootstrapMigration(context, journal, deps);
|
|
79
|
+
if (journal.state !== 'bootstrap_pending') {
|
|
80
|
+
throw updateError('update_transaction_state_changed', 'Bootstrap migration is no longer waiting to continue.');
|
|
81
|
+
}
|
|
82
|
+
if (deps.runningVersion !== journal.targetVersion) {
|
|
83
|
+
throw updateError('update_bootstrap_native_restart_required', 'The target Native Host must start before updating Bootstrap.');
|
|
84
|
+
}
|
|
85
|
+
deps.assertAuthorization(journal);
|
|
86
|
+
const descriptor = verifiedDescriptor(journal, journal.bootstrapMigration, deps);
|
|
87
|
+
const payloadRoot = payloadRootFor(context, journal);
|
|
88
|
+
Contract.assertBootstrapManifestShape(JSON.parse(fs.readFileSync(path.join(context.extensionRoot, 'manifest.json'), 'utf8')),
|
|
89
|
+
{ version: journal.targetVersion });
|
|
90
|
+
Contract.assertBootstrapManifestShape(JSON.parse(fs.readFileSync(path.join(payloadRoot, 'manifest.template.json'), 'utf8')),
|
|
91
|
+
{ version: journal.targetVersion });
|
|
92
|
+
// A syntax failure must be found before changing the only background entry.
|
|
93
|
+
for (const file of descriptor.files) {
|
|
94
|
+
const bytes = fs.readFileSync(path.join(payloadRoot, file.path));
|
|
95
|
+
if (hash(bytes) !== file.targetSha256) throw updateError('update_bootstrap_payload_mismatch', 'Bootstrap payload changed after verification.');
|
|
96
|
+
if (file.path.endsWith('.js')) new vm.Script(bytes.toString('utf8'), { filename: file.path });
|
|
97
|
+
}
|
|
98
|
+
deps.verifyArchive(journal);
|
|
99
|
+
let plan = loadPlan(context, journal);
|
|
100
|
+
if (!plan) plan = Files.prepareBootstrapMigration({
|
|
101
|
+
descriptor, payloadRoot, extensionRoot: context.extensionRoot, backupRoot: backupRoot(context),
|
|
102
|
+
transactionId: journal.id, sourceVersion: journal.sourceVersion, targetVersion: journal.targetVersion
|
|
103
|
+
});
|
|
104
|
+
if (plan.descriptorSha256 !== hash(JSON.stringify(descriptor))) {
|
|
105
|
+
throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap recovery material does not match the signed release.');
|
|
106
|
+
}
|
|
107
|
+
let migration = { ...journal.bootstrapMigration, phase: plan.state === 'applied' ? 'applied' : 'prepared',
|
|
108
|
+
targetDigest: plan.targetDigest, sourceDigest: plan.sourceDigest,
|
|
109
|
+
deadline: journal.bootstrapMigration.deadline || Date.now() + BOOTSTRAP_DEADLINE_MS };
|
|
110
|
+
if (Date.now() >= migration.deadline) throw updateError('update_bootstrap_health_timeout', 'Bootstrap update did not finish within its health window.');
|
|
111
|
+
journal = { ...journal, bootstrapMigration: migration };
|
|
112
|
+
deps.writeJournal(journal);
|
|
113
|
+
// A new connection always establishes a new monitor handshake. PID reuse
|
|
114
|
+
// alone can never authorize writing the only browser background entry.
|
|
115
|
+
{
|
|
116
|
+
const token = crypto.randomUUID();
|
|
117
|
+
migration = { ...migration, watchdogToken: token, watchdogPid: null };
|
|
118
|
+
journal = { ...journal, bootstrapMigration: migration };
|
|
119
|
+
deps.writeJournal(journal);
|
|
120
|
+
const pid = await (options.startWatchdog || startWatchdog)(context, journal, token);
|
|
121
|
+
migration = { ...migration, watchdogPid: pid };
|
|
122
|
+
journal = { ...journal, bootstrapMigration: migration };
|
|
123
|
+
deps.writeJournal(journal);
|
|
124
|
+
}
|
|
125
|
+
Files.applyBootstrapMigration(plan, options.fileHooks);
|
|
126
|
+
const observed = Files.verifyBootstrapMigration(plan);
|
|
127
|
+
migration = { ...migration, phase: 'applied', targetDigest: observed.digest };
|
|
128
|
+
if (params.bootstrapRevision === descriptor.targetRevision) {
|
|
129
|
+
migration = { ...migration, phase: 'loaded', loadedAt: new Date().toISOString() };
|
|
130
|
+
deps.writeJournal({ ...journal, state: 'awaiting_health', bootstrapMigration: migration });
|
|
131
|
+
return { state: 'awaiting_health', transactionId: journal.id, targetVersion: journal.targetVersion,
|
|
132
|
+
bootstrapRevision: descriptor.targetRevision };
|
|
133
|
+
}
|
|
134
|
+
if (migration.reloadCount >= 2) throw updateError('update_bootstrap_reload_failed', 'The new Bootstrap did not load after extension restart.');
|
|
135
|
+
migration.reloadCount += 1;
|
|
136
|
+
deps.writeJournal({ ...journal, bootstrapMigration: migration });
|
|
137
|
+
return { state: 'reload_required', transactionId: journal.id, targetVersion: journal.targetVersion,
|
|
138
|
+
bootstrapRevision: descriptor.targetRevision };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertBootstrapConfirmation(context, journal, params) {
|
|
142
|
+
if (!journal.bootstrapMigration) {
|
|
143
|
+
if (migrationHint(context, journal)) throw updateError('update_bootstrap_confirmation_pending', 'Bootstrap migration is still pending.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (journal.bootstrapMigration.phase !== 'loaded' || params.bootstrapRevision !== journal.bootstrapMigration.targetRevision) {
|
|
147
|
+
throw updateError('update_bootstrap_confirmation_pending', 'The loaded Bootstrap did not confirm the target revision.');
|
|
148
|
+
}
|
|
149
|
+
const plan = loadPlan(context, journal);
|
|
150
|
+
if (!plan || Files.verifyBootstrapMigration(plan).digest !== journal.bootstrapMigration.targetDigest) {
|
|
151
|
+
throw updateError('update_bootstrap_target_mismatch', 'The confirmed Bootstrap bytes no longer match the target.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function restoreBootstrap(context, journal) {
|
|
156
|
+
const plan = loadPlan(context, journal);
|
|
157
|
+
if (!plan) {
|
|
158
|
+
if (journal.bootstrapMigration && journal.bootstrapMigration.phase !== 'pending') {
|
|
159
|
+
throw updateError('update_bootstrap_record_missing', 'Bootstrap recovery material is missing.');
|
|
160
|
+
}
|
|
161
|
+
if (journal.bootstrapMigration) return untouchedBootstrap(context, journal);
|
|
162
|
+
}
|
|
163
|
+
return plan ? Files.restoreBootstrapMigration(plan) : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function bootstrapEvidence(context, journal, expected) {
|
|
167
|
+
const plan = loadPlan(context, journal);
|
|
168
|
+
if (!plan && journal.bootstrapMigration) {
|
|
169
|
+
if (expected !== 'source' || !(journal.bootstrapMigration.phase === 'pending'
|
|
170
|
+
|| journal.bootstrapMigration.phase === 'restored' && journal.bootstrapMigration.sourceUnchanged === true)) {
|
|
171
|
+
throw updateError('update_bootstrap_record_missing', 'Bootstrap recovery material is missing.');
|
|
172
|
+
}
|
|
173
|
+
return untouchedBootstrap(context, journal);
|
|
174
|
+
}
|
|
175
|
+
return plan ? Files.verifyBootstrapMigration(plan, { expected }) : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
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.');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function bootstrapExpired(journal, { recoveryRequested = false, context } = {}) {
|
|
224
|
+
const migration = journal?.bootstrapMigration;
|
|
225
|
+
if (!migration || !['bootstrap_pending', 'bootstrap_rolling_back', 'awaiting_health'].includes(journal.state)) return false;
|
|
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)
|
|
231
|
+
|| (recoveryRequested && migration.phase !== 'pending' && !watchdogAlive(migration, context, journal));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function watchdogAlive(migration, context, journal) {
|
|
235
|
+
if (!Number.isInteger(migration?.watchdogPid) || migration.watchdogPid < 1) return false;
|
|
236
|
+
const heartbeat = context && journal && readJson(path.join(backupRoot(context), journal.id, 'watchdog.json'));
|
|
237
|
+
if (!heartbeat || heartbeat.token !== migration.watchdogToken || heartbeat.pid !== migration.watchdogPid
|
|
238
|
+
|| heartbeat.transactionId !== journal.id || Date.now() - heartbeat.at > 2500 || heartbeat.at > Date.now() + 1000) return false;
|
|
239
|
+
try { process.kill(migration.watchdogPid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function startWatchdog(context, journal, token) {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const child = spawn(process.execPath, [path.join(__dirname, 'bootstrapUpdateWatchdog.js'),
|
|
245
|
+
context.nativeRoot, context.extensionRoot, journal.id, token], {
|
|
246
|
+
env: { ...context.env }, detached: true, windowsHide: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc']
|
|
247
|
+
});
|
|
248
|
+
const timer = setTimeout(() => fail(new Error('Bootstrap recovery monitor did not become ready.')), 5000);
|
|
249
|
+
let settled = false;
|
|
250
|
+
function fail(error) {
|
|
251
|
+
if (settled) return;
|
|
252
|
+
settled = true; clearTimeout(timer);
|
|
253
|
+
try { child.kill(); } catch (_) { /* A failed child may already be gone. */ }
|
|
254
|
+
reject(Object.assign(error, { code: 'update_bootstrap_monitor_unavailable' }));
|
|
255
|
+
}
|
|
256
|
+
child.on('error', fail);
|
|
257
|
+
child.on('exit', () => { if (!settled) fail(new Error('Bootstrap recovery monitor exited before readiness.')); });
|
|
258
|
+
child.on('message', message => {
|
|
259
|
+
if (settled || message?.type !== 'ready' || message.token !== token || message.pid !== child.pid) return;
|
|
260
|
+
settled = true; clearTimeout(timer); child.disconnect(); child.unref(); resolve(child.pid);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readJson(filename) { try { return JSON.parse(fs.readFileSync(filename, 'utf8')); } catch (_) { return null; } }
|
|
266
|
+
|
|
267
|
+
module.exports = { BOOTSTRAP_DEADLINE_MS, BOOTSTRAP_STATES, MAX_AUTOMATIC_ROLLBACK_ATTEMPTS, migrationHint,
|
|
268
|
+
bindBootstrapMigration, advanceBootstrapMigration, assertBootstrapConfirmation, assertMigrationSource,
|
|
269
|
+
restoreBootstrap, bootstrapEvidence, bootstrapExpired, watchdogAlive };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Independent of the browser worker being replaced. The Native update lock
|
|
4
|
+
// serializes timeout recovery with normal health confirmation.
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const [nativeRoot, extensionRoot, transactionId, token] = process.argv.slice(2);
|
|
8
|
+
const { recoverBootstrapTransaction } = require('./updateManager');
|
|
9
|
+
if (typeof recoverBootstrapTransaction !== 'function') throw new Error('Bootstrap recovery entry is unavailable.');
|
|
10
|
+
const env = { ...process.env, CODEX_OVERLEAF_MANAGED: '1',
|
|
11
|
+
CODEX_OVERLEAF_MANAGED_NATIVE_ROOT: nativeRoot, CODEX_OVERLEAF_MANAGED_EXTENSION_ROOT: extensionRoot };
|
|
12
|
+
let busy = false;
|
|
13
|
+
let failures = 0;
|
|
14
|
+
function heartbeat() {
|
|
15
|
+
const directory = path.join(nativeRoot, 'updates', 'bootstrap-migrations', transactionId);
|
|
16
|
+
const temporary = path.join(directory, 'watchdog-' + process.pid + '.tmp');
|
|
17
|
+
fs.writeFileSync(temporary, JSON.stringify({ transactionId, token, pid: process.pid, at: Date.now() }), { mode: 0o600 });
|
|
18
|
+
fs.renameSync(temporary, path.join(directory, 'watchdog.json'));
|
|
19
|
+
}
|
|
20
|
+
const timer = setInterval(async () => {
|
|
21
|
+
if (busy) return;
|
|
22
|
+
busy = true;
|
|
23
|
+
try {
|
|
24
|
+
const journal = JSON.parse(fs.readFileSync(path.join(nativeRoot, 'updates', 'transaction.json'), 'utf8'));
|
|
25
|
+
if (journal.id !== transactionId || journal.bootstrapMigration?.watchdogToken !== token
|
|
26
|
+
|| ['committed', 'superseded'].includes(journal.state)
|
|
27
|
+
|| journal.state === 'rolled_back' && !journal.sourceActivationPending) {
|
|
28
|
+
clearInterval(timer); return;
|
|
29
|
+
}
|
|
30
|
+
heartbeat();
|
|
31
|
+
if (journal.sourceActivationPending || journal.bootstrapMigration?.deadline && Date.now() >= journal.bootstrapMigration.deadline) {
|
|
32
|
+
await recoverBootstrapTransaction({ env }, transactionId, token);
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
}
|
|
35
|
+
} catch (_) {
|
|
36
|
+
// Retry boundedly if a filesystem/lock is temporarily unavailable. Recovery
|
|
37
|
+
// material remains durable for the next Native start or popup recovery probe.
|
|
38
|
+
if (++failures >= 20) clearInterval(timer);
|
|
39
|
+
} finally { busy = false; }
|
|
40
|
+
}, 500);
|
|
41
|
+
const initial = JSON.parse(fs.readFileSync(path.join(nativeRoot, 'updates', 'transaction.json'), 'utf8'));
|
|
42
|
+
if (initial.id !== transactionId || initial.bootstrapMigration?.watchdogToken !== token) throw new Error('Bootstrap recovery identity changed.');
|
|
43
|
+
heartbeat();
|
|
44
|
+
process.send?.({ type: 'ready', token, pid: process.pid });
|
|
@@ -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
|
}
|
|
@@ -295,6 +297,7 @@ function completeMessage(state, res) {
|
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
function completeTool(state, res, tool) {
|
|
300
|
+
assertCompleteToolArguments(tool.arguments);
|
|
298
301
|
if (!tool.opened) openTool(state, res, tool);
|
|
299
302
|
tool.item.status = 'completed';
|
|
300
303
|
if (tool.kind === 'custom') {
|
|
@@ -419,6 +422,7 @@ function normalizeToolCalls(values, toolKinds) {
|
|
|
419
422
|
const name = normalizeText(value?.function?.name) || 'tool';
|
|
420
423
|
const callId = normalizeText(value?.id) || randomId('call');
|
|
421
424
|
const argumentsText = normalizeText(value?.function?.arguments) || '{}';
|
|
425
|
+
assertCompleteToolArguments(argumentsText);
|
|
422
426
|
const spec = toolKinds?.get(name);
|
|
423
427
|
const kind = (typeof spec === 'object' ? spec.kind : spec) === 'custom' ? 'custom' : 'function';
|
|
424
428
|
const originalName = typeof spec === 'object' ? spec.name || name : name;
|
|
@@ -489,6 +493,13 @@ function mapChatFinishReason(value) {
|
|
|
489
493
|
return { status: 'completed', incompleteReason: null };
|
|
490
494
|
}
|
|
491
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
|
+
|
|
492
503
|
function responseError(message, code = 'provider_response_invalid') {
|
|
493
504
|
const error = new Error(message);
|
|
494
505
|
error.code = code;
|
|
@@ -533,6 +544,14 @@ function unwrapCustomArguments(value) {
|
|
|
533
544
|
}
|
|
534
545
|
}
|
|
535
546
|
|
|
547
|
+
function assertCompleteToolArguments(value) {
|
|
548
|
+
try {
|
|
549
|
+
JSON.parse(value || '{}');
|
|
550
|
+
} catch (_error) {
|
|
551
|
+
throw responseError('The provider returned incomplete or invalid JSON tool arguments.');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
536
555
|
function reserveItem(state, item) {
|
|
537
556
|
const index = state.items.length;
|
|
538
557
|
state.items.push(item);
|
|
@@ -11,7 +11,9 @@ const {
|
|
|
11
11
|
const { requiresReasoningContentReplay } = require('./providerReasoning');
|
|
12
12
|
const { classifyResponsesRoute } = require('./providerBridgeRoutes');
|
|
13
13
|
const { providerError } = require('./providerProfile');
|
|
14
|
+
const { fetchProvider, readProviderText, readProviderJson } = require('./providerHttp');
|
|
14
15
|
const { sanitizeProviderMessage } = require('./providerRedaction');
|
|
16
|
+
const { isContextLimitError } = require('./providerContextError');
|
|
15
17
|
const { logDebug } = require('./debugLog');
|
|
16
18
|
const {
|
|
17
19
|
createProviderStreamLifecycle,
|
|
@@ -118,15 +120,19 @@ async function handleRequest({
|
|
|
118
120
|
const startedAt = Date.now();
|
|
119
121
|
const previousResponseId = String(requestBody.previous_response_id || '');
|
|
120
122
|
const previous = history.get(previousResponseId);
|
|
121
|
-
const
|
|
123
|
+
const fallbackAllowed = !previousResponseId || previousResponseId === continuationState.responseId;
|
|
124
|
+
const fallbackAnchor = previous || !fallbackAllowed
|
|
122
125
|
? []
|
|
123
126
|
: continuationState.anchorMessages;
|
|
127
|
+
const continuationAttempts = previous
|
|
128
|
+
? Number(previous.continuationAttempts) || 0
|
|
129
|
+
: fallbackAllowed ? continuationState.attempts : 0;
|
|
124
130
|
const translated = buildChatRequest({
|
|
125
131
|
requestBody,
|
|
126
132
|
launch,
|
|
127
133
|
historyMessages: previous?.messages || fallbackAnchor
|
|
128
134
|
});
|
|
129
|
-
assertReasoningContinuationBudget(translated.messages,
|
|
135
|
+
assertReasoningContinuationBudget(translated.messages, continuationAttempts);
|
|
130
136
|
const idleTimeoutMs = resolveProviderIdleTimeoutMs(launch);
|
|
131
137
|
const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, idleTimeoutMs);
|
|
132
138
|
logDebug('provider.bridge.request', {
|
|
@@ -141,7 +147,7 @@ async function handleRequest({
|
|
|
141
147
|
usedFallbackAnchor: fallbackAnchor.length > 0,
|
|
142
148
|
inputItemCount: Array.isArray(requestBody.input) ? requestBody.input.length : requestBody.input ? 1 : 0,
|
|
143
149
|
messageCount: translated.messages.length,
|
|
144
|
-
continuationAttempts
|
|
150
|
+
continuationAttempts,
|
|
145
151
|
idleTimeoutMs,
|
|
146
152
|
totalTimeoutMs
|
|
147
153
|
});
|
|
@@ -161,15 +167,15 @@ async function handleRequest({
|
|
|
161
167
|
res.on('close', onClientClose);
|
|
162
168
|
lifecycle.start();
|
|
163
169
|
try {
|
|
164
|
-
const upstream = await
|
|
170
|
+
const upstream = await fetchProvider(buildChatCompletionsUrl(launch.baseUrl, launch), {
|
|
165
171
|
method: 'POST',
|
|
166
172
|
headers: buildUpstreamHeaders(launch),
|
|
167
173
|
body: JSON.stringify(translated.body),
|
|
168
174
|
signal: controller.signal
|
|
169
|
-
});
|
|
175
|
+
}, launch);
|
|
170
176
|
lifecycle.touch();
|
|
171
177
|
if (!upstream.ok) {
|
|
172
|
-
await forwardUpstreamError(upstream, res, launch.apiKey);
|
|
178
|
+
await forwardUpstreamError(upstream, res, launch.apiKey, lifecycle.touch, requestBody.stream !== false);
|
|
173
179
|
return;
|
|
174
180
|
}
|
|
175
181
|
const context = {
|
|
@@ -193,7 +199,7 @@ async function handleRequest({
|
|
|
193
199
|
: 0;
|
|
194
200
|
if (anchored) {
|
|
195
201
|
continuationState.anchorMessages = continuation;
|
|
196
|
-
continuationState.attempts
|
|
202
|
+
continuationState.attempts = continuationAttempts + 1;
|
|
197
203
|
continuationState.responseId = converted.response.id;
|
|
198
204
|
} else {
|
|
199
205
|
continuationState.anchorMessages = [];
|
|
@@ -217,7 +223,7 @@ async function handleRequest({
|
|
|
217
223
|
rememberHistory(history, converted.response.id, [
|
|
218
224
|
...translated.messages,
|
|
219
225
|
...continuation
|
|
220
|
-
]);
|
|
226
|
+
], { continuationAttempts: anchored ? continuationAttempts + 1 : 0 });
|
|
221
227
|
};
|
|
222
228
|
if (requestBody.stream !== false) {
|
|
223
229
|
const contentType = String(upstream.headers.get('content-type') || '').toLowerCase();
|
|
@@ -231,11 +237,11 @@ async function handleRequest({
|
|
|
231
237
|
recoverInterrupted: lifecycle.recoverInterrupted
|
|
232
238
|
});
|
|
233
239
|
} else {
|
|
234
|
-
const converted = convertChatResponse(await upstream.
|
|
240
|
+
const converted = convertChatResponse(await readProviderJson(upstream, lifecycle.touch), context);
|
|
235
241
|
writeConvertedResponseAsSse(res, converted, context, remember);
|
|
236
242
|
}
|
|
237
243
|
} else {
|
|
238
|
-
const converted = convertChatResponse(await upstream.
|
|
244
|
+
const converted = convertChatResponse(await readProviderJson(upstream, lifecycle.touch), context);
|
|
239
245
|
remember(converted);
|
|
240
246
|
sendJson(res, 200, converted.response);
|
|
241
247
|
}
|
|
@@ -250,6 +256,10 @@ async function handleRequest({
|
|
|
250
256
|
if (controller.signal.aborted) {
|
|
251
257
|
throw providerError('provider_connection_timeout', 'The provider request was cancelled or timed out.');
|
|
252
258
|
}
|
|
259
|
+
if (error?.code === 'context_length_exceeded' && !res.headersSent) {
|
|
260
|
+
sendContextLimitError(res, sanitizeProviderMessage(error.message, [launch.apiKey]), requestBody.stream !== false);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
253
263
|
throw error;
|
|
254
264
|
} finally {
|
|
255
265
|
lifecycle.dispose();
|
|
@@ -376,7 +386,9 @@ function normalizeBridgeErrorCode(value) {
|
|
|
376
386
|
'provider_agent_tools_incompatible',
|
|
377
387
|
'provider_reasoning_continuation_exhausted',
|
|
378
388
|
'provider_stream_tool_parse_failed',
|
|
379
|
-
'provider_protocol_incompatible'
|
|
389
|
+
'provider_protocol_incompatible',
|
|
390
|
+
'context_length_exceeded',
|
|
391
|
+
'provider_upstream_error'
|
|
380
392
|
].includes(value) ? value : 'provider_bridge_failed';
|
|
381
393
|
}
|
|
382
394
|
|
|
@@ -386,19 +398,40 @@ function bridgeErrorMessage(code) {
|
|
|
386
398
|
: 'The local provider bridge failed.';
|
|
387
399
|
}
|
|
388
400
|
|
|
389
|
-
async function forwardUpstreamError(upstream, res, apiKey) {
|
|
390
|
-
const text = (await upstream
|
|
401
|
+
async function forwardUpstreamError(upstream, res, apiKey, onActivity, clientStream) {
|
|
402
|
+
const text = (await readProviderText(upstream, onActivity)).slice(0, MAX_ERROR_BYTES);
|
|
391
403
|
let message = `Provider returned HTTP ${upstream.status}.`;
|
|
404
|
+
let error;
|
|
392
405
|
try {
|
|
393
406
|
const parsed = JSON.parse(text);
|
|
407
|
+
error = parsed?.error || parsed;
|
|
394
408
|
message = parsed?.error?.message || parsed?.message || message;
|
|
395
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
|
+
}
|
|
396
414
|
sendJsonError(res, upstream.status, 'provider_upstream_error', sanitizeProviderMessage(message, [apiKey]));
|
|
397
415
|
}
|
|
398
416
|
|
|
399
|
-
function
|
|
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
|
+
|
|
432
|
+
function rememberHistory(history, responseId, messages, metadata = {}) {
|
|
400
433
|
history.delete(responseId);
|
|
401
|
-
history.set(responseId, { messages });
|
|
434
|
+
history.set(responseId, { messages, ...metadata });
|
|
402
435
|
while (history.size > MAX_HISTORY_ENTRIES) {
|
|
403
436
|
history.delete(history.keys().next().value);
|
|
404
437
|
}
|