codex-overleaf-link 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -1564,15 +1564,6 @@
1564
1564
  };
1565
1565
  }
1566
1566
 
1567
- function fileTreeMethodNames(type) {
1568
- return {
1569
- create: ['createDoc', 'createFile', 'addDoc', 'addFile'],
1570
- rename: ['renameEntity', 'renameFile', 'renameDoc'],
1571
- move: ['moveEntity', 'moveFile', 'moveDoc'],
1572
- delete: ['deleteEntity', 'deleteFile', 'deleteDoc', 'removeEntity']
1573
- }[type] || [];
1574
- }
1575
-
1576
1567
  function resolveTimeoutMs(options = {}, fallback) {
1577
1568
  const value = Number(options.timeoutMs);
1578
1569
  return Number.isFinite(value) && value >= 0 ? value : fallback;
@@ -1615,5 +1606,11 @@
1615
1606
  return text;
1616
1607
  }
1617
1608
 
1618
- return { create };
1609
+ function fileTreeMethodNames(type) {
1610
+ return {
1611
+ create: ['createDoc', 'createFile', 'addDoc', 'addFile'], rename: ['renameEntity', 'renameFile', 'renameDoc'],
1612
+ move: ['moveEntity', 'moveFile', 'moveDoc'], delete: ['deleteEntity', 'deleteFile', 'deleteDoc', 'removeEntity']
1613
+ }[type] || [];
1614
+ }
1615
+ return { create, fileTreeMethodNames };
1619
1616
  });
