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.
Files changed (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. 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 fetch(buildResponsesUrl(launch.baseUrl, launch, route), {
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 forwardOpaqueResponse(upstream, res, lifecycle.touch);
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
- response = await upstream.json();
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
- sendJson(res, 200, response);
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
- async function forwardResponsesSse(upstream, res, onActivity) {
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
- while (true) {
421
- const { done, value } = await reader.read();
422
- if (done) break;
423
- onActivity();
424
- observeSseText(observer, decoder.decode(value, { stream: true }));
425
- if (!res.writableEnded) res.write(Buffer.from(value));
426
- }
427
- observeSseText(observer, decoder.decode(), true);
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 observeSseText(observer, text, flush = false) {
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
- observeSseBlock(observer, observer.buffer);
544
+ forward(observer.buffer);
445
545
  observer.buffer = '';
446
546
  }
447
547
  }
448
548
 
449
- function observeSseBlock(observer, block) {
450
- const data = String(block || '')
451
- .split(/\r?\n/)
452
- .filter(line => line.startsWith('data:'))
453
- .map(line => line.slice(5).trimStart())
454
- .join('\n');
455
- if (!data || data === '[DONE]') return;
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
- const payload = JSON.parse(data);
458
- if (['response.completed', 'response.incomplete', 'response.failed'].includes(payload?.type) && payload.response) {
459
- observer.terminalResponse = payload.response;
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
- } catch (_error) {}
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
- async function forwardOpaqueResponse(upstream, res, onActivity) {
465
- const contentType = upstream.headers.get('content-type') || 'application/json; charset=utf-8';
466
- res.writeHead(upstream.status, { 'content-type': contentType, 'cache-control': 'no-store' });
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
- res.end();
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
- while (true) {
473
- const { done, value } = await reader.read();
474
- if (done) break;
475
- onActivity();
476
- if (!res.writableEnded) res.write(Buffer.from(value));
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
- if (!res.writableEnded) res.end();
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.text()).slice(0, MAX_ERROR_BYTES);
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 = crypto.createHash('sha256').update(before, 'utf8').digest('hex');
33
- if (digest !== selection.documentHash || before.slice(selection.from, selection.to) !== selection.text) {
34
- throw invalid('The document changed after the selection was captured. Select the passage again before editing.');
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
- return { selection, before };
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
- if (items.every(withinSelection)) return { changes: items, unsupportedChanges: [] };
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.path, reason: 'selection_scope_violation'
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 };