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
@@ -1,13 +1,48 @@
1
1
  (function initUndoOperations(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('./staleGuard'));
4
4
  } else {
5
- root.CodexOverleafUndoOperations = factory();
5
+ root.CodexOverleafUndoOperations = factory(root.CodexOverleafStaleGuard);
6
6
  }
7
- })(typeof globalThis !== 'undefined' ? globalThis : window, function undoOperationsFactory() {
7
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function undoOperationsFactory(StaleGuard) {
8
8
  'use strict';
9
9
 
10
10
  function buildUndoOperations(project, appliedOperations) {
11
+ return buildUndoPlan(project, appliedOperations).undoOperations;
12
+ }
13
+
14
+ function getUndoUnavailableFiles(run = {}) {
15
+ const payload = run.recoveryPayload || run;
16
+ const files = new Map();
17
+ for (const operation of payload.appliedOperations || []) {
18
+ const reason = operation?.undoUnavailable;
19
+ if (typeof reason?.path === 'string' && reason.path === operation?.path
20
+ && ['stale_source_changed', 'snapshot_undo_unavailable'].includes(reason.code)) {
21
+ files.set(reason.path, { path: reason.path, code: reason.code });
22
+ }
23
+ }
24
+ return [...files.values()];
25
+ }
26
+
27
+ function withAvailableUndo(run = {}) {
28
+ const unavailable = new Set(getUndoUnavailableFiles(run).map(item => item.path));
29
+ if (!unavailable.size) return run;
30
+ const result = { ...run };
31
+ for (const field of ['undoOperations', 'undoBaseFiles', 'undoExpectedFiles', 'undoTrackedChanges', 'trackedChangeCaptures']) {
32
+ result[field] = (run[field] || []).filter(item => !unavailable.has(item?.path) && !unavailable.has(item?.to));
33
+ }
34
+ return result;
35
+ }
36
+
37
+ // Fallback history keeps only bounded reasons, never document bytes or
38
+ // authority to reconstruct a withheld checkpoint.
39
+ function compactUnavailableUndoOperations(run = {}) {
40
+ if (run.forkSnapshot || run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) return [];
41
+ return getUndoUnavailableFiles(run).filter(item => item.path.length <= 1024).slice(0, 100)
42
+ .map(item => ({ type: 'edit', path: item.path, undoUnavailable: item }));
43
+ }
44
+
45
+ function buildUndoPlan(project, appliedOperations) {
11
46
  const workingFilesByPath = new Map();
12
47
  for (const file of project?.files || []) {
13
48
  if (typeof file?.path === 'string' && typeof file.content === 'string') {
@@ -15,7 +50,12 @@
15
50
  }
16
51
  }
17
52
 
53
+ const sourceFilesByPath = new Map(workingFilesByPath);
18
54
  const undo = [];
55
+ const inverseBases = new Map();
56
+ const mappedPaths = new Set((appliedOperations || []).filter(operation => Array.isArray(operation?.appliedPatches))
57
+ .map(operation => operation.path));
58
+ const unavailable = new Map(getUndoUnavailableFiles({ appliedOperations }).map(item => [item.path, item.code]));
19
59
  const fullRestoreEditPaths = new Set();
20
60
 
21
61
  for (const operation of appliedOperations || []) {
@@ -29,22 +69,33 @@
29
69
  }
30
70
  const currentBeforeEdit = workingFilesByPath.get(operation.path);
31
71
  if (!fullRestoreEditPaths.has(operation.path)) {
32
- const inversePatches = buildInverseTextPatches(currentBeforeEdit, operation.patches);
72
+ const hasAppliedPatches = Array.isArray(operation.appliedPatches) && typeof operation.verifiedContent === 'string';
73
+ const inversePatches = hasAppliedPatches
74
+ ? buildVerifiedInversePatches(operation.appliedPatches, operation.verifiedContent)
75
+ : buildInverseTextPatches(sourceFilesByPath.get(operation.path), operation.patches,
76
+ operation.verifiedContent, operation.path);
77
+ let inverse;
33
78
  if (inversePatches.length) {
34
- undo.push(normalizeUndoOperation({
79
+ inverse = normalizeUndoOperation({
35
80
  type: 'edit',
36
81
  path: operation.path,
37
82
  patches: inversePatches,
38
83
  reason: 'Undo edit'
39
- }));
40
- } else {
84
+ });
85
+ } else if (!hasAppliedPatches) {
41
86
  fullRestoreEditPaths.add(operation.path);
42
- undo.push(normalizeUndoOperation({
87
+ inverse = normalizeUndoOperation({
43
88
  type: 'edit',
44
89
  path: operation.path,
45
90
  replaceAll: currentBeforeEdit,
46
91
  reason: 'Undo edit'
47
- }));
92
+ });
93
+ } else {
94
+ unavailable.set(operation.path, 'snapshot_undo_unavailable');
95
+ }
96
+ if (inverse) {
97
+ undo.push(inverse);
98
+ if (typeof operation.verifiedContent === 'string') inverseBases.set(inverse, operation.verifiedContent);
48
99
  }
49
100
  }
50
101
  } else if (operation.type === 'create') {
@@ -70,10 +121,13 @@
70
121
  if (!operation.path || !operation.to) {
71
122
  continue;
72
123
  }
124
+ if (operation.moveProof && !operation.moveProof.targetEntityId) continue;
73
125
  undo.push(normalizeUndoOperation({
74
126
  type: 'move',
75
127
  path: operation.to,
76
128
  to: operation.path,
129
+ ...(operation.moveProof ? { moveProof: { ...operation.moveProof,
130
+ sourceEntityId: operation.moveProof.targetEntityId, targetEntityId: operation.moveProof.sourceEntityId } } : {}),
77
131
  reason: 'Undo move'
78
132
  }));
79
133
  } else if (operation.type === 'delete') {
@@ -89,12 +143,57 @@
89
143
  }
90
144
 
91
145
  applyOperationToFiles(workingFilesByPath, operation);
146
+ applyOperationToFiles(sourceFilesByPath, { ...operation, verifiedContent: undefined });
92
147
  }
93
148
 
94
- return undo.reverse();
149
+ // Each inverse was measured in its own verified post-image. Compose in
150
+ // reverse order against the latest image, preserving changes between
151
+ // entries. Empty inverse insertions must never reuse an older offset.
152
+ const composed = [];
153
+ for (const inverse of undo.reverse()) {
154
+ let operation = inverse;
155
+ if (mappedPaths.has(inverse.path) && inverseBases.has(inverse)) {
156
+ const current = workingFilesByPath.get(inverse.path);
157
+ const base = StaleGuard.buildBaseFileLookup([{ path: inverse.path, content: inverseBases.get(inverse) }]);
158
+ const fresh = StaleGuard.checkOperationFreshness(inverse, current, base);
159
+ const prepared = fresh.ok && inverse.patches
160
+ ? fresh.preparedPatches || StaleGuard.prepareOperationPatches(inverse, current, base) : fresh;
161
+ if (typeof current !== 'string' || !prepared.ok) {
162
+ unavailable.set(inverse.path, 'stale_source_changed');
163
+ continue;
164
+ }
165
+ if (inverse.patches) operation = { ...inverse, patches: prepared.patches };
166
+ }
167
+ composed.push(operation);
168
+ applyOperationToFiles(workingFilesByPath, operation);
169
+ }
170
+ return { undoOperations: composed.filter(operation => !unavailable.has(operation.path)),
171
+ undoUnavailable: [...unavailable].map(([path, code]) => ({ path, code })) };
95
172
  }
96
173
 
97
- function buildInverseTextPatches(textBeforeEdit, patches) {
174
+ // Invert the ranges actually dispatched to C, not the original Native
175
+ // coordinates in B. Verify against the observed post-image before storing
176
+ // recovery; invalid evidence must not fall back to overwriting C with B.
177
+ function buildVerifiedInversePatches(patches, postImage) {
178
+ const beforeLength = patches.reduce((length, patch) =>
179
+ length + Number(patch?.to) - Number(patch?.from) - String(patch?.insert ?? '').length, postImage.length);
180
+ if (!Number.isSafeInteger(beforeLength) || beforeLength < 0) return [];
181
+ const normalized = normalizeTextPatches(patches, beforeLength);
182
+ if (!normalized.ok || normalized.patches.some(patch => patch.expected.length !== patch.to - patch.from)) return [];
183
+ const inverse = invertPatches(normalized.patches);
184
+ return applyTextPatches(postImage, inverse).ok ? inverse : [];
185
+ }
186
+
187
+ function invertPatches(patches) {
188
+ let offset = 0;
189
+ return patches.map(patch => {
190
+ const from = patch.from + offset;
191
+ offset += patch.insert.length - (patch.to - patch.from);
192
+ return { from, to: from + patch.insert.length, expected: patch.insert, insert: patch.expected };
193
+ });
194
+ }
195
+
196
+ function buildInverseTextPatches(textBeforeEdit, patches, verifiedContent, path) {
98
197
  if (!Array.isArray(patches) || !patches.length) {
99
198
  return [];
100
199
  }
@@ -107,26 +206,57 @@
107
206
  return [];
108
207
  }
109
208
 
110
- let offset = 0;
111
- return normalized.patches.map(patch => {
112
- const from = patch.from + offset;
113
- const to = from + patch.insert.length;
114
- offset += patch.insert.length - (patch.to - patch.from);
115
- return {
116
- from,
117
- to,
118
- expected: patch.insert,
119
- insert: patch.expected
120
- };
121
- });
209
+ const inverse = invertPatches(normalized.patches);
210
+ if (typeof verifiedContent !== 'string' || verifiedContent === applied.text) return inverse;
211
+ // The stored checkpoint uses the verified editor post-image. Its inverse
212
+ // must use those same offsets, while Native patches retain their source
213
+ // representation for later operations in the batch.
214
+ const prepared = StaleGuard.prepareStoredInversePatches({ type: 'edit', path, patches: inverse }, verifiedContent,
215
+ StaleGuard.buildBaseFileLookup([{ path, content: applied.text }]));
216
+ return prepared.ok && applyTextPatches(verifiedContent, prepared.patches).ok ? prepared.patches : [];
217
+ }
218
+
219
+ function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = [], previousExpectedFiles = []) {
220
+ const checkpoint = buildUndoCheckpoint(project, operations);
221
+ const unavailable = new Set(checkpoint.undoUnavailable.map(item => item.path));
222
+ const mappedPaths = new Set(operations.filter(operation => Array.isArray(operation?.appliedPatches)).map(operation => operation.path));
223
+ const paths = new Set();
224
+ for (const change of trackedChanges || []) if (change?.path) paths.add(change.path);
225
+ for (const operation of operations || []) {
226
+ if (operation?.path) paths.add(operation.path);
227
+ if (operation?.to) paths.add(operation.to);
228
+ }
229
+ const expectedByPath = new Map();
230
+ for (const file of previousExpectedFiles || []) {
231
+ if (paths.has(file?.path) && !unavailable.has(file.path) && !mappedPaths.has(file.path)
232
+ && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
233
+ expectedByPath.set(file.path, { path: file.path, content: file.content });
234
+ }
235
+ }
236
+ // Reconstruct the pre-image with the same inverse patches as plain Undo.
237
+ // This preserves the editor's unchanged line endings and collaborator
238
+ // content, including when the ZIP baseline used a different representation.
239
+ const hasVerifiedPatches = operations.some(operation => typeof operation?.verifiedContent === 'string'
240
+ && Array.isArray(operation.patches) && operation.patches.length);
241
+ const restored = hasVerifiedPatches ? buildExpectedFilesAfterOperations({ files: checkpoint.undoBaseFiles }, checkpoint.undoOperations) : new Map();
242
+ for (const file of project?.files || []) {
243
+ if (paths.has(file?.path) && !unavailable.has(file.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
244
+ expectedByPath.set(file.path, { path: file.path, content: restored.get(file.path) ?? file.content });
245
+ }
246
+ }
247
+ return Array.from(expectedByPath.values());
122
248
  }
123
249
 
124
250
  function buildUndoCheckpoint(project, appliedOperations) {
125
- const undoOperations = buildUndoOperations(project, appliedOperations);
251
+ const { undoOperations, undoUnavailable } = buildUndoPlan(project, appliedOperations);
252
+ const unavailable = new Map(undoUnavailable.map(item => [item.path, item]));
126
253
  const expectedFiles = buildExpectedFilesAfterOperations(project, appliedOperations);
127
254
  return {
128
255
  undoOperations,
129
- undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations))
256
+ undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations)),
257
+ undoUnavailable,
258
+ appliedOperations: (appliedOperations || []).map(operation => unavailable.has(operation?.path)
259
+ ? { ...operation, undoUnavailable: unavailable.get(operation.path) } : operation)
130
260
  };
