codex-overleaf-link 2.5.1 → 2.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +148 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -15,10 +15,11 @@ const { buildCodexHomeEnv } = require('./codexHome');
15
15
  const { applyCodexSkillIsolation } = require('./codexSkillIsolation');
16
16
  const { buildCodexSpeedArgs } = require('./codexArgs');
17
17
  const { truncateText } = require('./debugLog');
18
+ const { isContextLimitError } = require('./providerContextError');
18
19
  const {
19
20
  reduceTaskResult
20
21
  } = require('./nativeTransportEnvelope');
21
- const { buildCodexTurnPrompt: buildCodexPromptParts } = require('./codexPromptAssembly');
22
+ const { buildCodexDeveloperInstructions, buildCodexTurnPrompt: buildCodexPromptParts, validatePromptSize } = require('./codexPromptAssembly');
22
23
  const { evaluateSkillCommand } = require('./commandApproval');
23
24
  const {
24
25
  getCodexOverleafSkillsRoot,
@@ -30,7 +31,7 @@ const { createNativeSubagentObserver } = require('./subagentTelemetry');
30
31
  const { prepareBinaryAssetChanges } = require('./nativeAssetTransfer');
31
32
  const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
32
33
  const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
33
- const { buildReadProgressRules, createReadProgressController, createRunEventScope } = require('./readProgressGuard');
34
+ const { createReadProgressController, createRunEventScope } = require('./readProgressGuard');
34
35
  const {
35
36
  createCodexIdleWatchdog,
36
37
  createProcessStopIdentity,
@@ -48,6 +49,28 @@ const { materializeTurnAttachments } = require('./turnAttachments');
48
49
 
49
50
  async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, providerLaunch, signal, onControlReady, onStopUnconfirmed, onProcessStarted } = {}) {
50
51
  throwIfAborted(signal);
52
+ if (params.operation === 'compact') {
53
+ const mirror = getProjectMirror(params.projectId, { rootDir });
54
+ let result;
55
+ try {
56
+ result = await (executeCodex || runCodexAppServerSession)({
57
+ operation: 'compact', threadId: params.threadId, workspacePath: mirror.workspacePath,
58
+ mode: 'ask', model: params.model || '', speedTier: normalizeSpeedTier(params.speedTier),
59
+ sandboxMode: 'read-only', approvalPolicy: 'never',
60
+ ...normalizeSkillLoadingSettings(params),
61
+ enabledCodexOverleafSkillIds: params.enabledCodexOverleafSkillIds,
62
+ enabledCodexLocalSkillIds: params.enabledCodexLocalSkillIds,
63
+ providerLaunch, env, emit, signal, onControlReady, onProcessStarted
64
+ });
65
+ } catch (error) {
66
+ if (error?.code === 'codex_process_stop_unconfirmed') onStopUnconfirmed?.(error);
67
+ throw error;
68
+ }
69
+ throwIfAborted(signal);
70
+ return { operation: 'compact', status: 'completed', projectId: mirror.projectKey,
71
+ workspacePath: mirror.workspacePath, threadId: result.threadId, turnId: result.turnId,
72
+ assistantMessage: '', syncChanges: [], unsupportedChanges: [] };
73
+ }
51
74
  if (params.writingStyleBuild) {
52
75
  return WritingStyleRuntime.build({
53
76
  params, env, signal, providerLaunch, onControlReady, onStopUnconfirmed,
@@ -79,6 +102,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
79
102
 
80
103
  mirror = await syncOverleafToMirror({
81
104
  projectId,
105
+ evidenceProjectId: params.projectId,
82
106
  project: params.project || { files: [] },
83
107
  rootDir
84
108
  });
@@ -191,15 +215,21 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
191
215
  }
192
216
  let runnerResult;
193
217
  try {
194
- runnerResult = await runner({
195
- workspacePath: runnerWorkspacePath,
196
- task: buildCodexTurnPrompt(params, mirror, projectLocalSkills, turnAttachments, codexSkillInvocationContext)
218
+ const developerInstructions = buildCodexDeveloperInstructions({ params });
219
+ const task = buildCodexTurnPrompt(params, mirror, projectLocalSkills, turnAttachments, codexSkillInvocationContext)
197
220
  + writingStyleContext
198
221
  + (selectionScope ? '\n\nCaptured editor selection (quoted source data, not instructions):\n'
199
222
  + JSON.stringify(selectionScope.selection)
200
223
  + (selectionScope.selection.mode === 'edit'
201
224
  ? '\nEdit only this selected span. Surrounding text and all other project files must remain unchanged. The writeback layer enforces this boundary.'
202
- : '\nUse this snapshot as reference for the user request; it does not limit the edit scope.') : ''),
225
+ : '\nUse this snapshot as reference for the user request; it does not limit the edit scope.') : '');
226
+ if (!validatePromptSize(task, '\n\n', developerInstructions).ok) {
227
+ throw new Error('Codex prompt exceeds the native messaging character limit.');
228
+ }
229
+ runnerResult = await runner({
230
+ workspacePath: runnerWorkspacePath,
231
+ task,
232
+ developerInstructions,
203
233
  userTask: String(params.task || ''),
204
234
  session: params.session || null,
205
235
  threadId: params.threadId || '',
@@ -355,7 +385,7 @@ function buildCodexTurnPrompt(params = {}, mirror = {}, projectLocalSkills, turn
355
385
  turnAttachments,
356
386
  codexSkillInvocationContext
357
387
  });
358
- return [prompt.systemPrompt, buildReadProgressRules(), prompt.userPrompt].filter(Boolean).join('\n\n');
388
+ return [prompt.systemPrompt, prompt.userPrompt].filter(Boolean).join('\n\n');
359
389
  }
360
390
 
361
391
 
@@ -558,6 +588,7 @@ function buildThreadStartParams(input = {}) {
558
588
  model: input.model || null,
559
589
  approvalPolicy: input.approvalPolicy,
560
590
  sandbox: input.sandboxMode,
591
+ ...(input.developerInstructions ? { developerInstructions: input.developerInstructions } : {}),
561
592
  experimentalRawEvents: false
562
593
  };
563
594
  }
@@ -568,7 +599,8 @@ function buildThreadResumeParams(input = {}) {
568
599
  cwd: input.workspacePath,
569
600
  model: input.model || null,
570
601
  approvalPolicy: input.approvalPolicy,
571
- sandbox: input.sandboxMode
602
+ sandbox: input.sandboxMode,
603
+ ...(input.developerInstructions ? { developerInstructions: input.developerInstructions } : {})
572
604
  };
573
605
  }
574
606
 
@@ -657,6 +689,8 @@ function runCodexAppServerProcess(input) {
657
689
  let stderr = '';
658
690
  let activeThreadId = String(input.threadId || '');
659
691
  let activeTurnId = '';
692
+ let prependDeveloperInstructions = false;
693
+ let compactRequested = false;
660
694
  let controlPublished = false;
661
695
  const assistantMessages = new Map();
662
696
  const assistantMessageOrder = [];
@@ -747,31 +781,78 @@ function runCodexAppServerProcess(input) {
747
781
  emit: input.emit
748
782
  });
749
783
 
784
+ if (input.operation === 'compact') {
785
+ if (!input.threadId) throw compactError('thread_required', 'Compaction requires an existing conversation.');
786
+ const existing = await request('thread/read', { threadId: input.threadId, includeTurns: false });
787
+ const thread = existing?.thread;
788
+ if (thread?.id !== input.threadId || thread.parentThreadId || !sameWorkspace(thread.cwd, input.workspacePath)) {
789
+ throw compactError('thread_mismatch', 'The conversation does not belong to this project.');
790
+ }
791
+ if (thread.status?.type === 'active') {
792
+ throw compactError('thread_busy', 'Wait for the current conversation turn to finish before compacting.');
793
+ }
794
+ }
795
+
750
796
  if (input.threadId) {
751
797
  try {
752
- const resumeResponse = await request('thread/resume', buildThreadResumeParams(input));
753
- activeThreadId = resumeResponse?.thread?.id || resumeResponse?.threadId || input.threadId;
798
+ const resumeResponse = await requestThreadWithInstructionsFallback('thread/resume', buildThreadResumeParams(input));
799
+ const resumedThreadId = resumeResponse?.thread?.id || resumeResponse?.threadId;
800
+ if (input.operation === 'compact' && resumedThreadId !== input.threadId) {
801
+ throw compactError('thread_mismatch', 'The resumed conversation changed before compaction.');
802
+ }
803
+ activeThreadId = resumedThreadId || input.threadId;
754
804
  } catch (resumeError) {
805
+ if (String(resumeError.code || '').startsWith('codex_compact_')) throw resumeError;
755
806
  const error = new Error(resumeError.message || 'thread/resume failed');
756
807
  error.code = 'thread_resume_failed';
757
808
  throw error;
758
809
  }
759
810
  } else {
760
- const threadResponse = await request('thread/start', buildThreadStartParams(input));
811
+ const threadResponse = await requestThreadWithInstructionsFallback('thread/start', buildThreadStartParams(input));
761
812
  activeThreadId = threadResponse?.thread?.id || threadResponse?.threadId || '';
762
813
  if (!activeThreadId) {
763
814
  throw new Error('Codex app-server did not return a thread id');
764
815
  }
765
816
  }
766
817
 
818
+ if (input.operation === 'compact') {
819
+ compactRequested = true;
820
+ try {
821
+ await request('thread/compact/start', { threadId: activeThreadId });
822
+ } catch (error) {
823
+ if (error.code === -32601) throw compactError('unsupported', 'This Codex version does not support manual compaction.');
824
+ throw error;
825
+ }
826
+ // The empty RPC response acknowledges dispatch only. turn/started
827
+ // supplies the identity; matching turn/completed settles this process.
828
+ return;
829
+ }
767
830
  const turnResponse = await startTurnWithSummaryFallback(activeThreadId);
768
831
  activeTurnId = turnResponse?.turn?.id || '';
769
832
  publishActiveTurnControl();
770
833
  readProgressController.flush();
771
834
  }
772
835
 
836
+ async function requestThreadWithInstructionsFallback(method, params) {
837
+ try {
838
+ return await request(method, params);
839
+ } catch (error) {
840
+ if (!params.developerInstructions || !isUnsupportedDeveloperInstructionsError(error)) throw error;
841
+ const retryParams = { ...params };
842
+ delete retryParams.developerInstructions;
843
+ emitCodexEvent(input.emit, 'codex.session.event', 'developer instructions unsupported; retrying in turn text', {
844
+ method, params: { retriedWithoutDeveloperInstructions: true }
845
+ }, 'completed');
846
+ const result = await request(method, retryParams);
847
+ prependDeveloperInstructions = true;
848
+ return result;
849
+ }
850
+ }
851
+
773
852
  async function startTurnWithSummaryFallback(threadId) {
774
- const params = buildTurnStartParams(input, threadId);
853
+ const params = buildTurnStartParams(prependDeveloperInstructions
854
+ ? { ...input, task: `${input.developerInstructions}\n\n${input.task}` }
855
+ : input, threadId);
775
856
  try {
776
857
  return await request('turn/start', params);
777
858
  } catch (error) {
@@ -859,6 +940,7 @@ function runCodexAppServerProcess(input) {
859
940
  if (subagentObserver.observe(message)) return;
860
941
  if (!eventScope.accepts(message.params)) return;
861
942
  if (message.method === 'turn/started') {
943
+ if (input.operation === 'compact' && !compactRequested) return;
862
944
  const threadId = message.params?.thread?.id || message.params?.threadId;
863
945
  if (!activeTurnId && threadId === activeThreadId) activeTurnId = message.params?.turn?.id || message.params?.turnId || '';
864
946
  publishActiveTurnControl();
@@ -886,7 +968,9 @@ function runCodexAppServerProcess(input) {
886
968
  return;
887
969
  }
888
970
  if (message.method === 'error') {
889
- fail(new Error(message.params?.error?.message || 'Codex turn failed'));
971
+ const error = new Error(extractCodexAppServerErrorMessage(message.params) || 'Codex turn failed');
972
+ if (isContextLimitError(message.params)) error.code = 'codex_context_limit';
973
+ fail(error);
890
974
  }
891
975
  }
892
976
  }
@@ -959,6 +1043,7 @@ function runCodexAppServerProcess(input) {
959
1043
  const control = Object.freeze({
960
1044
  threadId: activeThreadId,
961
1045
  turnId: activeTurnId,
1046
+ ...(input.operation === 'compact' ? { operation: 'compact' } : {}),
962
1047
  steer: ({ input: steerInput, clientUserMessageId } = {}) => request('turn/steer', {
963
1048
  threadId: activeThreadId,
964
1049
  expectedTurnId: activeTurnId,
@@ -969,9 +1054,11 @@ function runCodexAppServerProcess(input) {
969
1054
  });
970
1055
  input.onControlReady?.(control);
971
1056
  if (settled || stopping) return;
972
- emitCodexEvent(input.emit, 'codex.turn.bound', 'Codex turn is ready for follow-up guidance', {
1057
+ emitCodexEvent(input.emit, 'codex.turn.bound', input.operation === 'compact'
1058
+ ? 'Codex context compaction is running' : 'Codex turn is ready for follow-up guidance', {
973
1059
  threadId: activeThreadId,
974
- turnId: activeTurnId
1060
+ turnId: activeTurnId,
1061
+ ...(input.operation === 'compact' ? { operation: 'compact' } : {})
975
1062
  }, 'running');
976
1063
  }
977
1064
 
@@ -998,7 +1085,8 @@ function runCodexAppServerProcess(input) {
998
1085
  assistantMessage: buildFinalAssistantMessage(assistantMessages, assistantMessageOrder, {
999
1086
  turnCompleted: turn.status === undefined || turn.status === 'completed'
1000
1087
  }),
1001
- threadId: activeThreadId
1088
+ threadId: activeThreadId,
1089
+ ...(input.operation === 'compact' ? { turnId: activeTurnId } : {})
1002
1090
  };
1003
1091
  return stopCodexAppServer(child, { processGroup: process.platform !== 'win32' }).then(() => {
1004
1092
  settled = true;
@@ -1032,6 +1120,19 @@ function runCodexAppServerProcess(input) {
1032
1120
  });
1033
1121
  }
1034
1122
 
1123
+ function compactError(reason, message) {
1124
+ return Object.assign(new Error(message), { code: `codex_compact_${reason}` });
1125
+ }
1126
+
1127
+ function sameWorkspace(actual, expected) {
1128
+ if (typeof actual !== 'string' || !actual || !expected) return false;
1129
+ const canonical = value => {
1130
+ try { return fs.realpathSync.native(value); } catch (_error) { return path.resolve(value); }
1131
+ };
1132
+ const actualPath = canonical(actual), expectedPath = canonical(expected);
1133
+ return process.platform === 'win32' ? actualPath.toLowerCase() === expectedPath.toLowerCase() : actualPath === expectedPath;
1134
+ }
1135
+
1035
1136
  function decideCommandApproval({ mode = 'auto', params = {}, skillInvocation = null, env = process.env, workspacePath = '' } = {}) {
1036
1137
  if (isSkillInstallerInvocation(skillInvocation)) {
1037
1138
  return decideSkillInstallerCommandApproval({ params, env, workspacePath });
@@ -1316,6 +1417,7 @@ function emitCodexEvent(emit, type, title, detail = {}, status = 'running') {
1316
1417
  }
1317
1418
 
1318
1419
  function inferNotificationStatus(message) {
1420
+ if (message.method === 'thread/compacted') return 'completed';
1319
1421
  if (message.method === 'turn/completed') {
1320
1422
  const error = getTurnCompletionError(message.params?.turn || {});
1321
1423
  if (error) return error.code === 'codex_cancelled' ? 'cancelled' : 'failed';
@@ -1337,7 +1439,8 @@ function getTurnCompletionError(turn) {
1337
1439
  const invalid = status !== undefined && !['completed', 'failed', 'interrupted'].includes(status);
1338
1440
  const error = new Error(extractCodexAppServerErrorMessage(turn) || (interrupted
1339
1441
  ? 'Codex turn was interrupted.' : invalid ? 'Codex turn ended with an invalid terminal status.' : 'Codex turn failed.'));
1340
- error.code = interrupted ? 'codex_cancelled' : invalid ? 'codex_turn_status_invalid' : 'codex_turn_failed';
1442
+ error.code = interrupted ? 'codex_cancelled' : invalid ? 'codex_turn_status_invalid'
1443
+ : isContextLimitError(turn) ? 'codex_context_limit' : 'codex_turn_failed';
1341
1444
  error.turnStatus = status;
1342
1445
  return error;
1343
1446
  }
@@ -1375,6 +1478,13 @@ function isUnsupportedReasoningSummaryError(error) {
1375
1478
  return /unsupported_parameter/i.test(message) && /reasoning\.summary|summary/i.test(message);
1376
1479
  }
1377
1480
 
1481
+ function isUnsupportedDeveloperInstructionsError(error) {
1482
+ const message = String(error?.message || error || '');
1483
+ return (/developer[_\s]?instructions/i.test(message)
1484
+ && /unknown|unrecognized|unexpected|unsupported|invalid|not supported|not allowed/i.test(message))
1485
+ || (Number(error?.code) === -32602 && /^invalid (?:params|parameters)[.!]?$/i.test(message.trim()));
1486
+ }
1487
+
1378
1488
  const cleanAssistantMessage = value => String(value || '').trim();
1379
1489
 
1380
1490
  module.exports = {
@@ -8,6 +8,7 @@ const {
8
8
  } = require('./nativeHostPlatform');
9
9
 
10
10
  const MAX_FIELD_LENGTH = 8000;
11
+ const MAX_DEBUG_LOG_BYTES = 8 * 1024 * 1024;
11
12
 
12
13
  function getDebugLogDir(options = {}) {
13
14
  return getCodexOverleafHome(options);
@@ -22,12 +23,35 @@ function logDebug(event, detail = {}, options = {}) {
22
23
  try {
23
24
  const logDir = getDebugLogDir(options);
24
25
  const logPath = getDebugLogPath(options);
25
- fs.mkdirSync(logDir, { recursive: true });
26
- fs.appendFileSync(logPath, `${JSON.stringify({
26
+ const record = `${JSON.stringify({
27
27
  time: new Date().toISOString(),
28
28
  event,
29
29
  detail: sanitize(detail)
30
- })}\n`, 'utf8');
30
+ })}\n`;
31
+ const recordBytes = Buffer.byteLength(record, 'utf8');
32
+ if (recordBytes > MAX_DEBUG_LOG_BYTES) return;
33
+ fs.mkdirSync(logDir, { recursive: true });
34
+ try {
35
+ const size = fs.statSync(logPath).size;
36
+ if (size > MAX_DEBUG_LOG_BYTES) {
37
+ // Discard oversized legacy logs instead of retaining a huge .1 copy.
38
+ fs.truncateSync(logPath, 0);
39
+ } else if (size + recordBytes > MAX_DEBUG_LOG_BYTES) {
40
+ fs.renameSync(logPath, `${logPath}.1`);
41
+ }
42
+ } catch (error) {
43
+ if (error.code !== 'ENOENT') throw error;
44
+ }
45
+ fs.appendFileSync(logPath, record, 'utf8');
46
+ // Concurrent Hosts can cross the cap between stat and append/rename.
47
+ // Logging is best-effort: drop an oversized raced file rather than grow it.
48
+ for (const file of [logPath, `${logPath}.1`]) {
49
+ try {
50
+ if (fs.statSync(file).size > MAX_DEBUG_LOG_BYTES) fs.truncateSync(file, 0);
51
+ } catch (error) {
52
+ if (error.code !== 'ENOENT') throw error;
53
+ }
54
+ }
31
55
  } catch {
32
56
  // Debug logging must never break the native messaging host.
33
57
  }
@@ -11,6 +11,8 @@ const { buildNativeRuntimeEnv, summarizeNativeEnvironment } = require('./nativeE
11
11
  let buffered = Buffer.alloc(0);
12
12
  let stdoutUnavailable = false;
13
13
  let nativeDisconnectHandled = false;
14
+ let fatalShutdownStarted = false;
15
+ const FATAL_SHUTDOWN_TIMEOUT_MS = 3000;
14
16
  const runtimeEnv = buildNativeRuntimeEnv(process.env);
15
17
  Object.assign(process.env, runtimeEnv);
16
18
  logDebug('environment.ready', summarizeNativeEnvironment(runtimeEnv));
@@ -93,21 +95,46 @@ process.on('uncaughtException', error => {
93
95
  handleNativeDisconnect('uncaught_native_disconnect', error);
94
96
  return;
95
97
  }
96
- logDebug('process.uncaught_exception', {
97
- message: error.message,
98
- stack: error.stack
99
- });
100
- process.exit(1);
98
+ handleFatalError('process.uncaught_exception', error);
101
99
  });
102
100
 
103
101
  process.on('unhandledRejection', reason => {
104
- logDebug('process.unhandled_rejection', {
105
- message: reason?.message || String(reason),
106
- stack: reason?.stack
107
- });
108
- process.exit(1);
102
+ handleFatalError('process.unhandled_rejection', reason);
109
103
  });
110
104
 
105
+ function handleFatalError(event, error) {
106
+ if (fatalShutdownStarted) {
107
+ process.exit(1);
108
+ return;
109
+ }
110
+ fatalShutdownStarted = true;
111
+ nativeDisconnectHandled = true;
112
+ stdoutUnavailable = true;
113
+ process.exitCode = 1;
114
+ logDebug(event, { message: error?.message || String(error), stack: error?.stack });
115
+ const exitTimer = setTimeout(() => {
116
+ const failures = quarantineActiveOperations();
117
+ logDebug('process.fatal_shutdown_deadline', { persistenceFailures: failures });
118
+ process.exit(1);
119
+ }, FATAL_SHUTDOWN_TIMEOUT_MS);
120
+ try {
121
+ // Persist protection before aborting; stop confirmation owns its removal.
122
+ quarantineActiveOperations();
123
+ Promise.resolve(abortAllActiveOperations('Native Host stopped after a fatal error.')).then(() => {
124
+ clearTimeout(exitTimer);
125
+ process.exit(1);
126
+ }, failure => {
127
+ logDebug('process.fatal_shutdown_failed', { message: failure?.message });
128
+ clearTimeout(exitTimer);
129
+ process.exit(1);
130
+ });
131
+ } catch (failure) {
132
+ logDebug('process.fatal_shutdown_failed', { message: failure?.message });
133
+ clearTimeout(exitTimer);
134
+ process.exit(1);
135
+ }
136
+ }
137
+
111
138
  function writeResponse(response) {
112
139
  if (stdoutUnavailable || process.stdout.destroyed || process.stdout.writableEnded) {
113
140
  logDebug('stdout.write_skipped', { ok: response?.ok, code: response?.error?.code });
@@ -152,6 +179,7 @@ function handleNativeDisconnect(source, error) {
152
179
  // old owned process is still inside its interrupt/termination budget.
153
180
  quarantineActiveOperations();
154
181
  const exitTimer = setTimeout(() => {
182
+ if (fatalShutdownStarted) return;
155
183
  const failures = quarantineActiveOperations();
156
184
  logDebug('native_transport.shutdown_deadline', { persistenceFailures: failures });
157
185
  process.exit(failures.length ? 1 : 0);
@@ -160,7 +188,10 @@ function handleNativeDisconnect(source, error) {
160
188
  Promise.all([
161
189
  abortAllActiveOperations('Native messaging transport disconnected.'),
162
190
  new Promise(resolve => setTimeout(resolve, 1500))
163
- ]).then(() => { clearTimeout(exitTimer); process.exit(0); });
191
+ ]).then(() => {
192
+ clearTimeout(exitTimer);
193
+ if (!fatalShutdownStarted) process.exit(0);
194
+ });
164
195
  }
165
196
 
166
197
  function summarizeRequest(message) {
@@ -13,7 +13,7 @@ const MAX_SKILL_CONTENT_CHARS = MAX_SKILL_CONTENT_BYTES;
13
13
  const MAX_SKILL_PREVIEW_CHARS = 240;
14
14
  const PROJECT_SKILL_SCOPE = 'project';
15
15
  const CODEX_OVERLEAF_SKILL_SCOPE = 'codex-overleaf';
16
- const OFFICIAL_CODEX_OVERLEAF_SKILL_IDS = ['annotated-rewrite', 'parallel-subagents'];
16
+ const OFFICIAL_CODEX_OVERLEAF_SKILL_IDS = ['annotated-rewrite', 'parallel-subagents', 'reviewer-response'];
17
17
 
18
18
  function listProjectSkills({ projectId, rootDir } = {}) {
19
19
  const skillsDir = getProjectSkillsDir(projectId, { rootDir });
@@ -5,6 +5,7 @@ const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const { getHomeDir } = require('./nativeHostPlatform');
7
7
  const { assertProjectPathsForPlatform } = require('./projectPathPlatform');
8
+ const { assessDocumentProvenance } = require('../../extension/src/shared/staleGuard');
8
9
 
9
10
  const BASELINE_FILE = 'baseline.json';
10
11
  const MAX_BINARY_FILE_BYTES = 10 * 1024 * 1024;
@@ -34,11 +35,20 @@ function getDefaultMirrorRoot(options = {}) {
34
35
  return path.join(getHomeDir(options), '.codex-overleaf', 'projects');
35
36
  }
36
37
 
37
- async function syncOverleafToMirror({ projectId, project, rootDir }) {
38
+ async function syncOverleafToMirror({ projectId, project, rootDir, evidenceProjectId }) {
38
39
  const mirror = getProjectMirror(projectId, { rootDir });
39
40
  const normalized = normalizeProjectFilesDetailed(project?.files || []);
40
41
  const files = normalized.files;
41
42
  const previous = readBaseline(mirror.baselinePath);
43
+ // Page already admits evidence-free snapshot text against its own saved
44
+ // references, which may no longer be present after editor/writeback merging.
45
+ for (const file of project?.files || []) {
46
+ if (file.kind === 'binary' || typeof file.content !== 'string') continue;
47
+ if (file.documentIdentity !== undefined) {
48
+ assertDocumentProvenance(file.path, file.content, undefined, file.documentIdentity,
49
+ project?.id !== undefined && project.id !== evidenceProjectId ? undefined : evidenceProjectId);
50
+ }
51
+ }
42
52
  const fullProjectSnapshot = project?.capabilities?.fullProjectSnapshot !== false;
43
53
  const skippedFiles = resolveUnreadSnapshotFiles(project, normalized, previous, fullProjectSnapshot);
44
54
  // Runtime platform is authoritative; request fields cannot opt out of this check.
@@ -808,11 +818,20 @@ function buildOtStatusFields(baseline = {}, trusted) {
808
818
  };
809
819
  }
810
820
 
811
- async function applyFileOverlays({ projectId, overlays, rootDir }) {
821
+ async function applyFileOverlays({ projectId, overlays, rootDir, evidenceProjectId }) {
812
822
  const mirror = getProjectMirror(projectId, { rootDir });
813
823
  const baseline = readBaseline(mirror.baselinePath);
814
824
  const filesByPath = new Map((baseline.files || []).map(f => [f.path, f]));
815
825
  const unreadPaths = new Set((baseline.unreadFiles || []).map(file => file.path));
826
+ const references = baselineTextReferences(baseline);
827
+ // Validate the entire request against the unchanged baseline before any file
828
+ // or baseline write. An earlier overlay cannot hide a later misattribution.
829
+ for (const overlay of overlays || []) {
830
+ if (!overlay?.path || typeof overlay.content !== 'string') continue;
831
+ const normalizedPath = normalizeRelativePath(overlay.path);
832
+ if (!unreadPaths.has(normalizedPath))
833
+ assertDocumentProvenance(normalizedPath, overlay.content, references, overlay.documentIdentity, evidenceProjectId);
834
+ }
816
835
 
817
836
  for (const overlay of overlays || []) {
818
837
  if (!overlay?.path || typeof overlay.content !== 'string') {
@@ -828,7 +847,7 @@ async function applyFileOverlays({ projectId, overlays, rootDir }) {
828
847
  filesByPath.set(normalizedPath, {
829
848
  path: normalizedPath,
830
849
  kind: 'text',
831
- hash: overlay.hash || hashText(overlay.content),
850
+ hash: hashText(overlay.content),
832
851
  content: overlay.content
833
852
  });
834
853
  }
@@ -840,6 +859,24 @@ async function applyFileOverlays({ projectId, overlays, rootDir }) {
840
859
  });
841
860
  }
842
861
 
862
+ function baselineTextReferences(baseline) {
863
+ return new Map((baseline.files || []).filter(file => file.kind === 'text' && typeof file.content === 'string')
864
+ .map(file => [file.path, file.content]));
865
+ }
866
+
867
+ function assertDocumentProvenance(path, text, references, documentIdentity, projectId) {
868
+ const result = documentIdentity !== undefined
869
+ && (!documentIdentity || documentIdentity.contentSha256 !== hashText(text))
870
+ ? { ok: false, code: 'document_identity_mismatch' }
871
+ : assessDocumentProvenance({ path, text, references, documentIdentity, projectId });
872
+ if (result.ok) return;
873
+ const error = new Error('Codex could not confirm that the editor content belongs to the target file, so it was not used.');
874
+ error.code = result.code;
875
+ error.path = path;
876
+ error.matchedPath = result.matchedPath;
877
+ throw error;
878
+ }
879
+
843
880
  // v1.8.0: after a VERIFIED writeback, the workspace already holds the exact
844
881
  // content that landed in Overleaf only while its hashes match the page's
845
882
  // verified post-images. Check every target before updating any baseline.
@@ -932,6 +969,13 @@ async function patchMirrorFiles({ projectId, files, rootDir, source = 'ot' }) {
932
969
  let lastOtPatchAt = baseline.lastOtPatchAt || '';
933
970
  let lastOtErrorCode = '';
934
971
  let patchBatchAt = '';
972
+ const references = baselineTextReferences(baseline);
973
+ for (const patch of Array.isArray(files) ? files : []) {
974
+ const normalized = normalizePatchPath(patch, mirror.workspacePath);
975
+ if (normalized.ok && baselineByPath.get(normalized.path)?.kind === 'text' && typeof patch?.nextContent === 'string') {
976
+ assertDocumentProvenance(normalized.path, patch.nextContent, references);
977
+ }
978
+ }
935
979
 
936
980
  for (const patch of Array.isArray(files) ? files : []) {
937
981
  const normalized = normalizePatchPath(patch, mirror.workspacePath);
@@ -40,6 +40,11 @@ function buildNativeRuntimeEnv(baseEnv = process.env, options = {}) {
40
40
  for (const tool of TOOL_NAMES) {
41
41
  env[getToolEnvName(tool)] = resolveExecutable(tool, env.PATH, { ...runtimeOptions, env }) || '';
42
42
  }
43
+ const startupContext = getCodexRuntimeRefreshContext({
44
+ ...env,
45
+ CODEX_OVERLEAF_CODEX_PATH: explicitCodexPath || env.CODEX_OVERLEAF_CODEX_PATH,
46
+ CODEX_OVERLEAF_CODEX_RUNTIME_JSON: ''
47
+ }, runtimeOptions);
43
48
  const codexRuntime = buildCodexRuntimeIdentity({
44
49
  selectedPath: explicitCodexPath || env.CODEX_OVERLEAF_CODEX_PATH,
45
50
  selectionPolicy: explicitCodexPath ? 'explicit-path' : 'newest-version',
@@ -53,12 +58,20 @@ function buildNativeRuntimeEnv(baseEnv = process.env, options = {}) {
53
58
  env.CODEX_OVERLEAF_CODEX_VERSION = codexRuntime.selected?.version || '';
54
59
  env.CODEX_OVERLEAF_CODEX_SOURCE = codexRuntime.selected?.source || '';
55
60
 
61
+ const { key, pathValue, signatures } = getCodexRuntimeRefreshContext(env, runtimeOptions);
62
+ // Selection can reorder candidates, but discovery must not cache changed files.
63
+ if (pathValue === startupContext.pathValue
64
+ && signatures.length === startupContext.signatures.length
65
+ && signatures.every(signature => startupContext.signatures.some(before => JSON.stringify(before) === JSON.stringify(signature)))) {
66
+ cacheCodexRuntimeIdentity(key, codexRuntime);
67
+ }
68
+
56
69
  return env;
57
70
  }
58
71
 
59
72
  const codexRuntimeRefreshCache = new Map();
60
73
 
61
- function refreshCodexRuntimeEnv(baseEnv = process.env, options = {}) {
74
+ function getCodexRuntimeRefreshContext(baseEnv, options) {
62
75
  const runtimeOptions = { ...options, platform: getNativeRuntimePlatform({ ...options, env: baseEnv }) };
63
76
  const previous = getCodexRuntimeIdentityFromEnv(baseEnv);
64
77
  const selectedPath = previous.selected?.path || String(baseEnv.CODEX_OVERLEAF_CODEX_PATH || '');
@@ -78,6 +91,18 @@ function refreshCodexRuntimeEnv(baseEnv = process.env, options = {}) {
78
91
  } catch (_) { return [candidate, 'missing']; }
79
92
  });
80
93
  const key = JSON.stringify([runtimeOptions.platform, pinned ? selectedPath : '', pathValue, signatures]);
94
+ return { runtimeOptions, previous, selectedPath, pinned, pathValue, signatures, key };
95
+ }
96
+
97
+ function cacheCodexRuntimeIdentity(key, identity) {
98
+ codexRuntimeRefreshCache.set(key, { identity, createdAt: Date.now() });
99
+ while (codexRuntimeRefreshCache.size > 4) {
100
+ codexRuntimeRefreshCache.delete(codexRuntimeRefreshCache.keys().next().value);
101
+ }
102
+ }
103
+
104
+ function refreshCodexRuntimeEnv(baseEnv = process.env, options = {}) {
105
+ const { runtimeOptions, previous, selectedPath, pinned, pathValue, key } = getCodexRuntimeRefreshContext(baseEnv, options);
81
106
  const cached = codexRuntimeRefreshCache.get(key);
82
107
  let identity = cached?.identity;
83
108
  if (options.force || !cached || Date.now() - cached.createdAt > 30000) {
@@ -89,10 +114,7 @@ function refreshCodexRuntimeEnv(baseEnv = process.env, options = {}) {
89
114
  env: { ...baseEnv, PATH: pathValue },
90
115
  platform: runtimeOptions.platform
91
116
  });
92
- codexRuntimeRefreshCache.set(key, { identity, createdAt: Date.now() });
93
- while (codexRuntimeRefreshCache.size > 4) {
94
- codexRuntimeRefreshCache.delete(codexRuntimeRefreshCache.keys().next().value);
95
- }
117
+ cacheCodexRuntimeIdentity(key, identity);
96
118
  if (identity.selected?.path !== previous.selected?.path
97
119
  || identity.selected?.version !== previous.selected?.version) {
98
120
  logDebug('codex.runtime.refreshed', {
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // Provider and CLI representations of the same recoverable condition.
4
+ // Authentication, transport and output-budget errors keep their own handling.
5
+ function isContextLimitError(value, depth = 0) {
6
+ if (depth > 3 || value == null) return false;
7
+ if (typeof value === 'object') {
8
+ return [value.code, value.codexErrorInfo, value.codex_error_info, value.error, value.message]
9
+ .some(part => isContextLimitError(part, depth + 1));
10
+ }
11
+ if (typeof value !== 'string') return false;
12
+ const text = value.trim();
13
+ if (['contextlengthexceeded', 'contextwindowexceeded', 'modelcontextwindowexceeded', 'codexcontextlimit']
14
+ .includes(text.replace(/[_-]/g, '').toLowerCase())) return true;
15
+ return /Codex ran out of room in the model['’]s context window/i.test(text)
16
+ || /maximum context length is\s+\d+\s+tokens[\s\S]*?requested\s+\d+\s+tokens/i.test(text)
17
+ || /(?:context (?:length|window|limit) (?:has been |is )?exceeded|exceeds? (?:the |this model['’]s )?(?:maximum )?context (?:length|window|limit))/i.test(text);
18
+ }
19
+
20
+ module.exports = { isContextLimitError };
@@ -237,6 +237,7 @@ function resolveProviderModels(params = {}, env = process.env, resolveBuiltInMod
237
237
  providerId: profile.id,
238
238
  providerRevision: profile.revision,
239
239
  providerName: profile.name,
240
+ defaultModelId: profile.defaultModelId,
240
241
  models: profile.models.map(model => {
241
242
  const reasoning = getReasoningControl(profile, model);
242
243
  return {