codex-overleaf-link 2.5.1 → 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 (93) 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 +78 -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 +11 -2
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +920 -276
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsPanel.js +18 -0
  14. package/extension/src/content/generated/content.bundle.js +4496 -924
  15. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  16. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  17. package/extension/src/content/localSkillsPanel.js +2 -2
  18. package/extension/src/content/markdownDomRenderer.js +5 -3
  19. package/extension/src/content/markdownText.js +42 -5
  20. package/extension/src/content/modelPicker.js +1 -0
  21. package/extension/src/content/nativeCompatibilityController.js +29 -10
  22. package/extension/src/content/otWarmMirror.js +1 -1
  23. package/extension/src/content/pageBridgeClient.js +2 -2
  24. package/extension/src/content/panelMaintenance.js +6 -2
  25. package/extension/src/content/panelRenderer.js +51 -27
  26. package/extension/src/content/pendingInputView.js +1 -1
  27. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  28. package/extension/src/content/providerSettingsDialog.js +2 -1
  29. package/extension/src/content/recentProjects.js +4 -3
  30. package/extension/src/content/runActivitySummary.js +122 -14
  31. package/extension/src/content/runCompletionMeta.js +194 -6
  32. package/extension/src/content/runController.js +2 -1
  33. package/extension/src/content/runFailureNotice.js +8 -1
  34. package/extension/src/content/runPresence.js +18 -8
  35. package/extension/src/content/runQueueScheduler.js +9 -5
  36. package/extension/src/content/runRecoveryActions.js +2 -1
  37. package/extension/src/content/runResultActions.js +22 -20
  38. package/extension/src/content/runReviewController.js +94 -35
  39. package/extension/src/content/runTimelineView.js +224 -25
  40. package/extension/src/content/scopedPersistencePanelState.js +92 -23
  41. package/extension/src/content/sensitivePreflight.js +14 -1
  42. package/extension/src/content/sessionManager.js +49 -3
  43. package/extension/src/content/sessionMenuView.js +54 -18
  44. package/extension/src/content/sessionPersistence.js +25 -5
  45. package/extension/src/content/settingsPanel.js +15 -2
  46. package/extension/src/content/settingsWorkbench.js +127 -89
  47. package/extension/src/content/sharedSessionViewSync.js +4 -3
  48. package/extension/src/content/subagentActivityView.js +23 -7
  49. package/extension/src/content/updateNotice.js +60 -6
  50. package/extension/src/content/writebackOrchestrator.js +35 -22
  51. package/extension/src/page/compileBridge.js +193 -180
  52. package/extension/src/page/overleafCapabilities.js +4 -1
  53. package/extension/src/page/textCoordinates.js +17 -1
  54. package/extension/src/page/textFileCreator.js +9 -1
  55. package/extension/src/page/writebackRouter.js +24 -77
  56. package/extension/src/pageBridge.js +107 -66
  57. package/extension/src/popup.js +1 -1
  58. package/extension/src/shared/agentTranscript.js +350 -17
  59. package/extension/src/shared/auditRecords.js +2 -0
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/src/shared/compileAdapter.js +58 -17
  62. package/extension/src/shared/composerAttachmentDraft.js +6 -1
  63. package/extension/src/shared/contextUsage.js +125 -0
  64. package/extension/src/shared/failureReasons.js +42 -3
  65. package/extension/src/shared/i18n.js +299 -29
  66. package/extension/src/shared/lineReferences.js +38 -31
  67. package/extension/src/shared/pageRpcContract.js +1 -1
  68. package/extension/src/shared/panelIcons.js +39 -0
  69. package/extension/src/shared/reviewing.js +130 -1
  70. package/extension/src/shared/runActivityModel.js +70 -4
  71. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  72. package/extension/src/shared/sessionState.js +63 -4
  73. package/extension/src/shared/sessionTitle.js +14 -1
  74. package/extension/src/shared/sharedSessionBridge.js +81 -13
  75. package/extension/src/shared/staleGuard.js +72 -63
  76. package/extension/src/shared/storageDb.js +35 -3
  77. package/extension/src/shared/storageMigration.js +7 -1
  78. package/extension/src/shared/storageRunActions.js +62 -8
  79. package/extension/src/shared/undoOperations.js +126 -28
  80. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  81. package/extension/src/shared/writebackSettlement.js +21 -3
  82. package/extension/styles/panel.css +1106 -490
  83. package/native-host/src/bootstrapUpdate.js +52 -16
  84. package/native-host/src/chatBridgeRequest.js +60 -6
  85. package/native-host/src/chatBridgeResponse.js +10 -1
  86. package/native-host/src/chatCompletionsBridge.js +29 -2
  87. package/native-host/src/codexPromptAssembly.js +86 -118
  88. package/native-host/src/codexSessionRunner.js +126 -17
  89. package/native-host/src/providerContextError.js +20 -0
  90. package/native-host/src/taskRunnerRuntime.js +18 -6
  91. package/native-host/src/textPatch.js +473 -377
  92. package/native-host/src/updateManager.js +120 -15
  93. package/package.json +5 -8
@@ -12,12 +12,13 @@
12
12
  require('./subagentActivityModel'),
13
13
  require('./writebackIntent'),
14
14
  require('./directoryCleanupProof'),
15
- require('./undoOperations')
15
+ require('./undoOperations'),
16
+ require('./contextUsage')
16
17
  );