131
261
  }
132
262
 
@@ -140,9 +270,9 @@
140
270
  const proof = operation?.undoCreatedFile;
141
271
  if (operation?.type !== 'delete' || proof?.v !== 1) continue;
142
272
  if (proof.kind === 'text' && priorFiles.has(operation.path)) {
143
- created.set(operation.path, { kind: 'text', content: priorFiles.get(operation.path) });
273
+ created.set(operation.path, { kind: 'text', content: priorFiles.get(operation.path), createdDirectories: proof.createdDirectories });
144
274
  } else if (proof.kind === 'binary' && /^[a-f0-9]{64}$/i.test(proof.sha256 || '')) {
145
- created.set(operation.path, { kind: 'binary', sha256: proof.sha256.toLowerCase() });
275
+ created.set(operation.path, { kind: 'binary', sha256: proof.sha256.toLowerCase(), createdDirectories: proof.createdDirectories });
146
276
  }
147
277
  }
148
278
  for (const operation of appliedOperations || []) {
@@ -152,17 +282,17 @@
152
282
  if (originalPaths.has(path) && !created.has(path)) continue;
153
283
  if (operation.type === 'create' && typeof operation.content === 'string') {
154
284
  created.set(path, { kind: 'text', content: typeof operation.verifiedContent === 'string'
155
- ? operation.verifiedContent : operation.content });
285
+ ? operation.verifiedContent : operation.content, createdDirectories: operation.createdDirectories });
156
286
  } else {
157
287
  const hash = operation.sha256 || operation.assetRef?.sha256;
158
288
  if (operation.previousExists !== true && /^[a-f0-9]{64}$/i.test(hash || '')) {
159
- created.set(path, { kind: 'binary', sha256: hash.toLowerCase() });
289
+ created.set(path, { kind: 'binary', sha256: hash.toLowerCase(), createdDirectories: operation.createdDirectories });
160
290
  }
161
291
  }