@@ -0,0 +1,41 @@
1
+ (function installUpdateSafetyProbe(root) {
2
+ 'use strict';
3
+ if (root.CodexOverleafUpdateSafetyProbe?.revision === '1') return;
4
+ const identities = new WeakMap();
5
+ function identity(value) {
6
+ if (!identities.has(value)) identities.set(value, crypto.randomUUID());
7
+ return identities.get(value);
8
+ }
9
+ // The same full CodeMirror document sources used by pageBridge. No DOM
10
+ // viewport text is eligible for the quiet-save fallback.
11
+ function editorView() {
12
+ const store = root.overleaf?.unstable?.store || root.Overleaf?.unstable?.store;
13
+ const get = key => { try { return store?.get?.(key) || {}; } catch (_) { return {}; } };
14
+ return [get('editor.view'), get('editor.view.camel'), get('editor').view,
15
+ root.overleaf?.unstable?.editorView, root.Overleaf?.editorView, root._ide?.editorView]
16
+ .find(value => value && typeof value.dispatch === 'function' && value.state?.doc
17
+ && typeof value.state.doc.toString === 'function') || null;
18
+ }
19
+ function fileIdentity() {
20
+ const store = root.overleaf?.unstable?.store || root.Overleaf?.unstable?.store;
21
+ const get = key => { try { return store?.get?.(key); } catch (_) { return ''; } };
22
+ const selected = document.querySelector('[role="treeitem"][aria-selected="true"] [data-file-id]');
23
+ return [get('editor.open_doc_id'), get('editor.openDocId'), root._ide?.editor?.open_doc_id,
24
+ root._ide?.editor?.openDocId, selected?.getAttribute('data-file-id'),
25
+ get('editor.open_doc_name'), get('editor.openDocName')].find(value => typeof value === 'string' && value) || '';
26
+ }
27
+ async function probe(expectedUrl) {
28
+ const view = editorView(), doc = view?.state?.doc, file = fileIdentity();
29
+ if (location.href !== expectedUrl || !file || !root.CodexOverleafSaveState) return { ok: false, state: 'unavailable' };
30
+ const save = root.CodexOverleafSaveState.create({ window: root, document,
31
+ delay: ms => new Promise(resolve => setTimeout(resolve, ms)),
32
+ detectEditor: () => ({ ok: Boolean(view && editorView() === view && view.state.doc === doc) }),
33
+ getActiveFilePath: () => doc ? identity(doc) : '', readActiveEditorText: () => view?.state.doc.toString() || '' });
34
+ const result = await save.waitForSaveState({ deadlineMs: 1500, pollIntervalMs: 100,
35
+ requirePositiveSignal: true, allowQuietEditorFallback: Boolean(doc), quietFallbackMs: 1000 });
36
+ if (location.href !== expectedUrl || fileIdentity() !== file || editorView() !== view
37
+ || (view && view.state.doc !== doc)) return { ok: false, state: 'unavailable' };
38
+ return { ok: result.ok === true, state: result.state, identity: file + ':' + (doc ? identity(doc) : 'positive-save-signal') };
39
+ }
40
+ root.CodexOverleafUpdateSafetyProbe = Object.freeze({ revision: '1', probe });
41
+ })(globalThis);
@@ -87,8 +87,10 @@
87
87
  return applyOperations(params.operations || [], {
88
88
  baseFiles: params.baseFiles || null,
89
89
  reviewingPolicy: params.reviewingPolicy || '',
90
+ ...(params.reviewOwnerToken ? { reviewOwnerToken: params.reviewOwnerToken } : {}),
90
91
  requireReviewing: params.requireReviewing === true,
91
92
  requireEditing: params.requireEditing === true,
93
+ retryCreates: params.retryCreates === true,
92
94
  runProjectId: projectId,
93
95
  ...(params.deadlineAt == null ? {} : { deadlineAt: params.deadlineAt }),
94
96
  onOperationResult
@@ -1,11 +1,12 @@
1
1
  (function initCodexOverleafWritebackRouter(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('./treeOperations'));
4
4
  } else {
5
- root.CodexOverleafWritebackRouter = factory();
5
+ root.CodexOverleafWritebackRouter = factory(root.CodexOverleafTreeOperations);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function writebackRouterFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function writebackRouterFactory(TreeOperations) {
8
8
  'use strict';
9
+ const { fileTreeMethodNames } = TreeOperations;
9
10
 
10
11
  function create(deps = {}) {
11
12
  const window = deps.window || (typeof globalThis !== 'undefined' ? globalThis : {});
@@ -358,18 +359,20 @@
358
359
 
359
360
  async function applyOperations(operations, options = {}) {
360
361
  if (options.reviewingPolicy === 'no-trace-undo') {
361
- return applyOperationsWithNoTraceUndo(operations, options);
362
+ const Context = window.CodexOverleafReviewContext || (typeof module === 'object' && module.exports ? require('./reviewContext') : null);
363
+ return Context.create({ window, now, readWriteCancellationSequence, getProjectId: () => treeOperations.getProjectId?.() }, options).run(
364
+ (scoped, input) => applyOperationsWithNoTraceUndo(operations, { ...input, reviewGuard: scoped.reviewGuard, reviewAwait: scoped.reviewAwait }));
362
365
  }
363
366
  const hasOperations = (operations || []).length > 0;
364
367
  const trackReviewingChanges = options.requireReviewing === true && hasOperations;
365
368
  let reviewingPolicy = null;
366
369
  if (options.requireReviewing === true && hasOperations) {
367
- const reviewing = await ensureReviewing({ waitMs: 1800 });
370
+ const reviewing = await ensureReviewing({ waitMs: 1800, runProjectId: options.runProjectId });
368
371
  if (!reviewing.ok) {
369
372
  return buildReviewingRequiredBlockedResult(operations, reviewing);
370
373
  }
371
374
  } else if (options.requireEditing === true && hasOperations) {
372
- const editing = await ensureEditing({ waitMs: 1800 });
375
+ const editing = await ensureEditing({ waitMs: 1800, runProjectId: options.runProjectId });
373
376
  if (!editing.ok) {
374
377
  return buildEditingRequiredBlockedResult(operations, editing);
375
378
  }
@@ -388,6 +391,7 @@
388
391
  }
389
392
  const result = await applyOperationsCore(operations, {
390
393
  ...options,
394
+ noTraceUndo: false,
391
395
  trackReviewingChanges
392
396
  });
393
397
  return reviewingPolicy
@@ -398,10 +402,12 @@
398
402
  : result;
399
403
  }
400
404
 
401
- async function confirmNoTraceUndoEditing() {
405
+ async function confirmNoTraceUndoEditing(options = {}) {
402
406
  let changed = false;
403
407
  let toggle = { ok: true, changed: false, enabled: false };
404
408
  for (let attempt = 0; attempt < 2; attempt++) {
409
+ const blocked = options.reviewGuard?.();
410
+ if (blocked) return blocked;
405
411
  if (!isEditingConfirmedForNoTraceUndo(getReviewingState({}))) {
406
412
  toggle = await setReviewingEnabled(false, { waitMs: 1800 });
407
413
  if (!toggle.ok && (!['editing_not_confirmed', 'reviewing_disable_failed'].includes(toggle.code)
@@ -418,8 +424,13 @@
418
424
  reason: 'Overleaf Editing mode did not remain confirmed before undo; no undo write was started.' };
419
425
  }
420
426
 
427
+ async function awaitReview(options, work) {
428
+ try { return options.reviewAwait ? await options.reviewAwait(work) : await work(); }
429
+ catch (error) { if (error?.reviewFailure) return error.reviewFailure; throw error; }
430
+ }
431
+
421
432
  async function applyOperationsWithNoTraceUndo(operations, options = {}) {
422
- const disabled = await confirmNoTraceUndoEditing();
433
+ const disabled = await awaitReview(options, () => confirmNoTraceUndoEditing(options));
423
434
  if (!disabled.ok) return buildNoTraceUndoBlockedResult(operations, disabled);
424
435
  const result = await applyOperationsCore(operations, {
425
436
  ...options, noTraceUndo: true, trackReviewingChanges: false
@@ -441,13 +452,7 @@
441
452
  const safeBaseFiles = normalizeBaseFilesForSafety(options.baseFiles);
442
453
  const baseFileLookup = window.CodexOverleafStaleGuard?.buildBaseFileLookup(safeBaseFiles);
443
454
  const baseBinaryFileLookup = buildBaseBinaryFileLookup(safeBaseFiles);
444
- // Mid-run project-change guard. The pageBridge wrapper already ran the
445
- // writeGuard once at entry, but each per-op step here can take 0.5-5s
446
- // (openFileByPath, waitForActiveEditorText, save-state verification),
447
- // so a multi-op write can span 10-30 seconds total. If the user SPA-
448
- // navigates to a different project mid-flight, the remaining operations
449
- // would land in the wrong project. Re-check the editor project before
450
- // each op and short-circuit the rest of the queue when it diverges.
455
+ // Recheck project identity between awaited operations and before each write.
451
456
  const runProjectId = typeof options.runProjectId === 'string' ? options.runProjectId : '';
452
457
  const writeGuardSurface = window.CodexOverleafWriteGuard?.create({
453
458
  window,
@@ -478,13 +483,17 @@
478
483
  ? await deps.beginTextCreateBatch(createCandidates, { isCurrent: () =>
479
484
  treeOperations.getProjectId?.() === runProjectId && readWriteCancellationSequence() === cancelBaselineSequence }) : null;
480
485
  const deferredCreates = [];
486
+ let haltRemainingOperations = false;
487
+ const skipTail = (operation, result) => operations.slice(operations.indexOf(operation) + 1)
488
+ .forEach(tail => skipped.push({ operation: normalizeOperationPaths(tail), result }));
481
489
  const reopenQueue = [];
482
490
  const deadlineFailure = () => Number.isFinite(options.deadlineAt) && now() >= options.deadlineAt
483
491
  ? { ok: false, code: 'writeback_deadline_exceeded', changedDocument: false,
484
492
  reason: 'The undo time budget expired before this operation could start.' } : null;
485
- const editOptions = () => ({ baseFileLookup, runProjectId,
493
+ const editOptions = () => ({ baseFileLookup, runProjectId, deadlineAt: options.deadlineAt,
494
+ reviewGuard: options.reviewGuard, reviewAwait: options.reviewAwait,
486
495
  recheckWriteProject: writeGuardSurface ? () => {
487
- const expired = deadlineFailure();
496
+ const expired = options.reviewGuard?.() || deadlineFailure();
488
497
  return expired ? { applied: [], skipped: [{ operation: {}, result: expired }] }
489
498
  : writeGuardSurface.runWriteGuard({ runProjectId });
490
499
  } : null,
@@ -499,13 +508,15 @@
499
508
  };
500
509
 
501
510
  for (const rawOperation of operations) {
511
+ const reviewBlock = options.reviewGuard?.();
512
+ if (reviewBlock) {
513
+ skipped.push({ operation: normalizeOperationPaths(rawOperation), result: reviewBlock });
514
+ skipTail(rawOperation, reviewBlock); break;
515
+ }
502
516
  // Stop undispatched work when cancellation arrives during an awaited operation.
503
517
  if (readWriteCancellationSequence() !== cancelBaselineSequence) {
504
518
  skipped.push({ operation: normalizeOperationPaths(rawOperation), result: cancelledSkipResult });
505
- const remainingForCancel = operations.slice(operations.indexOf(rawOperation) + 1);
506
- for (const tailOperation of remainingForCancel) {
507
- skipped.push({ operation: normalizeOperationPaths(tailOperation), result: cancelledSkipResult });
508
- }
519
+ skipTail(rawOperation, cancelledSkipResult);
509
520
  break;
510
521
  }
511
522
  // Re-check project ownership before this operation and preserve every skipped tail entry.
@@ -515,10 +526,7 @@
515
526
  // Keep the current path and the remaining operations attributable.
516
527
  const guardFailure = guardBlock.skipped[0].result;
517
528
  skipped.push({ operation: normalizeOperationPaths(rawOperation), result: guardFailure });
518
- const remaining = operations.slice(operations.indexOf(rawOperation) + 1);
519
- for (const tailOperation of remaining) {
520
- skipped.push({ operation: normalizeOperationPaths(tailOperation), result: guardFailure });
521
- }
529
+ skipTail(rawOperation, guardFailure);
522
530
  break;
523
531
  }
524
532
  }
@@ -538,7 +546,7 @@
538
546
  } else if (['binary-create', 'overwrite-binary'].includes(operation.type)) {
539
547
  result = await applyBinaryAssetOperation(operation, { baseFileLookup, baseBinaryFileLookup });
540
548
  } else if (['create', 'rename', 'move', 'delete'].includes(operation.type)) {
541
- result = await applyFileTreeOperation(operation, { baseFileLookup, runProjectId, deadlineAt: options.deadlineAt,
549
+ result = await applyFileTreeOperation(operation, { baseFileLookup, runProjectId, deadlineAt: options.deadlineAt, reviewGuard: options.reviewGuard, noTraceUndo: options.noTraceUndo === true,
542
550
  createBatch: createBatch?.targets.has(operation.path) ? createBatch : null,
543
551
  retryCreates: options.retryCreates === true });
544
552
  } else {
@@ -557,16 +565,21 @@
557
565
  continue;
558
566
  }
559
567
  record(operation, result);
568
+ if (operation.type === 'move' && operation.moveProof && !result.ok) {
569
+ haltRemainingOperations = true;
570
+ skipTail(rawOperation, { ok: false, code: 'file_plan_not_completed', changedDocument: false,
571
+ reason: 'The file move did not finish; the remaining file plan was not started.' }); break;
572
+ }
560
573
  }
561
574
 
562
575
  // A file that could not be opened mid-batch usually opens once the
563
576
  // previous document has settled: give untouched open failures one pass.
564
- if (reopenQueue.length && readWriteCancellationSequence() === cancelBaselineSequence
577
+ if (!haltRemainingOperations && reopenQueue.length && readWriteCancellationSequence() === cancelBaselineSequence
565
578
  && !skipped.some(entry => entry.result?.code === 'aborted_project_changed')) {
566
579
  await delay(deps.reopenRetryDelayMs ?? 1200);
567
580
  }
568
581
  for (const entry of reopenQueue) {
569
- const blocked = readWriteCancellationSequence() !== cancelBaselineSequence
582
+ const blocked = options.reviewGuard?.() || haltRemainingOperations || readWriteCancellationSequence() !== cancelBaselineSequence
570
583
  || skipped.some(item => item.result?.code === 'aborted_project_changed')
571
584
  || Boolean(runProjectId && writeGuardSurface && await writeGuardSurface.runWriteGuard({ runProjectId }));
572
585
  const result = blocked ? entry.result : await applyEditOperation(entry.operation, editOptions());
@@ -589,25 +602,26 @@
589
602
  ok: skipped.length === 0,
590
603
  applied,
591
604
  skipped,
605
+ ...(haltRemainingOperations ? { haltRemainingOperations: true } : {}),
592
606
  trackedChanges: trackedChangeCaptures.some(capture => capture.state !== 'observed') ? [] : mergeTrackedChangeRefs(trackedChanges),
593
607
  trackedChangeCaptures
594
608
  };
595
609
  }
596
610
 
597
- function isUntouchedOpenFailure(result) {
598
- return result?.ok === false && result.failure?.code === 'target_file_open_failed'
599
- && result.failure.changedDocument === false;
600
- }
611
+ const isUntouchedOpenFailure = result => result?.ok === false && result.failure?.code === 'target_file_open_failed'
612
+ && result.failure.changedDocument === false;
601
613
 
602
614
  async function applyEditOperation(operation, options = {}) {
615
+ const reviewBlock = options.reviewGuard?.();
616
+ if (reviewBlock) return reviewBlock;
603
617
  const currentPath = getActiveFilePath();
604
- const editorReady = await ensureEditorReadyForOperation(operation, options.baseFileLookup, currentPath);
618
+ const editorReady = await awaitReview(options, () => ensureEditorReadyForOperation(operation, options.baseFileLookup, currentPath));
605
619
  if (!editorReady.ok) {
606
620
  return editorReady;
607
621
  }
608
622
 
609
623
  if (options.noTraceUndo === true) {
610
- const editing = await confirmNoTraceUndoEditing();
624
+ const editing = await awaitReview(options, () => confirmNoTraceUndoEditing(options));
611
625
  if (!editing.ok) return buildNoTraceUndoBlockedResult([operation], editing).skipped[0].result;
612
626
  }
613
627
  const trackReviewingChanges = options.trackReviewingChanges === true;
@@ -625,7 +639,8 @@
625
639
  options.baseFileLookup
626
640
  ) || { ok: true };
627
641
  if (!freshness.ok) {
628
- const ready = await waitForFreshEditorTextForOperation(operation, options.baseFileLookup, 1500);
642
+ const ready = await awaitReview(options, () => waitForFreshEditorTextForOperation(operation, options.baseFileLookup, 1500));
643
+ if (options.reviewGuard?.()) return options.reviewGuard();
629
644
  if (!ready.ok) {
630
645
  const decorated = attachWritebackDebug(freshness, 'stale_guard', operation, options.baseFileLookup, current, {
631
646
  initialActivePath: currentPath,
@@ -661,12 +676,15 @@
661
676
  return decorated;
662
677
  }
663
678
  current = ready.text;
664
- freshness = { ok: true };
679
+ freshness = ready.freshness;
665
680
  }
666
681
 
667
682
  let nextContent = null;
683
+ let preparedPatches;
668
684
  if (Array.isArray(operation.patches) && operation.patches.length) {
669
- const patched = applyTextPatches(current, operation.patches);
685
+ preparedPatches = freshness.preparedPatches || window.CodexOverleafStaleGuard?.prepareOperationPatches(operation, current, options.baseFileLookup)
686
+ || { ok: true, patches: operation.patches };
687
+ const patched = preparedPatches.ok ? applyTextPatches(current, preparedPatches.patches) : preparedPatches;
670
688
  if (!patched.ok) {
671
689
  const decorated = attachWritebackDebug(patched, 'apply_text_patches', operation, options.baseFileLookup, current, {
672
690
  initialActivePath: currentPath,
@@ -718,25 +736,31 @@
718
736
  operationType: 'undo', changedDocument: false,
719
737
  userMessage: 'The project identity guard is unavailable; the undo write was not applied.' }) };
720
738
  if (blocked) return blocked.skipped?.[0]?.result || blocked;
721
- if (getActiveFilePath() !== operation.path || readActiveEditorText() !== current) {
722
- const reason = 'The editor changed while confirming Editing mode; the undo write was not applied.';
723
- return { ok: false, code: 'stale_source_changed', reason,
724
- failure: buildPageFailure('stale_source_changed', { file: operation.path,
725
- operationType: 'undo', changedDocument: false, userMessage: reason }) };
726
- }
727
739
  if (!isEditingConfirmedForNoTraceUndo(getReviewingState({})))
728
740
  return buildNoTraceUndoBlockedResult([operation], { ok: false, code: 'editing_not_confirmed',
729
741
  reason: 'Overleaf left Editing mode before the undo write.' }).skipped[0].result;
730
742
  }
731
743
 
744
+ const finalReviewBlock = options.reviewGuard?.();
745
+ if (finalReviewBlock) return finalReviewBlock;
746
+ // Read and dispatch synchronously against the same document. Navigation,
747
+ // freshness polling and Undo mode/project checks can all have awaited.
748
+ if (getActiveFilePath() !== operation.path || readActiveEditorText() !== current) {
749
+ const reason = 'The editor changed before the edit could be applied; the document was not overwritten.';
750
+ return { ok: false, code: 'stale_source_changed', reason,
751
+ failure: buildPageFailure('stale_source_changed', { file: operation.path,
752
+ operationType: options.noTraceUndo ? 'undo' : 'edit', changedDocument: false, userMessage: reason }) };
753
+ }
732
754
  const result = Array.isArray(operation.patches) && operation.patches.length
733
- ? replaceActiveEditorPatches(operation.patches, nextContent)
755
+ ? replaceActiveEditorPatches(preparedPatches.patches, nextContent)
734
756
  : replaceActiveEditorText(nextContent);
735
757
  if (!result.ok) {
736
758
  return result;
737
759
  }
738
760
 
739
- const verified = await verifyActiveEditorText(nextContent, operation.path, deps.writeVerifyWaitMs ?? 2500, current);
761
+ const verifyWaitMs = Math.min(deps.writeVerifyWaitMs ?? 2500, Number.isFinite(options.deadlineAt)
762
+ ? Math.max(0, options.deadlineAt - now()) : Infinity);
763
+ const verified = await verifyActiveEditorText(nextContent, operation.path, verifyWaitMs, current);
740
764
  if (!verified.ok) {
741
765
  const decorated = attachWritebackDebug(verified, 'write_verification', operation, options.baseFileLookup, readActiveEditorText(), {
742
766
  initialActivePath: currentPath,
@@ -776,7 +800,7 @@
776
800
  // Slow batches are covered by request receipts; Accept replay retains
777
801
  // its independent safety window proving that no new tracking was added.
778
802
  const trackedDiff = trackReviewingChanges
779
- ? await captureTrackedWrite({ trackedBefore, captureBaseline, operation, beforeContent: current,
803
+ ? await captureTrackedWrite({ trackedBefore, captureBaseline, operation: { ...operation, patches: preparedPatches?.patches }, beforeContent: current,
780
804
  postContent: nextContent, runProjectId: options.runProjectId, waitMs: deps.trackCaptureWaitMs ?? 35000 })
781
805
  : await waitForTrackedChangeDiff(trackedBefore, operationPaths, {
782
806
  waitMs: 3600, intervalMs: 180, stopOnFirst: true
@@ -805,6 +829,7 @@
805
829
  }),
806
830
  verified: true,
807
831
  verifiedContent: nextContent,
832
+ ...(preparedPatches ? { appliedPatches: preparedPatches.patches } : {}),
808
833
  trackedChanges: mergeTrackedChangeRefs(trackedChanges)
809
834
  };
810
835
  }
@@ -812,18 +837,18 @@
812
837
  window.CodexOverleafStaleGuard?.updateExpectedFileContent(
813
838
  options.baseFileLookup,
814
839
  operation.path,
815
- nextContent
840
+ preparedPatches?.sourceContent ?? nextContent
816
841
  );
817
842
  return {
818
843
  ...result,
819
844
  verified: true,
820
845
  verifiedContent: nextContent,
846
+ ...(preparedPatches ? { appliedPatches: preparedPatches.patches } : {}),
821
847
  trackedChangeCapture,
822
848
  trackedChanges: mergeTrackedChangeRefs(trackedChanges)
823
849
  };
824
850
  }
825
851
 
826
-
827
852
  async function verifyActiveEditorText(expected, filePath, waitMs = 1000, before) {
828
853
  const deadline = Date.now() + waitMs;
829
854
  let actual = readActiveEditorText();
@@ -949,35 +974,17 @@
949
974
  lastText = readActiveEditorText();
950
975
  const activeFileMatches = lastActivePath === filePath;
951
976
  const identityChanged = Boolean(previousEditorIdentity) && activeEditorIdentityChanged(previousEditorIdentity);
952
- const baseComparison = compareEditorTextToBase(filePath, lastText, baseFileLookup);
953
- const baseMatches = baseComparison.matches;
954
977
  const settledLongEnough = Date.now() - startedAt >= minSettleMs;
955
978
  const contentChangedFromPrevious = !options.previousSignature
956
979
  || contentSignature(lastText) !== options.previousSignature;
957
- // Switch confirmation must not depend on any single fragile signal
958
- // (v1.6: identity comparison proved unreliable against newer Overleaf
959
- // view lifecycles, which made every write to a non-active file time
960
- // out as target_editor_not_ready). Acceptors, strongest first:
961
- // 1. editor identity visibly changed (fast path when it works);
962
- // 2. the editor shows EXACTLY the target file's expected base —
963
- // the strongest possible proof the right document is loaded;
964
- // 3. same-file reopen settled with unchanged content;
965
- // 4. base UNKNOWN only: the content moved off the previous document
966
- // and the settle window elapsed. When the base IS known, content
967
- // must match it (acceptor 2) — a weakly-anchored patch (e.g. an
968
- // insert into an empty file, expected:'') would otherwise anchor
969
- // trivially into a wrong-but-different document.
980
+ // Confirm navigation independently of source freshness. A stale target
981
+ // can be correctly open; identical baseline bytes alone cannot prove a
982
+ // switch. Content movement remains the fallback for editor lifecycles
983
+ // whose document identity cannot be observed reliably.
970
984
  const switchConfirmed = identityChanged
971
- || baseMatches
972
- || (options.initialActivePath === filePath
973
- && options.previousSignature
974
- && contentSignature(lastText) === options.previousSignature
975
- && settledLongEnough)
976
- || (!baseComparison.known && contentChangedFromPrevious && settledLongEnough);
977
- const contentNoLongerLooksLikePreviousDocument = contentChangedFromPrevious
978
- || baseMatches
979
- || options.initialActivePath === filePath;
980
- if (activeFileMatches && switchConfirmed && settledLongEnough && contentNoLongerLooksLikePreviousDocument) {
985
+ || options.initialActivePath === filePath
986
+ || (contentChangedFromPrevious && settledLongEnough);
987
+ if (activeFileMatches && switchConfirmed && settledLongEnough) {
981
988
  return {
982
989
  ok: true,
983
990
  text: lastText
@@ -1053,47 +1060,6 @@
1053
1060
  };
1054
1061
  }
1055
1062
 
1056
- function editorContentMatchesBase(filePath, content, baseFileLookup) {
1057
- return compareEditorTextToBase(filePath, content, baseFileLookup).matches;
1058
- }
1059
-
1060
- function compareEditorTextToBase(filePath, content, baseFileLookup) {
1061
- const normalizedPath = normalizeSafeProjectPath(filePath);
1062
- const current = normalizeEditorTextForComparison(content);
1063
- if (!baseFileLookup || !normalizedPath || !baseFileLookup.has(normalizedPath)) {
1064
- return {
1065
- known: false,
1066
- hasContent: false,
1067
- matches: false,
1068
- baseLength: null,
1069
- currentLength: current.length
1070
- };
1071
- }
1072
- const base = normalizeEditorTextForComparison(baseFileLookup.get(normalizedPath));
1073
- const matches = current === base;
1074
- if (!matches && base.trim().length > 0) {
1075
- console.warn('[codex-overleaf] editorContentMatchesBase MISMATCH', filePath,
1076
- 'base.length:', base.length, 'current.length:', current.length,
1077
- 'base[0..80]:', JSON.stringify(base.slice(0, 80)),
1078
- 'current[0..80]:', JSON.stringify(current.slice(0, 80)));
1079
- }
1080
- return {
1081
- known: true,
1082
- hasContent: base.trim().length > 0,
1083
- matches,
1084
- baseLength: base.length,
1085
- currentLength: current.length
1086
- };
1087
- }
1088
-
1089
- function hasBaseFileContent(filePath, baseFileLookup) {
1090
- if (!baseFileLookup || !filePath || !baseFileLookup.has(filePath)) {
1091
- return false;
1092
- }
1093
- const content = baseFileLookup.get(filePath);
1094
- return typeof content === 'string' && content.trim().length > 0;
1095
- }
1096
-
1097
1063
  function normalizeEditorTextForComparison(value) {
1098
1064
  const normalized = String(value ?? '').replace(/\r\n/g, '\n');
1099
1065
  return normalized.trim().length === 0 ? '' : normalized;
@@ -1115,7 +1081,8 @@
1115
1081
  if (freshness.ok) {
1116
1082
  return {
1117
1083
  ok: true,
1118
- text
1084
+ text,
1085
+ freshness
1119
1086
  };
1120
1087
  }
1121
1088
  await delay(60);
@@ -1149,7 +1116,6 @@
1149
1116
  };
1150
1117
  }
1151
1118
 
1152
-
1153
1119
  async function applyBinaryAssetOperation(operation, options = {}) {
1154
1120
  const freshness = await checkBinaryAssetFreshness(operation, options);
1155
1121
  if (!freshness.ok) {
@@ -1319,9 +1285,7 @@
1319
1285
  return baseFile.size == null || currentSize == null || baseFile.size === currentSize;
1320
1286
  }
1321
1287
 
1322
- function normalizeBase64(value) {
1323
- return String(value || '').replace(/\s+/g, '');
1324
- }
1288
+ const normalizeBase64 = value => String(value || '').replace(/\s+/g, '');
1325
1289
 
1326
1290
  function decodeBase64Bytes(contentBase64) {
1327
1291
  try {
@@ -1372,6 +1336,17 @@
1372
1336
  }
1373
1337
  const initialProjectId = treeOperations.getProjectId?.();
1374
1338
  const initialCancellationSequence = readWriteCancellationSequence();
1339
+ if (operation.type === 'move' && operation.moveProof) {
1340
+ if (options.noTraceUndo !== true) return { ok: false, code: 'file_tree_controls_unavailable', changedDocument: false,
1341
+ reason: 'Native file moves are available only to undo a previously recorded move.' };
1342
+ if (typeof deps.moveFile !== 'function') return { ok: false, code: 'file_move_unavailable', changedDocument: false,
1343
+ reason: 'Verified native file moves are unavailable. Reload the extension and Overleaf.' };
1344
+ const result = await deps.moveFile(operation, { noTraceUndo: true, runProjectId: options.runProjectId, deadlineAt: options.deadlineAt,
1345
+ isCurrent: () => treeOperations.getProjectId?.() === initialProjectId
1346
+ && readWriteCancellationSequence() === initialCancellationSequence && !options.reviewGuard?.() });
1347
+ if (result.ok) recordFileTreeOperationSuccess(operation, options.baseFileLookup);
1348
+ return result;
1349
+ }
1375
1350
  if (operation.type === 'create' && options.createBatch && typeof deps.createTextFile === 'function') {
1376
1351
  if (options.baseFileLookup?.has(operation.path) && options.retryCreates !== true) {
1377
1352
  return checkFileTreeOperationFreshness(operation, options.baseFileLookup);
@@ -1379,13 +1354,14 @@
1379
1354
  return deps.createTextFile(operation, { batch: options.createBatch,
1380
1355
  allowExistingMatches: options.retryCreates === true,
1381
1356
  isCurrent: () => treeOperations.getProjectId?.() === initialProjectId
1382
- && readWriteCancellationSequence() === initialCancellationSequence });
1357
+ && readWriteCancellationSequence() === initialCancellationSequence && !options.reviewGuard?.() });
1383
1358
  }
1384
1359
  const freshness = await checkFileTreeOperationFreshness(operation, options.baseFileLookup);
1385
1360
  if (!freshness.ok) {
1386
1361
  return freshness;
1387
1362
  }
1388
1363
 
1364
+ const ownerBlocked = options.reviewGuard?.(); if (ownerBlocked) return ownerBlocked;
1389
1365
  const manager = findFileTreeManager();
1390
1366
  const methodNames = fileTreeMethodNames(operation.type);
1391
1367
 
@@ -1432,7 +1408,7 @@
1432
1408
  const nativeOperation = { create: deps.createTextFile, delete: deps.deleteTextFile }[operation.type];
1433
1409
  if (typeof nativeOperation === 'function' && (operation.type === 'delete' || typeof operation.content === 'string')) {
1434
1410
  const isCurrent = () => Boolean(initialProjectId) && treeOperations.getProjectId?.() === initialProjectId
1435
- && readWriteCancellationSequence() === initialCancellationSequence;
1411
+ && readWriteCancellationSequence() === initialCancellationSequence && !options.reviewGuard?.();
1436
1412
  const created = await nativeOperation(operation, { isCurrent, expectedContent: options.baseFileLookup?.get(operation.path) });
1437
1413
  if (!created.ok) return created;
1438
1414
  if (!isCurrent() || created.verified !== true || created.verification !== 'overleaf-zip') {
@@ -1456,7 +1432,7 @@
1456
1432
  const text = proof?.v === 1 && proof.kind === 'text' && typeof expected === 'string';
1457
1433
  const binary = proof?.v === 1 && proof.kind === 'binary' && /^[a-f0-9]{64}$/i.test(proof.sha256 || '');
1458
1434
  const isCurrent = () => Boolean(projectId) && projectId === options.runProjectId
1459
- && treeOperations.getProjectId?.() === projectId && readWriteCancellationSequence() === cancellation;
1435
+ && treeOperations.getProjectId?.() === projectId && readWriteCancellationSequence() === cancellation && !options.reviewGuard?.();
1460
1436
  if (!isCurrent() || (!text && !binary) || typeof deps.deleteTextFile !== 'function') {
1461
1437
  return { ok: false, code: 'undo_operation_failed', changedDocument: false,
1462
1438
  reason: 'Verified created-file deletion is unavailable for this run.' };
@@ -1653,15 +1629,6 @@
1653
1629
  }
1654
1630
  }
1655
1631
 
1656
- function fileTreeMethodNames(type) {
1657
- return {
1658
- create: ['createDoc', 'createFile', 'addDoc', 'addFile'],
1659
- rename: ['renameEntity', 'renameFile', 'renameDoc'],
1660
- move: ['moveEntity', 'moveFile', 'moveDoc'],
1661
- delete: ['deleteEntity', 'deleteFile', 'deleteDoc', 'removeEntity']
1662
- }[type] || [];
1663
- }
1664
-
1665
1632
  function normalizeOperationPaths(operation) {
1666
1633
  if (!operation || typeof operation !== 'object') {
1667
1634
  return operation;
@@ -1734,10 +1701,6 @@
1734
1701
  }
1735
1702
 
1736
1703
 
1737
-
1738
-
1739
-
1740
-
1741
1704
  function normalizeTrackedChangeRefs(refs = []) {
1742
1705
  const seen = new Set();
1743
1706
  const normalized = [];
@@ -1796,23 +1759,12 @@
1796
1759
 
1797
1760
  async function applyOperationsForBridge(operationsOrOptions, options = {}) {
1798
1761
  if (Array.isArray(operationsOrOptions)) {
1799
- // Welcome-panel + write-guard: defense-in-depth
1800
- // also runs on the array-shaped entry. T2 only guarded the
1801
- // payload-object branch; if any future caller goes straight to the
1802
- // array entry without `options.runProjectId`, the router would have
1803
- // silently dispatched. Mirror the same `editor_project_id_unavailable`
1804
- // failure shape the payload branch and the page-side guard already
1805
- // emit.
1762
+ // Both public call shapes require the original project identity.
1806
1763
  const writeGuardBlock = checkWritebackRunProjectId(options);
1807
1764
  if (writeGuardBlock) return writeGuardBlock;
1808
1765
  return applyOperations(operationsOrOptions, options);
1809
1766
  }
1810
1767
  const payload = operationsOrOptions || {};
1811
- // Welcome-panel + write-guard: defense-in-depth.
1812
- // The pageBridge wrapper already runs the runProjectId guard, but a
1813
- // future caller could reach the router directly. A missing or empty
1814
- // `runProjectId` on the payload-shaped call blocks the write with the
1815
- // same `editor_project_id_unavailable` shape the page-side guard uses.
1816
1768
  const writeGuardBlock = checkWritebackRunProjectId(payload);
1817
1769
  if (writeGuardBlock) return writeGuardBlock;
1818
1770
  return applyOperations(payload.operations || [], payload);
@@ -1821,10 +1773,7 @@
1821
1773
  function checkWritebackRunProjectId(params) {
1822
1774
  const runProjectId = params && typeof params.runProjectId === 'string' ? params.runProjectId : '';
1823
1775
  if (runProjectId) return null;
1824
- // operation: {} (not null) — batch-level guard fired before any per-op
1825
- // dispatch; matches the abortDispatchResult shape in pageBridge.js and
1826
- // avoids crashing downstream audit / transcript code that reads
1827
- // operation.path without a null guard.
1776
+ // Keep operation.path safe for downstream readers of batch-level failures.
1828
1777
  return {
1829
1778
  ok: false,
1830
1779
  applied: [],
@@ -1892,6 +1841,7 @@
1892
1841
  ok: false,
1893
1842
  code: reviewing?.code || 'reviewing_not_enabled',
1894
1843
  reason: reviewing?.reason || 'Overleaf Reviewing/Track Changes was not confirmed before writing. Codex did not change this file.',
1844
+ evidence: reviewing?.evidence, failure: reviewing?.failure,
1895
1845
  reviewing: reviewing?.reviewing || null
1896
1846
  }
1897
1847
  })),
@@ -1916,9 +1866,7 @@
1916
1866
  };
1917
1867
  }
1918
1868
 
1919
- // Tracked-changes lifecycle — carved to trackedChangesLifecycle.js in
1920
- // v1.8.0 (structural-debt phase 7, #117). Loaded just before this file
1921
- // in the manifest's page-script list; the Node test path requires it.
1869
+ // Tracked lifecycle is loaded by the Page script list or required by Node tests.
1922
1870
  const trackedChangesLifecycleModule = (typeof window !== 'undefined' && window.CodexOverleafTrackedChangesLifecycle)
1923
1871
  || (typeof module === 'object' && module.exports ? require('./trackedChangesLifecycle.js') : null)
1924
1872
  || (typeof globalThis !== 'undefined' ? globalThis.CodexOverleafTrackedChangesLifecycle : null);
@@ -1930,7 +1878,7 @@
1930
1878
  captureTrackedWrite, reconcileTrackedChangeCapture,
1931
1879
  waitForTrackedChangeDiff
1932
1880
  } = trackedChangesLifecycleModule.create({
1933
- window, confirmReviewWriteback: deps.confirmReviewWriteback,
1881
+ window, now, readWriteCancellationSequence, confirmReviewWriteback: deps.confirmReviewWriteback,
1934
1882
  checkWritebackRunProjectId,
1935
1883
  buildNoTraceUndoBlockedResult,
1936
1884
  buildReviewingRequiredBlockedResult,