17
18
  } else {
18
19
  root.CodexOverleafModuleRegistry.define(
19
20
  'SessionState',
20
- ['I18n', 'SettlementFacts', 'RunInputQueue', 'SelectionContext', 'LineReferences', 'PathRedaction', 'SessionTitle', 'RunActivityModel', 'SubagentActivityModel', 'WritebackIntent', 'DirectoryCleanupProof', 'UndoOperations'],
21
+ ['I18n', 'SettlementFacts', 'RunInputQueue', 'SelectionContext', 'LineReferences', 'PathRedaction', 'SessionTitle', 'RunActivityModel', 'SubagentActivityModel', 'WritebackIntent', 'DirectoryCleanupProof', 'UndoOperations', 'ContextUsage'],
21
22
  factory
22
23
  );
23
24
  }
@@ -33,7 +34,8 @@
33
34
  SubagentActivityModel,
34
35
  WritebackIntent,
35
36
  DirectoryCleanupProof,
36
- UndoOperations
37
+ UndoOperations,
38
+ ContextUsage
37
39
  ) {
38
40
  'use strict';
39
41
 
@@ -250,6 +252,7 @@
250
252
  selectionContext: SelectionContext.normalize(session.selectionContext),
251
253
  projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
252
254
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
255
+ contextUsage: ContextUsage.normalize(session.contextUsage),
253
256
  forkedFromSessionId: typeof session.forkedFromSessionId === 'string' ? session.forkedFromSessionId : '',
254
257
  forkedFromRunId: typeof session.forkedFromRunId === 'string' ? session.forkedFromRunId : '',
255
258
  pendingInputs: normalizePendingInputs(
@@ -330,6 +333,7 @@
330
333
  focusFiles: normalizeFocusFiles(active.focusFiles),
331
334
  projectReferenceFiles: normalizeProjectReferenceFiles(active.projectReferenceFiles),
332
335
  codexThreadId: active.codexThreadId || '',
336
+ contextUsage: ContextUsage.normalize(active.contextUsage),
333
337
  providerId: state.providerId || 'builtin'
334
338
  };
335
339
  state.runs = Array.isArray(active.runs) ? active.runs : [];
@@ -374,6 +378,7 @@
374
378
  selectionContext: SelectionContext.normalize(overrides.selectionContext),
375
379
  projectReferenceFiles: normalizeProjectReferenceFiles(overrides.projectReferenceFiles),
376
380
  codexThreadId: typeof overrides.codexThreadId === 'string' ? overrides.codexThreadId : '',
381
+ contextUsage: ContextUsage.normalize(overrides.contextUsage),
377
382
  forkedFromSessionId: typeof overrides.forkedFromSessionId === 'string' ? overrides.forkedFromSessionId : '',
378
383
  forkedFromRunId: typeof overrides.forkedFromRunId === 'string' ? overrides.forkedFromRunId : '',
379
384
  pendingInputs: normalizePendingInputs(overrides.pendingInputs)
@@ -439,6 +444,34 @@
439
444
  });
440
445
  }
441
446
 
447
+ // Composer input changes only the draft on an already normalized state.
448
+ // Keep live run/event objects intact so typing cannot detach their writers.
449
+ function updateActiveSessionDraft(state, task) {
450
+ const active = getActiveSession(state);
451
+ if (!active) {
452
+ return mirrorActiveSession({ ...state });
453
+ }
454
+ const draft = sanitizeAssistantVisibleText(task);
455
+ if (draft === active.task && draft === state.task) {
456
+ return state;
457
+ }
458
+ const rawTitle = typeof active.title === 'string' ? active.title.trim() : '';
459
+ const derivedTitle = deriveSessionTitle(active.runs, task);
460
+ const titleSource = normalizeTitleSource(active.titleSource, rawTitle, derivedTitle);
461
+ const next = {
462
+ ...active,
463
+ task: draft,
464
+ updatedAt: new Date().toISOString(),
465
+ title: normalizeSessionTitle(rawTitle, derivedTitle, titleSource),
466
+ titleSource
467
+ };
468
+ return {
469
+ ...state,
470
+ sessions: state.sessions.map(session => session === active ? next : session),
471
+ task: next.task
472
+ };
473
+ }
474
+
442
475
  function setActiveSession(state, sessionId) {
443
476
  if (!(state.sessions || []).some(session => session.id === sessionId)) {
444
477
  return mirrorActiveSession(state);
@@ -665,7 +698,17 @@
665
698
  ...(value.directoryCleanup === true ? { directoryCleanup: true, ...(value.coreRetained === true ? { coreRetained: true } : {}) } : {}) };
666
699
  }
667
700
 
701
+ // Normalization makes UI copies of the same record. This identity is page-local:
702
+ // it never travels through JSON/storage and is never shared by a replacement run.
703
+ const runIdentities = new WeakMap();
704
+ function sameRunIdentity(left, right) {
705
+ return Boolean(left && right && (left === right
706
+ || (runIdentities.has(left) && runIdentities.get(left) === runIdentities.get(right))));
707
+ }
708
+
668
709
  function normalizeRun(run, options = {}) {
710
+ const sourceRun = run;
711
+
669
712
  if (options.restoreRunningRuns === true || options.recoverSettledRuns === true) run = recoverSettledRunningRun(run);
670
713
  run = recoverRecordedRunFailure(run, options.locale);
671
714
  const shouldStopRestoredRun = options.restoreRunningRuns === true && run.status === 'running';
@@ -681,6 +724,12 @@
681
724
  status: 'failed',
682
725
  kind: 'report',
683
726
  detail: i18n.t(locale, 'restoredRunStoppedDetail'),
727
+ detailStructured: {
728
+ conclusion: i18n.t(locale, 'restoredRunStoppedDetail'),
729
+ conclusionSource: { origin: 'product', key: 'reload_interrupted' },
730
+ // Ask forbids writeback; an interrupted Auto run has no zero-write proof.
731
+ meta: run.mode === 'ask' ? [{ key: 'writeResult', value: 'wrote 0 items, skipped 0 items' }] : []
732
+ },
684
733
  failure: { code: 'native_request_failed', source: 'panel_reload' },
685
734
  timestamp: new Date().toISOString()
686
735
  });