162
292
  } else if (operation.type === 'edit' && created.get(path)?.kind === 'text') {
163
293
  const files = new Map([[path, created.get(path).content]]);
164
294
  applyOperationToFiles(files, operation);
165
- created.set(path, { kind: 'text', content: files.get(path) });
295
+ created.set(path, { ...created.get(path), kind: 'text', content: files.get(path) });
166
296
  } else if (['delete', 'rename', 'move', 'overwrite-binary'].includes(operation.type)) {
167
297
  // Existing rename/overwrite recovery retains ownership of these cases.
168
298
  created.delete(path);
@@ -173,17 +303,83 @@
173
303
  for (const [path, proof] of Array.from(created).reverse()) {
174
304
  const undoCreatedFile = proof.kind === 'binary'
175
305
  ? { v: 1, kind: 'binary', sha256: proof.sha256 } : { v: 1, kind: 'text' };
306
+ if (Array.isArray(proof.createdDirectories) && proof.createdDirectories.length) {
307
+ undoCreatedFile.createdDirectories = proof.createdDirectories.map(item => ({ ...item }));
308
+ }
176
309
  undoOperations.push({ ...normalizeUndoOperation({ type: 'delete', path, reason: 'Undo create' }), undoCreatedFile });
177
310
  if (proof.kind === 'text') undoBaseFiles.push({ path, content: proof.content });
178
311
  }
312
+ // Reviewing has no text insertion/deletion refs for a native metadata move.
313
+ // Keep its identity-bound inverse with the other file-level proofs.
314
+ undoOperations.push(...buildUndoOperations(project, appliedOperations).filter(operation => operation.type === 'move' && operation.moveProof));
179
315
  return { undoOperations, undoBaseFiles };
180
316
  }
