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
@@ -11,31 +11,46 @@
11
11
  const COMPILABLE_EXTENSIONS = new Set(['.tex', '.bib', '.sty', '.cls', '.bst', '.bbx', '.cbx']);
12
12
  function parseCompileResponse(response) {
13
13
  if (!response || typeof response !== 'object') return { ok: false, reason: 'Invalid compile response: not an object' };
14
- if (!Array.isArray(response.outputFiles)) return { ok: false, reason: 'Invalid compile response: outputFiles is not an array' };
15
- const logFile = response.outputFiles.find(function (file) {
14
+ if (!Array.isArray(response.outputFiles)) return { ok: false, status: response.status, reason: 'Invalid compile response: outputFiles is not an array' };
15
+ const logFile = response.outputFiles.find(file => /(^|\/)output\.log$/.test(file?.path || '')) || response.outputFiles.find(function (file) {
16
16
  if (!file) return false;
17
17
  if (file.type === 'log') return true;
18
18
  if (typeof file.path === 'string' && file.path.endsWith('.log')) return true;
19
19
  return false;
20
20
  });
21
- if (!logFile) return { ok: false, reason: 'No log file found in compile output' };
22
- var logUrl = logFile.url || null;
23
- if (!logUrl) return { ok: false, reason: 'Log file has no URL' };
21
+ if (!logFile) return { ok: false, status: response.status, reason: 'No log file found in compile output' };
22
+ var logUrl = typeof logFile.url === 'string' ? logFile.url : null;
23
+ if (!logUrl) return { ok: false, status: response.status, reason: 'Log file has no URL' };
24
24
  return {
25
25
  ok: true,
26
26
  status: response.status,
27
+ fromCache: response.fromCache === true,
27
28
  logUrl: logUrl,
28
- logPath: logFile.path || null
29
+ logPath: logFile.path || null,
30
+ build: logFile.build || null,
31
+ pdfDownloadDomain: normalizePdfDownloadDomain(response.pdfDownloadDomain),
32
+ clsiServerId: typeof response.clsiServerId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(response.clsiServerId)
33
+ ? response.clsiServerId : null
29
34
  };
30
35
  }
31
36
 
37
+ function normalizePdfDownloadDomain(value) {
38
+ if (typeof value !== 'string' || !/^https:\/\/[^\s\\]+$/i.test(value)) return null;
39
+ try {
40
+ const url = new URL(value);
41
+ return url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash ? value : null;
42
+ } catch (_error) {
43
+ return null;
44
+ }
45
+ }
46
+
32
47
  function extractErrorBlocks(lines) {
33
48
  var blocks = [];
34
49
  var i = 0;
35
50
 
36
51
  while (i < lines.length && blocks.length < 20) {
37
52
  var line = lines[i];
38
- if (line.startsWith('!') || /^l\.\d+/.test(line)) {
53
+ if (isLogErrorLine(line) || /^l\.\d+/.test(line)) {
39
54
  var block = [];
40
55
  // Collect surrounding context: go back up to 2 lines for pre-context
41
56
  var start = Math.max(0, i - 2);
@@ -106,6 +121,12 @@
106
121
  return COMPILABLE_EXTENSIONS.has(ext);
107
122
  }
108
123
 
124
+ function isLogErrorLine(line) {
125
+ // TeX's -file-line-error output (used by Overleaf) has no leading '!'.
126
+ return line.startsWith('!') || /^Runaway argument/.test(line)
127
+ || (/^(?:\.{0,2}\/|[A-Za-z]:[\\/])?.+?:\d+:\s+\S/.test(line) && !/\bWarning\b/i.test(line));
128
+ }
129
+
109
130
  function parseLogErrors(logContent) {
110
131
  var result = { errors: [], warnings: [] };
111
132
 
@@ -115,20 +136,23 @@
115
136
 
116
137
  var lines = logContent.split('\n');
117
138
 
118
- // Extract errors: lines starting with '!' plus up to 3 following context lines
139
+ // Retain the bounded error list; consumers limit displayed snippets separately.
119
140
  for (var i = 0; i < lines.length && result.errors.length < 50; i++) {
120
- if (lines[i].startsWith('!')) {
141
+ if (isLogErrorLine(lines[i])) {
121
142
  var errorLines = [lines[i]];
122
143
  for (var j = 1; j <= 3 && (i + j) < lines.length; j++) {
144
+ if (!lines[i + j].trim() || isLogErrorLine(lines[i + j])) break;
123
145
  errorLines.push(lines[i + j]);
124
146
  }
125
147
  result.errors.push(errorLines.join('\n'));
126
148
  }
127
149
  }
128
150
 
129
- // Extract warnings: lines matching /Warning/i that don't start with whitespace
151
+ // Count only the warning forms Overleaf's own log parser reports, so
152
+ // informational lines such as "Package: infwarerr … info/warning/error
153
+ // messages" are not shown as warnings.
130
154
  for (var k = 0; k < lines.length && result.warnings.length < 50; k++) {
131
- if (/Warning/i.test(lines[k]) && !/^\s/.test(lines[k])) {
155
+ if (/^LaTeX(?:3| Font)? Warning: /.test(lines[k]) || /^(?:Package|Class|Module) \b.+\b Warning:/.test(lines[k])) {
132
156
  result.warnings.push(lines[k]);
133
157
  }
134
158
  }
@@ -137,18 +161,35 @@
137
161
  }
138
162
 
139
163
  function buildPostWriteCompileSummary({ result = null, logResult = null, error = null } = {}) {
140
- const logAvailable = logResult?.ok === true;
164
+ const captured = result?.ok === true && result.resultCaptured === true
165
+ && Number.isSafeInteger(result.compile?.captureId) && result.compile.captureId > 0;
166
+ const logAvailable = captured && logResult?.ok === true && logResult.fresh === true
167
+ && result.compile?.captureId === logResult.captureId;
141
168
  const errors = logAvailable && Array.isArray(logResult.errors)
142
- ? logResult.errors.slice(0, 5).map(formatCompileDiagnosticForSummary)
169
+ ? logResult.errors.map(formatCompileDiagnosticForSummary)
143
170
  : [];
144
171
  const warnings = logAvailable && Array.isArray(logResult.warnings)
145
172
  ? logResult.warnings.slice(0, 5).map(formatCompileDiagnosticForSummary)
146
173
  : [];
147
- const failed = Boolean(error) || !result?.ok;
174
+ const compilerFailed = captured && logResult?.fresh !== false
175
+ && ['failed', 'failure', 'error', 'timedout', 'timeout'].includes(result.compile?.status);
176
+ if (compilerFailed && !errors.length) errors.push('Post-write compile failed.');
177
+ const failed = compilerFailed || errors.length > 0;
178
+ const success = captured && result.compile?.ok === true && result.compile.status === 'success'
179
+ && result.compile.fromCache !== true && logAvailable;
180
+ const status = failed ? 'failed' : success ? 'success' : result?.ok && !captured ? 'triggered' : 'unconfirmed';
181
+ let reason = error ? error?.message || 'Post-write compile failed.'
182
+ : !result?.ok ? result?.reason || 'Post-write compile did not succeed.' : result.reason || '';
183
+ if (status === 'unconfirmed' || status === 'triggered') {
184
+ // Technical evidence belongs in the existing activity output, not the outcome label.
185
+ reason ||= logResult?.reason || (!captured ? 'compile_capture_missing'
186
+ : result.compile?.reason || (result.compile.fromCache ? 'compile_response_from_cache'
187
+ : logResult?.captureId !== result.compile.captureId ? 'compile_log_capture_mismatch'
188
+ : logResult?.fresh === false ? 'compile_log_stale' : 'compile_result_unconfirmed'));
189
+ }
148
190
  return {
149
- status: failed ? 'failed' : result.compile?.status || 'triggered',
150
- reason: error ? error?.message || 'Post-write compile failed.'
151
- : !result?.ok ? result?.reason || 'Post-write compile did not succeed.' : result.reason || '',
191
+ status,
192
+ reason,
152
193
  errors,
153
194
  warnings,
154
195
  logAvailable
@@ -5,6 +5,13 @@
5
5
  'use strict';
6
6
  const EPOCH_KEY = 'composer-attachment-draft:clear-epoch';
7
7
  const normalize = value => SessionState.normalizeComposerAttachmentsForPersistence(value);
8
+ function normalizeOmitted(value) {
9
+ const names = (Array.isArray(value?.names) ? value.names : [])
10
+ .filter(name => typeof name === 'string')
11
+ .map(name => name.replace(/\0/g, '').trim().slice(0, 180))
12
+ .filter(Boolean).slice(0, 8);
13
+ return { count: names.length, names };
14
+ }
8
15
  const scopeKey = scope => JSON.stringify([scope?.accountScopeId || '', scope?.projectId || '']);
9
16
  function checkedScope(scope) {
10
17
  const accountScopeId = String(scope?.accountScopeId || '').trim();
@@ -42,20 +49,28 @@
42
49
  throw failure('account_scope_unavailable');
43
50
  }
44
51
  const revision = Number(previous?.revision) || 0;
45
- result = { epoch, clearedAt, revision, attachments: normalize(previous?.attachments) };
52
+ result = { epoch, clearedAt, revision, attachments: normalize(previous?.attachments), omittedAttachments: normalizeOmitted(previous?.omittedAttachments) };
46
53
  if (!value) return;
47
54
  const attachments = normalize(value.attachments);
55
+ const omittedAttachments = normalizeOmitted(value.omittedAttachments);
56
+ const hasDraft = attachments.length || omittedAttachments.count;
48
57
  if (value.epoch !== epoch) {
49
58
  // A clear performed by this page also invalidates its old base.
50
59
  // Its empty draft needs no write; an old nonempty draft cannot return.
51
- if (!attachments.length) return;
60
+ if (!hasDraft) return;
52
61
  throw failure('composer_draft_cleared');
53
62
  }
54
- if (value.revision !== revision) throw failure('composer_draft_conflict');
55
- if (attachments.length && value.unbased && clearedAt && value.changedAt <= clearedAt) {
63
+ if (value.revision !== revision) {
64
+ // Writing what another tab already stored changes nothing (for example
65
+ // both tabs clearing an empty draft on send); only a real edit conflicts.
66
+ if (JSON.stringify(attachments) === JSON.stringify(result.attachments)
67
+ && JSON.stringify(omittedAttachments) === JSON.stringify(result.omittedAttachments)) return;
68
+ throw failure('composer_draft_conflict');
69
+ }
70
+ if (hasDraft && value.unbased && clearedAt && value.changedAt <= clearedAt) {
56
71
  throw failure('composer_draft_cleared');
57
72
  }
58
- result = { epoch, clearedAt, revision: revision + 1, attachments };
73
+ result = { epoch, clearedAt, revision: revision + 1, attachments, omittedAttachments };
59
74
  store.put({ id, type: 'composer-attachment-draft', ...scope, ...result });
60
75
  } catch (caught) { error = caught; tx.abort(); }
61
76
  };
@@ -115,7 +130,11 @@
115
130
  syncScope();
116
131
  if (hydrated) return;
117
132
  // Loading is allowed to finish after a local add/remove. Local edits win.
118
- if (!changedVersion && !options.getAttachments().length) options.restore(normalize(attachments));
133
+ if (!changedVersion && !options.getAttachments().length) {
134
+ options.restore(normalize(attachments));
135
+ const omitted = normalizeOmitted(base?.omittedAttachments);
136
+ if (omitted.count) options.onRestoreOmittedAttachments?.(omitted);
137
+ }
119
138
  hydrated = true; publish();
120
139
  }
121
140
  function changed() {
@@ -133,10 +152,11 @@
133
152
  if (captured !== generation || scopeKey(options.getScope()) !== key || changedVersion === savedVersion) return;
134
153
  const version = changedVersion;
135
154
  const attachments = normalize(options.getAttachments());
155
+ const omittedAttachments = normalizeOmitted({ names: options.getOmittedAttachmentNames?.() });
136
156
  const unbased = dirtyUnbased, changedAt = dirtySince;
137
157
  if (!base) await load(scope);
138
158
  if (captured !== generation || scopeKey(options.getScope()) !== key) return;
139
- const written = await options.write(checkedScope(scope), { ...base, attachments, unbased, changedAt });
159
+ const written = await options.write(checkedScope(scope), { ...base, attachments, omittedAttachments, unbased, changedAt });
140
160
  if (captured !== generation) return;
141
161
  base = written; savedVersion = version;
142
162
  if (changedVersion === version) { dirtySince = 0; dirtyUnbased = false; }
@@ -0,0 +1,125 @@
1
+ (function initContextUsage(root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodexOverleafModuleRegistry.define('ContextUsage', [], factory);
4
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function contextUsageFactory() {
5
+ 'use strict';
6
+
7
+ const count = value => Number.isSafeInteger(value) && value >= 0 ? value : null;
8
+ const identity = value => typeof value === 'string' && value.trim() && value.length <= 512
9
+ && !/[\0\r\n]/.test(value) ? value.trim() : '';
10
+ const timestamp = value => typeof value === 'string' && Number.isFinite(Date.parse(value))
11
+ ? new Date(value).toISOString() : '';
12
+
13
+ // Store a bounded fact from one model response, never the cumulative billed
14
+ // token total. The last reported response is an estimate of context occupancy;
15
+ // tool output added since then is not included until the next server update.
16
+ function normalize(value) {
17
+ if (!value || value.v !== 1) return null;
18
+ const threadId = identity(value.threadId), turnId = identity(value.turnId);
19
+ const runId = identity(value.runId), providerId = identity(value.providerId), model = identity(value.model);
20
+ const usedTokens = count(value.usedTokens), providerRevision = count(value.providerRevision);
21
+ const observedAt = timestamp(value.observedAt);
22
+ if (!threadId || !turnId || !runId || !providerId || !model || usedTokens === null
23
+ || providerRevision === null || !observedAt) return null;
24
+ const capacity = count(value.contextWindow);
25
+ const eventSeq = count(value.eventSeq);
26
+ const inputTokens = count(value.inputTokens), outputTokens = count(value.outputTokens);
27
+ const cachedInput = count(value.cachedInputTokens);
28
+ // Cache reads are a subset of input, never an additional category. Older
29
+ // snapshots and providers without these counters retain unknown values.
30
+ const cachedInputTokens = inputTokens !== null && cachedInput !== null && cachedInput <= inputTokens
31
+ ? cachedInput : null;
32
+ return { v: 1, threadId, turnId, runId, providerId, providerRevision, model,
33
+ usedTokens, contextWindow: capacity > 0 ? capacity : null,
34
+ inputTokens, outputTokens, cachedInputTokens,
35
+ eventSeq: eventSeq > 0 ? eventSeq : null, observedAt };
36
+ }
37
+
38
+ function matches(value, scope = {}) {
39
+ const snapshot = normalize(value);
40
+ return Boolean(snapshot && snapshot.threadId === identity(scope.threadId || scope.codexThreadId)
41
+ && snapshot.providerId === identity(scope.providerId)
42
+ && snapshot.providerRevision === count(scope.providerRevision)
43
+ && snapshot.model === identity(scope.model));
44
+ }
45
+
46
+ // The caller supplies the captured run owner, after its Native thread/turn
47
+ // binding is known. Reject worker, previous-turn and unbound notifications.
48
+ function fromEvent(event, owner = {}) {
49
+ if (!event || (event.type && event.type !== 'codex.session.event') || event.subagent === true) return null;
50
+ const notification = event.detail || event;
51
+ if (notification.method !== 'thread/tokenUsage/updated') return null;
52
+ const params = notification.params || {};
53
+ const threadId = identity(params.threadId), turnId = identity(params.turnId);
54
+ if (!threadId || !turnId || threadId !== identity(owner.threadId || owner.codexThreadId)
55
+ || turnId !== identity(owner.turnId || owner.codexTurnId) || owner.forkSnapshot === true) return null;
56
+ const last = params.tokenUsage?.last;
57
+ const usedTokens = count(last?.totalTokens);
58
+ if (usedTokens === null) return null;
59
+ return normalize({ v: 1, threadId, turnId, runId: owner.runId || owner.id,
60
+ providerId: owner.providerId, providerRevision: owner.providerRevision, model: owner.model,
61
+ usedTokens, contextWindow: params.tokenUsage?.modelContextWindow,
62
+ inputTokens: last?.inputTokens, outputTokens: last?.outputTokens, cachedInputTokens: last?.cachedInputTokens,
63
+ eventSeq: event.nativeEventSeq ?? event.eventSeq ?? event.seq ?? owner.eventSeq,
64
+ observedAt: timestamp(event.timestamp || event.at) || timestamp(owner.observedAt) || new Date().toISOString() });
65
+ }
66
+
67
+ function isNewer(next, previous) {
68
+ next = normalize(next); previous = normalize(previous);
69
+ if (!next) return false;
70
+ if (!previous) return true;
71
+ if (next.runId === previous.runId && next.threadId === previous.threadId && next.turnId === previous.turnId
72
+ && next.eventSeq !== null && previous.eventSeq !== null)
73
+ return next.eventSeq > previous.eventSeq;
74
+ return Date.parse(next.observedAt) > Date.parse(previous.observedAt);
75
+ }
76
+
77
+ // `scope.compactedAt` (epoch ms) carries an automatic compaction completed
78
+ // inside a normal turn; manual compact runs are read from the session.
79
+ // `pending` means the old sample is invalid; `compacted` additionally requires
80
+ // successful completion of the most recent compaction that invalidated it.
81
+ function status(session, scope = {}) {
82
+ if (!session || session.forkSnapshot === true) return { usage: null, pending: false, compacted: false };
83
+ const inThread = value => value && value.threadId === identity(scope.threadId || scope.codexThreadId)
84
+ && value.providerId === identity(scope.providerId)
85
+ && value.providerRevision === count(scope.providerRevision);
86
+ // A dispatched compaction can change this thread even when its completion
87
+ // is lost on reload. Do not re-display a pre-compaction sample as current.
88
+ const runs = Array.isArray(session.runs) ? session.runs : [];
89
+ let invalidatedAfter = Number.isFinite(scope.compactedAt) && scope.compactedAt > 0 ? scope.compactedAt : 0;
90
+ let completedAfter = invalidatedAfter;
91
+ for (const run of runs) {
92
+ if (run?.operation !== 'compact' || run.forkSnapshot
93
+ || run.codexThreadId !== identity(scope.threadId || scope.codexThreadId)) continue;
94
+ const startedAt = Date.parse(run.startedAt || '') || 0;
95
+ invalidatedAfter = Math.max(invalidatedAfter, startedAt);
96
+ if (run.status === 'completed') completedAfter = Math.max(completedAfter, startedAt);
97
+ }
98
+ let selected = normalize(session.contextUsage);
99
+ if (!inThread(selected)) selected = null;
100
+ for (const run of runs) {
101
+ if (run?.forkSnapshot === true) continue;
102
+ const candidate = normalize(run.contextUsage);
103
+ if (!inThread(candidate) || candidate.runId !== run.id) continue;
104
+ if (isNewer(candidate, selected)) selected = candidate;
105
+ }
106
+ const beforeCompaction = !selected || Date.parse(selected.observedAt) < invalidatedAfter;
107
+ // A later turn can grow this thread using another model. Selecting an older
108
+ // model again must not resurrect its obsolete, smaller context estimate.
109
+ const usage = selected?.model === identity(scope.model) && !beforeCompaction ? selected : null;
110
+ const pending = !usage && invalidatedAfter > 0 && beforeCompaction;
111
+ return { usage, pending, compacted: pending && completedAfter >= invalidatedAfter };
112
+ }
113
+
114
+ function select(session, scope = {}) {
115
+ return status(session, scope).usage;
116
+ }
117
+
118
+ // Presentation level of a reported share of the window, in percent.
119
+ function level(percent) {
120
+ if (typeof percent !== 'number' || !Number.isFinite(percent)) return '';
121
+ return percent >= 90 ? 'near-limit' : percent >= 70 ? 'high' : 'normal';
122
+ }
123
+
124
+ return { normalize, fromEvent, matches, isNewer, select, status, level };
125
+ });
@@ -106,6 +106,11 @@
106
106
  },
107
107
 
108
108
  // 9.2 Preflight
109
+ sensitive_content_blocked: {
110
+ stage: 'preflight', severity: 'blocked', defaultRetryable: false,
111
+ fallbackUserMessage: 'Sensitive content was found before sending, so Codex did not receive this request.',
112
+ fallbackNextAction: 'Remove the sensitive content, narrow @context, or enable explicit sensitive confirmation in Project Settings.'
113
+ },
109
114
  governance_blocked: {
110
115
  stage: 'preflight', severity: 'blocked', defaultRetryable: false,
111
116
  fallbackUserMessage: 'Project file rules blocked this write. Codex did not write the affected path.',
@@ -131,6 +136,11 @@
131
136
  fallbackUserMessage: 'The file changed while Codex was working.',
132
137
  fallbackNextAction: 'Review the current file, then rerun the task.'
133
138
  },
139
+ document_identity_mismatch: {
140
+ stage: 'preflight', severity: 'blocked', defaultRetryable: true,
141
+ fallbackUserMessage: 'Codex could not confirm that the editor content belongs to the target file, so it was not used.',
142
+ fallbackNextAction: 'Wait for the target file to open and settle, then retry. Reload Overleaf if this continues.'
143
+ },
134
144
  patch_anchor_not_found: {
135
145
  stage: 'preflight', severity: 'blocked', defaultRetryable: true,
136
146
  fallbackUserMessage: 'The edit anchor no longer matches current Overleaf content.',
@@ -232,6 +242,11 @@
232
242
  },
233
243
 
234
244
  // 9.4 Reviewing
245
+ reviewing_unavailable: {
246
+ stage: 'reviewing', severity: 'blocked', defaultRetryable: false,
247
+ fallbackUserMessage: "Track Changes is unavailable for this project. It depends on the project owner's Overleaf plan.",
248
+ fallbackNextAction: 'Turn off Track in the composer to write without tracked changes. Codex still records each change so you can undo it.'
249
+ },
235
250
  reviewing_state_unknown: {
236
251
  stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
237
252
  fallbackUserMessage: 'Codex could not determine whether Reviewing/Track Changes is enabled.',
@@ -348,6 +363,11 @@
348
363
  fallbackUserMessage: 'Native host request failed.',
349
364
  fallbackNextAction: 'Open diagnostics and retry.'
350
365
  },
366
+ native_request_timeout: {
367
+ stage: 'native', severity: 'error', defaultRetryable: true,
368
+ fallbackUserMessage: 'The local service did not respond.',
369
+ fallbackNextAction: 'Retry, or open Diagnostics to check the local connection.'
370
+ },
351
371
  native_protocol_incompatible: {
352
372
  stage: 'native', severity: 'blocked', defaultRetryable: true,
353
373
  fallbackUserMessage: 'Native host version or capabilities do not match the extension.',
@@ -363,6 +383,11 @@
363
383
  fallbackUserMessage: 'Local Codex skills could not be loaded. This run did not start.',
364
384
  fallbackNextAction: 'Open Skills to reload the local catalog, then submit again.'
365
385
  },
386
+ codex_context_limit: {
387
+ stage: 'codex', severity: 'error', defaultRetryable: true,
388
+ fallbackUserMessage: 'This run stopped because the model context limit was exceeded.',
389
+ fallbackNextAction: 'Retry the task. If the context limit is reached again, use /compact or start a new conversation with less context.'
390
+ },
366
391
  codex_no_usable_result: {
367
392
  stage: 'codex', severity: 'error', defaultRetryable: true,
368
393
  fallbackUserMessage: 'Local Codex returned no usable final report or operations.',
@@ -413,6 +438,11 @@
413
438
  fallbackUserMessage: 'The provider rate limit was reached.',
414
439
  fallbackNextAction: 'Wait for the rate-limit window to recover and retry.'
415
440
  },
441
+ provider_quota_exceeded: {
442
+ stage: 'codex', severity: 'error', defaultRetryable: true,
443
+ fallbackUserMessage: 'The model provider quota or billing limit was reached.',
444
+ fallbackNextAction: 'Check the provider account and plan, or switch model or provider before retrying.'
445
+ },
416
446
  provider_connection_timeout: {
417
447
  stage: 'codex', severity: 'error', defaultRetryable: true,
418
448
  fallbackUserMessage: 'The provider connection timed out.',
@@ -616,6 +646,7 @@
616
646
  nextAction: catalog.fallbackNextAction,
617
647
  evidence: {}
618
648
  };
649
+ if (canonicalCode === 'reviewing_unavailable') failure.changedDocument = false;
619
650
  if (legacyReason) {
620
651
  failure.technicalMessage = legacyReason;
621
652
  }
@@ -791,8 +822,20 @@
791
822
  return !path.startsWith('/') && !/^[A-Za-z]:/.test(path) && !path.split('/').includes('..') ? path : '';
792
823
  }
793
824
 
825
+ function isContextLimitError(message, code) {
826
+ const text = String(message || '');
827
+ return ['codex_context_limit', 'context_length_exceeded', 'context_window_exceeded'].includes(code)
828
+ || /\b(?:codex_context_limit|context_length_exceeded|context_window_exceeded)\b/i.test(text)
829
+ || /Codex ran out of room in the model['’]s context window/i.test(text)
830
+ || /maximum context length is \d+ tokens[\s\S]*?(?:you requested|resulted in) \d+ tokens/i.test(text);
831
+ }
832
+
794
833
  function recoverDisplayFailure(failure, allowFilePrefix = true) {
795
834
  if (!failure || typeof failure !== 'object') return failure;
835
+ if (failure.code === 'codex_no_usable_result' && failure.stage === 'codex'
836
+ && isContextLimitError(failure.technicalMessage)) return { ...failure, code: 'codex_context_limit',
837
+ userMessage: FAILURE_CODE_CATALOG.codex_context_limit.fallbackUserMessage,
838
+ nextAction: FAILURE_CODE_CATALOG.codex_context_limit.fallbackNextAction };
796
839
  const originalCode = failure.evidence?.originalCode;
797
840
  const displayCodeAllowed = code => REVIEW_DISPLAY_CODES.has(code)
798
841
  && (code !== 'native_untracked_undo_pending_changes'
@@ -829,7 +872,8 @@
829
872
  function formatFailureReasonForDisplay(failure, locale, i18nLookup) {
830
873
  const display = recoverDisplayFailure(failure);
831
874
  const lookup = typeof i18nLookup === 'function' ? i18nLookup : () => undefined;
832
- const key = display?.code ? 'failureReason_' + display.code + '_user' : '';
875
+ const messageCode = getFailureMessageCode(display);
876
+ const key = messageCode ? 'failureReason_' + messageCode + '_user' : '';
833
877
  const template = key ? lookup(key) : '';
834
878
  const file = safeDisplayProjectPath(display?.file);
835
879
  const localized = localizeFailureReason(display ? { ...display, file } : display, locale, lookup);
@@ -838,6 +882,15 @@
838
882
  return { ...localized, code: display?.code, message, file, localized: Boolean(template && template !== key) };
839
883
  }
840
884
 
885
+ function getFailureMessageCode(failure) {
886
+ // Reload recovery retains its historical retry code. Only its presentation
887
+ // identifies the page interruption instead of a failed bridge request.
888
+ if (failure?.code === 'native_request_failed' && failure.source === 'panel_reload') {
889
+ return 'panel_reload_interrupted';
890
+ }
891
+ return typeof failure?.code === 'string' ? failure.code : '';
892
+ }
893
+
841
894
  function localizeFailureMessage(message, locale, i18nLookup) {
842
895
  return formatFailureReasonForDisplay({ code: 'unknown_legacy_failure', userMessage: message }, locale, i18nLookup).message;
843
896
  }
@@ -949,7 +1002,7 @@
949
1002
  const value = typeof i18nLookup === 'function' ? i18nLookup(key) : undefined;
950
1003
  return value && value !== key ? value : undefined;
951
1004
  };
952
- const code = failure && typeof failure.code === 'string' ? failure.code : '';
1005
+ const code = getFailureMessageCode(failure);
953
1006
  const userTemplate = code ? lookup('failureReason_' + code + '_user') : undefined;
954
1007
  const nextTemplate = (code ? lookup('failureReason_' + code + '_next') : undefined)
955
1008
  || (REVIEW_DISPLAY_CODES.has(code) ? lookup('failureReason_review_detail_next') : undefined);
@@ -989,6 +1042,7 @@
989
1042
  selectPrimaryFailure,
990
1043
  localizeFailureReason,
991
1044
  formatFailureReasonForDisplay,
992
- localizeFailureMessage
1045
+ localizeFailureMessage,
1046
+ isContextLimitError
993
1047
  };
994
1048
  });