@@ -715,7 +764,9 @@
715
764
  undoStatus: sanitizeAssistantVisibleText(run.undoStatus),
716
765
  ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
717
766
  nativeRequestId: sanitizeAssistantVisibleText(run.nativeRequestId),
767
+ ...(run.operation === 'compact' ? { operation: 'compact' } : {}),
718
768
  codexThreadId: sanitizeAssistantVisibleText(run.codexThreadId), codexTurnId: sanitizeAssistantVisibleText(run.codexTurnId),
769
+ contextUsage: run.forkSnapshot === true ? null : ContextUsage.normalize(run.contextUsage),
719
770
  forkSnapshot: run.forkSnapshot === true,
720
771
  forkSourceRunId: sanitizeAssistantVisibleText(run.forkSourceRunId),
721
772
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
@@ -744,6 +795,9 @@
744
795
  applyTrackedChangeStatus(normalized, run.trackedChangeStatus);
745
796
  Object.assign(normalized, pickWritebackRecovery(run));
746
797
 
798
+ const identity = runIdentities.get(sourceRun) || {};
799
+ runIdentities.set(sourceRun, identity);
800
+ runIdentities.set(normalized, identity);
747
801
  return normalized;
748
802
  }
749
803
 
@@ -1231,6 +1285,7 @@
1231
1285
  selectionContext: SelectionContext.normalize(session.selectionContext),
1232
1286
  projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
1233
1287
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
1288
+ contextUsage: ContextUsage.normalize(session.contextUsage),
1234
1289
  forkedFromSessionId: normalizeTextField(session.forkedFromSessionId, 160),
1235
1290
  forkedFromRunId: normalizeTextField(session.forkedFromRunId, 160),
1236
1291
  pendingInputs: normalizePendingInputs(session.pendingInputs)
@@ -1319,7 +1374,7 @@
1319
1374
  events: compactRunEvents(run.events, limits),
1320
1375
  ...(run.subagents?.length ? { subagents: SubagentActivityModel.normalize(run.subagents, sanitizeAssistantVisibleText) } : {}),
1321
1376
  attachments: compactRunAttachmentsForStorage(run.attachments, limits),
1322
- appliedOperations: [],
1377
+ appliedOperations: UndoOperations.compactUnavailableUndoOperations(run),
1323
1378
  undoOperations: undoPayload.undoOperations,
1324
1379
  undoBaseFiles: undoPayload.undoBaseFiles,
1325
1380
  undoTrackedChanges: undoPayload.undoTrackedChanges,
@@ -1332,7 +1387,9 @@
1332
1387
  undoStatus: summarizeTextForStorage(run.undoStatus, 'undo status'),
1333
1388
  ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
1334
1389
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
1390
+ ...(run.operation === 'compact' ? { operation: 'compact' } : {}),
1335
1391
  codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
1392
+ contextUsage: run.forkSnapshot === true ? null : ContextUsage.normalize(run.contextUsage),
1336
1393
  forkSnapshot: run.forkSnapshot === true,
1337
1394
  forkSourceRunId: normalizeTextField(run.forkSourceRunId, 160),
1338
1395
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
@@ -1744,6 +1801,7 @@
1744
1801
  }
1745
1802
 
1746
1803
  return {
1804
+ sameRunIdentity,
1747
1805
  normalizeRecoveryRetention,
1748
1806
  pruneRunRecoveryScope,
1749
1807
  pickWritebackRecovery,
@@ -1759,6 +1817,7 @@
1759
1817
  selectVisibleSessionsForList,
1760
1818
  setActiveSession,
1761
1819
  updateActiveSession,
1820
+ updateActiveSessionDraft,
1762
1821
  normalizeRuns,
1763
1822
  normalizeRunStatus,
1764
1823
  recoverRecordedRunFailure,
@@ -25,6 +25,19 @@
25
25
  }
26
26
 
27
27
  function fromPrompt(value, { sanitize = text => text, maxChars = 24 } = {}) {
28
+ let title = projectPrompt(value, { sanitize, maxChars });
29
+ // Display titles pass through normalization again on unrelated callbacks.
30
+ // Finish removing nested formatting/prefixes now, before acknowledging a title.
31
+ // The bound guards against an input that never settles.
32
+ for (let pass = 0; pass < 8; pass += 1) {
33
+ const next = projectPrompt(title, { sanitize, maxChars });
34
+ if (next === title) return title;
35
+ title = next;
36
+ }
37
+ return title;
38
+ }
39
+
40
+ function projectPrompt(value, { sanitize, maxChars }) {
28
41
  if (typeof value !== 'string' || !value.trim()) return '';
29
42
  const source = String(sanitize(value.slice(0, INPUT_LIMIT)) || '');
30
43
  let fence = null;
@@ -75,7 +88,7 @@
75
88
  .replace(/\[(?:REDACTED_SECRET|local path(?::\d+)?)\]/gi, ' ')
76
89
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
77
90
  .replace(/\s+/g, ' ').trim()
78
- .replace(/^(?:\u8bf7(?:\u4f60)?\u5e2e\u6211|\u8bf7\u4f60|\u5e2e\u6211|\u9ebb\u70e6\u4f60(?:\u5e2e\u6211)?|\u9ebb\u70e6\u5e2e\u6211|please\s+)\s*/i, '');
91
+ .replace(/^(?:(?:\u8bf7(?:\u4f60)?\u5e2e\u6211|\u8bf7\u4f60|\u5e2e\u6211|\u9ebb\u70e6\u4f60(?:\u5e2e\u6211)?|\u9ebb\u70e6\u5e2e\u6211|please\s+)\s*)+/i, '');
79
92
  }