181
317
 
318
+ function isFileUndoOperation(operation) {
319
+ return operation?.undoCreatedFile?.v === 1
320
+ || (operation?.type === 'move' && operation.moveProof?.v === 1)
321
+ || (operation?.type === 'create' && typeof operation.content === 'string');
322
+ }
323
+
324
+ // A deleted original has no editor post-image. Its inverse is a guarded
325
+ // file creation, backed by both the forward delete and the captured original.
326
+ // This also recovers older Track-on records that omitted that inverse.
327
+ function getFileUndoOperations(run = {}) {
328
+ run = withAvailableUndo(run);
329
+ const operations = (run.undoOperations || []).filter(isFileUndoOperation);
330
+ const before = filesListToMap(run.undoExpectedFiles);
331
+ const after = buildExpectedFilesAfterOperations({ files: run.undoExpectedFiles }, run.appliedOperations);
332
+ const deleted = new Set((run.appliedOperations || []).filter(operation => operation?.type === 'delete'
333
+ && before.has(operation.path) && !after.has(operation.path)).map(operation => operation.path));
334
+ return [...operations.filter(operation => !deleted.has(operation.path)), ...Array.from(deleted, path =>
335
+ normalizeUndoOperation({ type: 'create', path, content: before.get(path), reason: 'Undo delete' }))];
336
+ }
337
+
338
+ function prepareRunUndo(run = {}) {
339
+ const original = run;
340
+ run = withAvailableUndo(run);
341
+ const files = getFileUndoOperations(run);
342
+ const restored = new Set(files.filter(operation => operation.type === 'create').map(operation => operation.path));
343
+ const pending = [...files], operations = [];
344
+ for (const operation of run.undoOperations || []) {
345
+ if (isFileUndoOperation(operation)) {
346
+ const index = pending.findIndex(file => file.path === operation.path && file.type === operation.type);
347
+ if (index >= 0) operations.push(...pending.splice(index, 1));
348
+ } else if (!restored.has(operation.path)) operations.push(operation);
349
+ }
350
+ return { ...original, undoOperations: [...operations, ...pending],
351
+ ...(run !== original ? { undoBaseFiles: run.undoBaseFiles, undoExpectedFiles: run.undoExpectedFiles } : {}) };
352
+ }
353
+
354
+ function textUndoRun(run = {}) {
355
+ run = withAvailableUndo(run);
356
+ const restored = new Set(getFileUndoOperations(run).flatMap(operation => operation.type === 'create' ? [operation.path]
357
+ : operation.type === 'move' ? [operation.path, operation.to] : []));
358
+ return { ...run,
359
+ undoExpectedFiles: (run.undoExpectedFiles || []).filter(file => !restored.has(file.path)),
360
+ undoTrackedChanges: (run.undoTrackedChanges || []).filter(ref => !restored.has(ref.path))
361
+ };
362
+ }
363
+
364
+ function mergeFileUndoResult(textResult, fileResult, operations) {
365
+ const applied = [...(textResult.applied || []), ...(fileResult.applied || [])];
366
+ const skipped = [...(textResult.skipped || []), ...(fileResult.skipped || [])];
367
+ for (const operation of operations) {
368
+ if (!(fileResult.applied || []).some(entry => entry.operation?.path === operation.path
369
+ && entry.operation?.type === operation.type && entry.result?.ok === true && entry.result?.verified === true
370
+ && entry.result.verification === 'overleaf-zip')
371
+ && !(fileResult.skipped || []).some(entry => entry.operation?.path === operation.path)) {
372
+ skipped.push({ operation, result: { ok: false, code: 'undo_operation_failed',
373
+ reason: 'The file restoration has no verified server receipt.' } });
374
+ }
375
+ }
376
+ return { ...textResult, ok: textResult.ok !== false && fileResult.ok !== false && !skipped.length, applied, skipped };
377
+ }
378
+
182
379
  // Consume only independently verified rollback receipts. Forward writes