80
93
 
81
94
  function isScaffolding(value) {
@@ -160,7 +160,9 @@
160
160
  Object.defineProperty(record, BASE, { value: base, enumerable: true });
161
161
  return record;
162
162
  }
163
- async function list(projectId = '', adopt = true) {
163
+ // List reads keep a per-record merge base without changing the visible view's
164
+ // baseline. Hydration explicitly adopts; passive refresh adopts after apply.
165
+ async function list(projectId = '', adopt = false) {
164
166
  const value = capture(projectId);
165
167
  await migrate(value);
166
168
  const result = []; let after = '';
@@ -185,29 +187,94 @@
185
187
  return Boolean(base && (session.updatedAt !== base.updatedAt || session.task !== base.task));
186
188
  }
187
189
  function wasStored(id) { return baselines.has(cacheKey(capture(), id)); }
190
+ function acknowledgeReviewBaseline(base, record, receipt, runIds) {
191
+ if (!base || !receipt) return base;
192
+ // Only this caller's acknowledged review fields have been submitted.
193
+ // The envelope base still describes its own view, never the latest read.
194
+ return { ...base, runs: (base.runs || []).map(run => {
195
+ const proposed = record.runs?.find(item => item.id === run.id);
196
+ const saved = receipt.runs?.find(item => item.id === run.id);
197
+ return runIds.includes(run.id) && proposed && saved
198
+ && legacy.reviewStateSnapshot(proposed) === legacy.reviewStateSnapshot(saved)
199
+ ? legacy.copyRunReviewState(run, proposed) : run;
200
+ }) };
201
+ }
188
202
  async function put(records, options = {}) {
189
203
  if (!records.length) return [];
190
204
  const value = capture(records[0].projectId, records[0].accountScopeId);
191
205
  if (records.some(record => record.projectId !== value.projectId || record.accountScopeId !== value.accountScopeId))
192
206
  throw failure('shared_store_scope_mismatch', 'A session batch must belong to one account and project.');
193
207
  return withWrite(value, async () => {
194
- const entries = records.map(record => ({
195
- record: clean(record),
196
- base: record[BASE] || baselines.get(cacheKey(value, record.id)) || null
197
- }));
208
+ const entries = records.map(record => {
209
+ let base = record[BASE] || baselines.get(cacheKey(value, record.id)) || null;
210
+ const receipt = options.reviewReceipts?.find(saved => saved.id === record.id
211
+ && saved.projectId === record.projectId && saved.accountScopeId === record.accountScopeId);
212
+ base = acknowledgeReviewBaseline(base, record, receipt, options.reviewRunIds || []);
213
+ return { record: clean(record), base };
214
+ });
198
215
  const transfer = await Transfer.encode(entries);
199
216
  const written = await request(transfer.method, value, { ...transfer.payload, reviewRunIds: options.reviewRunIds || [] });
200
217
  // Keep the caller's submitted view as its merge baseline. A merged
201
218
  // remote change has not necessarily been applied to the visible UI.
202
- for (const entry of entries) baselines.set(cacheKey(value, entry.record.id), clean(entry.record));
219
+ if (options.adopt !== false)
220
+ for (const entry of entries) baselines.set(cacheKey(value, entry.record.id), clean(entry.record));
203
221
  return written;
204
222
  });
205
223
  }
206
- async function get(id) {
224
+ async function get(id, options = {}) {
207
225
  const value = capture();
208
226
  await migrate(value);
209
227
  const record = await request('get', value, { id });
210
- return tag(record, value, !heldLocks.has(lockKey('state', value)));
228
+ return tag(record, value, options.adopt !== false && !heldLocks.has(lockKey('state', value)));
229
+ }
230
+ async function putSessionReviewState(record, runIds) {
231
+ const value = capture(record.projectId, record.accountScopeId);
232
+ const ids = new Set(runIds);
233
+ const assertOwners = () => {
234
+ assertScope(value);
235
+ for (const runId of ids) {
236
+ const owner = heldLocks.get(lockKey('review', { ...value, runId }));
237
+ if (!owner || owner.lost) throw failure('storage_lock_lost', 'The shared review owner disconnected.');
238
+ }
239
+ };
240
+ assertOwners();
241
+ return withWrite(value, async () => {
242
+ assertOwners();
243
+ const latest = await request('get', value, { id: record.id });
244
+ if (!latest) throw failure('shared_session_deleted', 'The session no longer exists.');
245
+ if (latest.projectId !== value.projectId || latest.accountScopeId !== value.accountScopeId)
246
+ throw failure('shared_store_scope_mismatch', 'The session does not belong to the captured account and project.');
247
+ const proposed = new Map((record.runs || []).map(run => [run.id, run]));
248
+ const identity = run => JSON.stringify(['id', 'runProjectId', 'runAccountScopeId', 'startedAt', 'nativeEventSeq',
249
+ 'codexThreadId', 'codexTurnId', 'executionSnapshot', 'forkSnapshot'].map(key => run[key] ?? null));
250
+ for (const id of ids) {
251
+ const current = latest.runs?.find(run => run.id === id), incoming = proposed.get(id);
252
+ if (!current || !incoming || incoming.runProjectId !== value.projectId
253
+ || (incoming.runAccountScopeId && incoming.runAccountScopeId !== value.accountScopeId)
254
+ || identity(current) !== identity(incoming))
255
+ throw failure('review_context_changed', 'The shared review run is no longer available in its original scope.');
256
+ }
257
+ const next = { ...clean(latest), runs: latest.runs.map(run => {
258
+ if (!ids.has(run.id) || run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) return run;
259
+ return legacy.copyRunReviewState(run, proposed.get(run.id));
260
+ }) };
261
+ const transfer = await Transfer.encode([{ record: next, base: clean(latest) }]);
262
+ assertOwners();
263
+ await request(transfer.method, value, { ...transfer.payload, reviewRunIds: [...ids] });
264
+ const saved = await request('get', value, { id: record.id });
265
+ assertOwners();
266
+ if (!saved || saved.projectId !== value.projectId || saved.accountScopeId !== value.accountScopeId
267
+ || [...ids].some(id => {
268
+ const run = saved.runs?.find(run => run.id === id);
269
+ return !run || identity(run) !== identity(proposed.get(id))
270
+ || legacy.reviewStateSnapshot(run) !== legacy.reviewStateSnapshot(proposed.get(id));
271
+ })) throw failure('review_state_not_persisted', 'The review outcome was not durably saved. Check Overleaf before retrying.');
272
+ // A later envelope retry must also recognize the review already saved,
273
+ // without adopting any peer envelope fields from this readback.
274
+ const key = cacheKey(value, record.id), base = baselines.get(key);
275
+ if (base) baselines.set(key, acknowledgeReviewBaseline(base, record, saved, [...ids]));
276
+ return saved;
277
+ });
211
278
  }
212
279
  async function erase(method, projectId = '', id, accountScopeId) {
213
280
  const value = capture(projectId, accountScopeId);
@@ -228,7 +295,7 @@
228
295
  return record;
229
296
  });
230
297
  route('getAllSessions', () => list());
231
- route('getRecord', (store, id) => store === 'sessions' ? get(id) : api.getRecord(store, id));
298
+ route('getRecord', (store, id, options) => store === 'sessions' ? get(id, options) : api.getRecord(store, id));
232
299
  route('getAllByIndex', async (store, index, value) => {
233
300
  if (store !== 'sessions') return api.getAllByIndex(store, index, value);
234
301
  const records = await list(index === 'projectId' ? value : '');
@@ -236,10 +303,10 @@
236
303
  });
237
304
  route('claimSessionsForAccount', async (projectId, accountScopeId, deleted = []) => {
238
305
  capture(projectId, accountScopeId);
239
- return (await list(projectId)).filter(record => !deleted.includes(record.id));
306
+ return (await list(projectId, true)).filter(record => !deleted.includes(record.id));
240
307
  });
241
- route('putRecord', async (store, record) =>
242
- store === 'sessions' ? (await put([record]))[0] : api.putRecord(store, record));
308
+ route('putRecord', async (store, record, options) =>
309
+ store === 'sessions' ? (await put([record], options))[0] : api.putRecord(store, record));
243
310
  route('putRecords', (store, records, options) =>
244
311
  store === 'sessions' ? put(records, options) : api.putRecords(store, records));
245
312
  route('deleteRecord', (store, id, expectedScope) =>
@@ -280,7 +347,8 @@
280
347
  readSharedProjectSnapshot: projectId => list(projectId, false),
281
348
  adoptSharedSessionBaseline: adopt,
282
349
  hasUnpersistedSharedSession: hasLocalChanges,
283
- wasSharedSessionStored: wasStored
350
+ wasSharedSessionStored: wasStored,
351
+ putSessionReviewState
284
352
  });