183
380
  // remain audit evidence; recovery arrays describe only unfinished work.
184
381
  function advanceUndoCheckpoint(run = {}, result = {}, verifyText = () => false) {
185
382
  const operations = Array.isArray(run?.undoOperations) ? run.undoOperations : [];
186
- if (!operations.some(operation => operation?.undoCreatedFile?.v === 1)) return run;
187
383
  const applied = Array.isArray(result.applied) ? result.applied : [];
188
384
  const skipped = Array.isArray(result.skipped) ? result.skipped : [];
189
385
  const failed = path => skipped.some(entry => {
@@ -201,8 +397,22 @@
201
397
  && JSON.stringify(entry.operation.undoCreatedFile) === JSON.stringify(operation.undoCreatedFile)
202
398
  && entry.result?.ok === true && entry.result.verified === true
203
399
  && entry.result.verification === 'overleaf-zip')).map(operation => operation.path));
400
+ const restoredPaths = new Set(operations.filter(operation => operation?.type === 'create'
401
+ && !failed(operation.path) && applied.some(entry => entry?.operation?.type === 'create'
402
+ && entry.operation.path === operation.path && entry.operation.content === operation.content
403
+ && entry.result?.ok === true && entry.result.verified === true
404
+ && entry.result.verification === 'overleaf-zip')).map(operation => operation.path));
405
+ const movedPaths = new Set(operations.filter(operation => operation?.type === 'move' && operation.moveProof?.v === 1
406
+ && !failed(operation.path) && applied.some(entry => entry?.operation?.type === 'move'
407
+ && entry.operation.path === operation.path && entry.operation.to === operation.to
408
+ && JSON.stringify(entry.operation.moveProof) === JSON.stringify(operation.moveProof)
409
+ && entry.result?.ok === true && entry.result.verified === true && entry.result.verification === 'overleaf-zip'))
410
+ .flatMap(operation => [operation.path, operation.to]));
411
+ for (const path of restoredPaths) textPaths.add(path);
412
+ for (const path of movedPaths) textPaths.add(path);
204
413
  if (!textPaths.size && !deletedPaths.size) return run;