285
353
  return wrapped;
286
354
  }
@@ -45,72 +45,62 @@
45
45
  const baseContent = normalizeText(baseFileLookup.get(filePath));
46
46
  const current = normalizeText(currentContent);
47
47
  if (current !== baseContent) {
48
- if (typeof console !== 'undefined') {
49
- console.warn('[codex-overleaf] staleGuard: STALE', filePath,
50
- 'base.length:', baseContent.length, 'current.length:', current.length,
51
- 'base[0..80]:', JSON.stringify(baseContent.slice(0, 80)),
52
- 'current[0..80]:', JSON.stringify(current.slice(0, 80)));
48
+ if (Array.isArray(operation.patches) && operation.patches.length) {
49
+ const preparedPatches = prepareOperationPatches(operation, currentContent, baseFileLookup);
50
+ if (preparedPatches.ok) return { ok: true, reconciled: true, strategy: 'change-envelope', preparedPatches };
53
51
  }
54
- const patchRangeFreshness = checkPatchRangeFreshness(operation, currentContent, baseFileLookup);
55
- if (patchRangeFreshness) {
56
- return patchRangeFreshness;
57
- }
58
- return {
59
- ok: false,
60
- code: 'stale_snapshot',
61
- reasonKey: 'staleSnapshot',
62
- reasonParams: { filePath },
63
- reason: `${filePath} 在任务执行期间被你或协作者改过,Codex 没有覆盖它。请查看差异后重试。`
64
- };
52
+ return staleSourceChanged(filePath);
65
53
  }
66
54
 
67
55
  return { ok: true };
68
56
  }