205
- const pending = operations.filter(operation => !(operation?.type === 'edit' && textPaths.has(operation.path))
414
+ const pending = operations.filter(operation => !restoredPaths.has(operation.path) && !(operation?.type === 'edit' && textPaths.has(operation.path))
415
+ && !(operation?.type === 'move' && movedPaths.has(operation.path))
206
416
  && !(operation?.type === 'delete' && operation.undoCreatedFile?.v === 1 && deletedPaths.has(operation.path)));
207
417
  const needed = new Set(pending.flatMap(operation => [operation?.path, operation?.to]).filter(Boolean));
208
418
  return { ...run,
@@ -216,6 +426,7 @@
216
426
  }
217
427
 
218
428
  function buildSnapshotRestoreUndo(run = {}) {
429
+ run = withAvailableUndo(run);
219
430
  const undoOperations = Array.isArray(run.undoOperations) ? run.undoOperations : [];
220
431
  const originalByPath = buildOriginalFilesForSnapshotUndo(run, undoOperations);
221
432
  if (!originalByPath.size) {
@@ -396,6 +607,7 @@
396
607
  insert: String(patch.insert ?? '')
397
608
  }));
398
609
  }
610
+ if (operation.type === 'move' && operation.moveProof?.v === 1) normalized.moveProof = JSON.parse(JSON.stringify(operation.moveProof));
399
611
  return normalized;
400
612
  }
401
613
 
@@ -454,11 +666,20 @@
454
666
  }
455
667
 