69
57
 
70
- function checkPatchRangeFreshness(operation, currentContent, baseFileLookup) {
71
- const prepared = prepareOperationPatches(operation, currentContent, baseFileLookup);
72
- if (!prepared.ok) {
73
- return { ...prepared, code: 'stale_patch_range', reasonKey: 'stalePatchConflict' };
74
- }
75
- const patches = prepared.patches;
76
- if (!patches.length) {
77
- return null;
78
- }
79
-
80
- const current = String(currentContent ?? '');
81
- for (const rawPatch of patches) {
82
- const from = Number(rawPatch?.from);
83
- const to = Number(rawPatch?.to);
84
- const expected = String(rawPatch?.expected ?? '');
85
- if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from || to > current.length) {
86
- return {
87
- ok: false,
88
- code: 'stale_patch_range',
89
- reasonKey: 'stalePatchLocation',
90
- reason: 'Codex 要修改的位置已经无法和当前 Overleaf 内容对齐,所以没有写入。请重新运行任务。'
91
- };
92
- }
93
- if (current.slice(from, to) !== expected) {
94
- return {
95
- ok: false,
96
- code: 'stale_patch_range',
97
- reasonKey: 'stalePatchConflict',
98
- reason: 'Codex 要修改的具体位置已经被你或协作者改过,所以没有覆盖它。请查看差异后重试。'
99
- };
100
- }
101
- }
102
-
58
+ function staleSourceChanged(filePath) {
103
59
  return {
104
- ok: true,
105
- reconciled: true,
106
- strategy: 'patch-range'
60
+ ok: false,
61
+ code: 'stale_snapshot',
62
+ reasonKey: 'staleSnapshot',
63
+ reasonParams: { filePath },
64
+ reason: `${filePath} 在任务执行期间被你或协作者改过,Codex 没有覆盖它。请查看差异后重试。`
107
65
  };
108
66
  }
109
67
 
110
- // Bridge the raw Native baseline to the editor's line-ending representation.
111
- // Logical LF offsets are only an intermediate coordinate system: both the
112
- // range check and the eventual editor dispatch use the same raw patches.
68
+ // Rebase only ranges wholly in a common prefix/suffix. All patches use the
69
+ // same envelope before any of them are applied to the editor.
113
70
  function prepareOperationPatches(operation, currentContent, baseFileLookup) {
71
+ const path = normalizePath(operation?.path), base = baseFileLookup?.get(path);
72
+ const current = String(currentContent ?? '');
73
+ const envelope = typeof base === 'string' ? changedEnvelope(normalizeText(base), normalizeText(current)) : null;
74
+ const prepared = mapOperationPatches(operation, current, baseFileLookup, envelope);
75
+ return !prepared.ok && envelope ? staleSourceChanged(path) : prepared;
76
+ }
77
+
78
+ function changedEnvelope(base, current) {
79
+ if (base === current) return null;
80
+ let prefix = 0, suffix = 0;
81
+ while (prefix < base.length && prefix < current.length && base[prefix] === current[prefix]) prefix++;
82
+ while (suffix < base.length - prefix && suffix < current.length - prefix
83
+ && base[base.length - suffix - 1] === current[current.length - suffix - 1]) suffix++;
84
+ return { prefix, end: base.length - suffix, delta: current.length - base.length };
85
+ }
86
+
87
+ function mapEnvelopeRange(from, to, envelope, insertion = from === to) {
88
+ if (!envelope) return { from, to };
89
+ if (insertion && (from === envelope.prefix || from === envelope.end)) return null;
90
+ if (to <= envelope.prefix) return { from, to };
91
+ if (from >= envelope.end) return { from: from + envelope.delta, to: to + envelope.delta };
92
+ return null;
93
+ }
94
+
95
+ // Historical checkpoints already refer to observed applied offsets. This
96
+ // compatibility converter never authorizes a new forward write or rebase.
97
+ function prepareStoredInversePatches(operation, currentContent, baseFileLookup) {
98
+ return mapOperationPatches(operation, currentContent, baseFileLookup, null);
99
+ }
100
+
101
+ // Logical LF offsets bridge raw Native and editor coordinates. Expand split
102
+ // CRLF edges before dispatch; keep unchanged editor line endings intact.
103
+ function mapOperationPatches(operation, currentContent, baseFileLookup, envelope) {
114
104
  const patches = Array.isArray(operation?.patches) ? operation.patches : [];
115
105
  const current = String(currentContent ?? '');
116
106
  const path = normalizePath(operation?.path);
@@ -120,8 +110,21 @@
120
110
  }
121
111
  const baseMap = lineEndingMap(base);
122
112
  const currentMap = lineEndingMap(current);
123
- // With no CRLF on either side, retain the existing raw range semantics.
124
- if (!baseMap.removed.length && !currentMap.removed.length) return { ok: true, patches };
113
+ if (!baseMap.removed.length && !currentMap.removed.length) {
114
+ if (!envelope) return { ok: true, patches };
115
+ const mapped = [];
116
+ let previousTo = 0;
117
+ for (const patch of patches) {
118
+ const from = Number(patch?.from), to = Number(patch?.to), expected = String(patch?.expected ?? '');
119
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from < previousTo || to < from || to > base.length
120
+ || base.slice(from, to) !== expected) return patchMappingFailure('stale_patch');
121
+ previousTo = to;
122
+ const range = mapEnvelopeRange(from, to, envelope);
123
+ if (!range || current.slice(range.from, range.to) !== expected) return patchMappingFailure('stale_patch');
124
+ mapped.push({ ...range, expected, insert: String(patch.insert ?? '') });
125
+ }
126
+ return { ok: true, patches: mapped };
127
+ }
125
128
 