456
668
  return {
669
+ getUndoUnavailableFiles,
670
+ withAvailableUndo,
671
+ compactUnavailableUndoOperations,
672
+ isFileUndoOperation,
673
+ getFileUndoOperations,
674
+ prepareRunUndo,
675
+ textUndoRun,
676
+ mergeFileUndoResult,
457
677
  buildUndoCheckpoint,
458
678
  buildCreatedFileUndoCheckpoint,
459
679
  advanceUndoCheckpoint,
460
680
  buildExpectedFilesAfterOperations,
461
681
  buildUndoOperations,
682
+ selectExpectedFilesForTrackedUndo,
462
683
  buildSnapshotRestoreUndo
463
684
  };
464
685
  });
@@ -78,6 +78,7 @@
78
78
  deadlineAt: finiteNumber(value.deadlineAt),
79
79
  heartbeatAt: finiteNumber(value.heartbeatAt),
80
80
  postponeUntil: finiteNumber(value.postponeUntil),
81
+ cooldownUntil: finiteNumber(value.cooldownUntil),
81
82
  code: safeToken(value.code),
82
83
  message: String(value.message || '').replace(/\s+/g, ' ').trim().slice(0, 300),
83
84
  manualReason: normalizeManualReason(value.manualReason)
@@ -118,9 +119,6 @@
118
119
  const state = normalizeUpdateState(updateValue, options.currentVersion);
119
120
  const consent = normalizeConsentState(consentValue);
120
121
  const newer = compareStableVersions(state.latestVersion, state.currentVersion) > 0;
121
- const snoozed = newer &&
122
- consent.snoozedVersion === state.latestVersion &&
123
- consent.snoozedUntil > now;
124
122
  const execution = EXECUTION_STATES.has(state.state);
125
123
  const available = state.state === 'update_available' && newer;
126
124
  // A newer release that only a reinstall can apply. It is shown even after
@@ -129,7 +127,11 @@
129
127
  // still names the old one, and the notice must clear by itself.
130
128
  const runningVersion = safeVersion(options.currentVersion) || state.currentVersion;
131
129
  const manualInstall = state.state === 'manual_install_required'
132
- && compareStableVersions(state.latestVersion, runningVersion) > 0;
130
+ && (compareStableVersions(state.latestVersion, runningVersion) > 0
131
+ || (state.code === 'prerelease_promotion_requires_manual_update'
132
+ && state.latestVersion === runningVersion));
133
+ const snoozed = (newer || manualInstall) &&
134
+ consent.snoozedVersion === state.latestVersion && consent.snoozedUntil > now;
133
135
  const progress = getProgressModel(state);
134
136
  const quietFailure = state.state === 'failed' &&
135
137
  state.initiatedBy !== 'manual' &&
@@ -12,21 +12,35 @@
12
12
  return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
13
13
  }
14
14
 
15
+ function verifiedRollback(status = {}) {
16
+ const transaction = status.transaction;
17
+ const pair = status.restoredPair;
18
+ const digest = value => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
19
+ return Boolean(transaction?.id && transaction.state === 'rolled_back'
20
+ && version(transaction.sourceVersion) && version(transaction.targetVersion)
21
+ && status.installedAligned === true && status.activeVersion === transaction.sourceVersion
22
+ && pair?.extensionVersion === transaction.sourceVersion && pair.nativeVersion === transaction.sourceVersion
23
+ && digest(pair.extensionSha256) && digest(pair.nativeSha256)
24
+ && (!transaction.bootstrapRevision || digest(pair.bootstrapSha256)));
25
+ }
26
+
15
27
  function inspectInstalledRuntime(status = {}, options = {}) {
16
28
  const result = { installedVersion: version(status.activeVersion), nativeVersion: version(status.runtimeVersion),
17
29
  extensionVersion: version(options.extensionVersion), runtimeVersion: version(options.runtimeVersion), state: 'unknown' };
18
30
  if (!status.managed || !result.installedVersion || !result.nativeVersion ||
19
31
  !result.extensionVersion || !result.runtimeVersion) return result;
20
32
  if (['authorized', 'bound'].includes(status.authorization?.state) ||
21
- ['staged', 'activation_pending', 'applying', 'awaiting_health'].includes(status.transaction?.state)) {
33
+ ['staged', 'activation_pending', 'applying', 'bootstrap_pending', 'bootstrap_rolling_back', 'awaiting_health', 'rolling_back'].includes(status.transaction?.state)) {
22
34
  return { ...result, state: 'transaction_active' };
23
35
  }
24
36
  if (status.installedAligned !== true) return { ...result, state: 'repair_required' };
25
37
  const loaded = [result.nativeVersion, result.extensionVersion, result.runtimeVersion];
26
38
  if (loaded.every(value => value === result.installedVersion)) return { ...result, state: 'aligned' };
27
- return { ...result, state: loaded.every(value => compare(result.installedVersion, value) >= 0)
39
+ const rollbackReload = verifiedRollback(status) && loaded.every(value =>
40
+ value === status.transaction.sourceVersion || value === status.transaction.targetVersion);
41
+ return { ...result, state: rollbackReload || loaded.every(value => compare(result.installedVersion, value) >= 0)
28
42
  ? 'reload_required' : 'repair_required' };
29
43
  }
30
44
 
31
- return Object.freeze({ inspectInstalledRuntime });
45
+ return Object.freeze({ inspectInstalledRuntime, verifiedRollback });
32
46
  });
@@ -18,6 +18,22 @@
18
18
  if (typeof input[key] === 'string') op[key] = input[key];
19
19
  }
20
20
  if (Number.isSafeInteger(input.size)) op.size = input.size;
21
+ if (input.moveProof !== undefined) {
22
+ const proof = input.moveProof;
23
+ if (input.type !== 'move' || proof?.v !== 1 || !['text', 'binary'].includes(proof.kind)
24
+ || !/^[a-f0-9]{64}$/i.test(proof.sha256 || '')) return null;
25
+ op.moveProof = { v: 1, kind: proof.kind, sha256: proof.sha256.toLowerCase() };
26
+ if (proof.entityType !== undefined) {
27
+ if (!['doc', 'file'].includes(proof.entityType)) return null;
28
+ op.moveProof.entityType = proof.entityType;
29
+ }
30
+ for (const key of ['sourceEntityId', 'targetEntityId']) {
31
+ if (proof[key] !== undefined) {
32
+ if (typeof proof[key] !== 'string' || !proof[key] || proof[key].length > 160 || /[\0\r\n]/.test(proof[key])) return null;
33
+ op.moveProof[key] = proof[key];
34
+ }
35
+ }
36
+ }
21
37
  if (Array.isArray(input.patches)) {
22
38
  if (input.patches.length > 1000) return null;
23
39
  op.patches = input.patches.map(p => ({ from: p.from, to: p.to, expected: String(p.expected ?? ''), insert: String(p.insert ?? '') }));
@@ -1,10 +1,10 @@
1
1
  (function initCodexOverleafWritebackOperationEvidence(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('./failureReasons'));
4
4
  } else {
5
- root.CodexOverleafModuleRegistry.define('WritebackOperationEvidence', [], factory);
5
+ root.CodexOverleafModuleRegistry.define('WritebackOperationEvidence', ['FailureReasons'], factory);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function writebackOperationEvidenceFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function writebackOperationEvidenceFactory(FailureReasons) {
8
8
  'use strict';
9
9
 
10
10
  function normalizeOperationEvidence(operations = [], applyResult = {}) {
@@ -99,18 +99,7 @@
99
99
  }
100
100
 
101
101
  function normalizeFailure(result, operation) {
102
- if (result?.failure && typeof result.failure === 'object') return cloneValue(result.failure);
103
- if (!result?.code) return null;
104
- return {
105
- code: result.code,
106
- stage: result.stage || 'write',
107
- severity: result.severity || 'error',
108
- userMessage: result.reason || result.message || result.code,
109
- retryable: result.retryable === true,
110
- file: operation?.path || operation?.to || '',
111
- changedDocument: result.changedDocument === true,
112
- terminalState: result.terminalState || ''
113
- };
102
+ return FailureReasons.normalizeWritebackFailure(result, operation);
114
103
  }
115
104
 
116
105
  function normalizeTrackedChanges(value) {