126
129
  const groups = [];
127
130
  let previousTo = 0;
@@ -132,6 +135,11 @@
132
135
  }
133
136
  if (base.slice(from, to) !== String(patch.expected ?? '')) return patchMappingFailure('stale_patch');
134
137
  previousTo = to;
138
+ // Check each insertion boundary even if CRLF expansion later merges it
139
+ // with an adjacent nonempty range.
140
+ if (!mapEnvelopeRange(baseMap.toLogical(from), baseMap.toLogical(to), envelope, from === to)) {
141
+ return patchMappingFailure('stale_patch');
142
+ }
135
143
  // Expand only a split CRLF or a new pair formed across an edit edge.
136
144
  // Other neighbouring newlines belong to the editor and stay untouched.
137
145
  let start = from, end = to;
@@ -147,9 +155,9 @@
147
155
 
148
156
  const mapped = [], sourceChanges = [];
149
157
  for (const group of groups) {
150
- const logicalFrom = baseMap.toLogical(group.start), logicalTo = baseMap.toLogical(group.end);
151
- if (logicalTo > currentMap.length) return patchMappingFailure('invalid_patch');
152
- const from = currentMap.toRaw(logicalFrom), to = currentMap.toRaw(logicalTo);
158
+ const range = mapEnvelopeRange(baseMap.toLogical(group.start), baseMap.toLogical(group.end), envelope);
159
+ if (!range || range.from < 0 || range.to > currentMap.length) return patchMappingFailure('stale_patch');
160
+ const from = currentMap.toRaw(range.from), to = currentMap.toRaw(range.to);
153
161
  const expected = current.slice(from, to);
154
162
  if (normalizeText(expected) !== normalizeText(base.slice(group.start, group.end))) {
155
163
  return patchMappingFailure('stale_patch');
@@ -173,13 +181,13 @@
173
181
  while (oldEnd > prefix && newEnd > prefix && expected[oldEnd - 1] === insert[newEnd - 1]) { oldEnd -= 1; newEnd -= 1; }
174
182
  mapped.push({ from: from + prefix, to: from + oldEnd, expected: expected.slice(prefix, oldEnd), insert: insert.slice(prefix, newEnd) });
175
183
  }
176
- // A later operation in the same batch still addresses the original
177
- // representation, so advance its baseline separately from editor text.
184
+ // Preserve source coordinates for line-ending-only conversion. A rebased
185
+ // entry instead advances to its actual verified editor post-image.
178
186
  let sourceContent = base;
179
187
  for (const patch of sourceChanges.reverse()) {
180
188
  sourceContent = sourceContent.slice(0, patch.from) + patch.insert + sourceContent.slice(patch.to);
181
189
  }
182
- return { ok: true, patches: mapped, sourceContent };
190
+ return { ok: true, patches: mapped, ...(!envelope ? { sourceContent } : {}) };
183
191
  }
184
192
 
185
193
  function patchMappingFailure(code) {
@@ -257,6 +265,7 @@
257
265
  buildBaseFileLookup,
258
266
  checkOperationFreshness,
259
267
  prepareOperationPatches,
268
+ prepareStoredInversePatches,
260
269
  updateExpectedFileContent,
261
270
  removeExpectedFile,
262
271
  moveExpectedFile,
@@ -1,7 +1,7 @@
1
1
  (function initStorageDb(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
3
  module.exports = factory(require('./storageRunActions'), require('./runInputQueue'),
4
- require('./settlementFacts'), require('./storageSessionClaims'), require('./sessionState'), require('./runActivityModel'), require('./sharedSessionBridge'), require('./subagentActivityModel'), require('./storageValuePruning'), require('./composerAttachmentDraft'), require('./sharedSessionTransfer'));
4
+ require('./settlementFacts'), require('./storageSessionClaims'), require('./sessionState'), require('./runActivityModel'), require('./sharedSessionBridge'), require('./subagentActivityModel'), require('./storageValuePruning'), require('./composerAttachmentDraft'), require('./sharedSessionTransfer'), require('./contextUsage'));
5
5
  } else {
6
6
  root.CodexOverleafModuleRegistry.define('StorageDb', [
7
7
  'StorageRunActions',
@@ -14,10 +14,11 @@
14
14
  'SubagentActivityModel',
15
15
  'StorageValuePruning',
16
16
  'ComposerAttachmentDraft',
17
- 'SharedSessionTransfer'
17
+ 'SharedSessionTransfer',
18
+ 'ContextUsage'
18
19
  ], factory);
19
20
  }
20
- })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims, SessionState, RunActivityModel, SharedSessionBridge, SubagentActivityModel, StorageValuePruning, ComposerAttachmentDraft, SharedSessionTransfer) {
21
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims, SessionState, RunActivityModel, SharedSessionBridge, SubagentActivityModel, StorageValuePruning, ComposerAttachmentDraft, SharedSessionTransfer, ContextUsage) {
21
22
  'use strict';
22
23
  const { removeEmptySummaryFields, removeEmptyFields } = StorageValuePruning;
23
24
  if (!StorageRunActions?.compactRunsForStorage || !StorageRunActions?.compactRunActionPayload || !StorageRunActions?.compactProviderSnapshot || !StorageRunActions?.compactRunExecutionSnapshot || !StorageRunActions?.compactStructuredEventValue || !StorageRunActions?.hashString || !RunInputQueue?.compactForStorage || !SettlementFacts?.compactSettlementFacts || !StorageSessionClaims?.createSessionClaimer) {
@@ -276,6 +277,7 @@
276
277
  title: normalizeSessionTitleForStorage(input.title, titleSource),
277
278
  titleSource: titleSource,
278
279
  codexThreadId: typeof input.codexThreadId === 'string' ? input.codexThreadId : '',
280
+ contextUsage: ContextUsage.normalize(input.contextUsage),
279
281
  forkedFromSessionId: normalizeTextField(input.forkedFromSessionId, 160),
280
282
  forkedFromRunId: normalizeTextField(input.forkedFromRunId, 160),
281
283
  status: typeof input.status === 'string' && input.status ? input.status : 'active',
@@ -303,6 +305,17 @@
303
305
  accountScopeUnavailable: accountScopeUnavailable,
304
306
  safeTaskSummary: safeTaskSummary
305
307
  };
308
+ // Optional provenance for the automatic clear of one captured submission.
309
+ // It is envelope metadata, not run/review authority; no schema migration is needed.
310
+ if (record.task === '' && typeof input.draftConsumption?.task === 'string'
311
+ && input.draftConsumption.task && typeof input.draftConsumption.runId === 'string'
312
+ && input.draftConsumption.runId && record.runs.some(run => run.id === input.draftConsumption.runId
313
+ && !run.forkSnapshot && run.runProjectId === record.projectId)) {
314
+ record.draftConsumption = {
315
+ runId: input.draftConsumption.runId,
316
+ task: normalizeDisplayTextForStorage(input.draftConsumption.task, SESSION_STORAGE_LIMITS.taskChars)
317
+ };
318
+ }
306
319
  if (options.preserveRunActionPayload === true) {
307
320
  // Count the real display record before allocating recovery space. Adding
308
321
  // older checkpoints must not make an otherwise valid history unwritable.
@@ -436,6 +449,8 @@
436
449
  turnId: typeof input.turnId === 'string' ? input.turnId : '',
437
450
  createdAt: typeof input.createdAt === 'string' ? input.createdAt : now,
438
451
  completedAt: typeof input.completedAt === 'string' ? input.completedAt : '',
452
+ requireReviewing: typeof input.requireReviewing === 'boolean' ? input.requireReviewing : undefined,
453
+ trackingDecision: ['continue', 'disable'].includes(input.trackingDecision) ? input.trackingDecision : '',
439
454
  mode: typeof input.mode === 'string' ? input.mode : '', ...StorageRunActions.compactProviderSnapshot(input),
440
455
  model: typeof input.model === 'string' ? input.model : '',
441
456
  reasoningEffort: typeof input.reasoningEffort === 'string' ? input.reasoningEffort : '',
@@ -708,7 +723,9 @@
708
723
  undoStatus: normalizeDisplayTextForStorage(run.undoStatus, SESSION_STORAGE_LIMITS.statusTextChars),
709
724
  ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
710
725
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
726
+ ...(run.operation === 'compact' ? { operation: 'compact' } : {}),
711
727
  codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
728
+ contextUsage: run.forkSnapshot === true ? null : ContextUsage.normalize(run.contextUsage),
712
729
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
713
730
  queueItemId: normalizeTextField(run.queueItemId, 160)
714
731
  };
@@ -775,6 +792,19 @@
775
792
  if (event.failure) {
776
793
  compact.failure = compactStructuredEventValueForStorage(event.failure, 0);
777
794
  }
795
+ if (Array.isArray(event.compileErrors) && event.compileErrors.length) {
796
+ compact.compileErrors = event.compileErrors.slice(0, 5)
797
+ .map(item => normalizeDisplayTextForStorage(String(item), SESSION_STORAGE_LIMITS.detailChars))
798
+ .filter(Boolean);
799
+ }
800
+ if (Array.isArray(event.rejectedHunks) && event.rejectedHunks.length) {
801
+ compact.rejectedHunks = event.rejectedHunks.slice(0, 10)
802
+ .map(item => ({
803
+ path: normalizeDisplayTextForStorage(String(item?.path || ''), SESSION_STORAGE_LIMITS.pathChars),
804
+ summary: normalizeDisplayTextForStorage(String(item?.summary || ''), SESSION_STORAGE_LIMITS.detailChars)
805
+ }))
806
+ .filter(item => item.path);
807
+ }
778
808
  return compact;
779
809
  });
780
810
  }
@@ -1343,6 +1373,8 @@
1343
1373
  projectRetentionWarnings: StorageRunActions.projectRetentionWarnings,
1344
1374
  projectRetentionUpdates: StorageRunActions.projectRetentionUpdates,
1345
1375
  mergeSessionReviewState: StorageRunActions.mergeSessionReviewState,
1376
+ copyRunReviewState: StorageRunActions.copyRunReviewState,
1377
+ reviewStateSnapshot: StorageRunActions.reviewStateSnapshot,
1346
1378
  buildTurnRecord: buildTurnRecord,
1347
1379
  buildEventRecord: buildEventRecord,
1348
1380
  buildArtifactRecord: buildArtifactRecord,