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,7 +1,7 @@
1
1
  (function (root, factory) {
2
- if (typeof module === 'object' && module.exports) module.exports = factory();
3
- else root.CodexOverleafModuleRegistry.define('SharedSessionBridge', [], factory);
4
- })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory(require('./sharedSessionTransfer'));
3
+ else root.CodexOverleafModuleRegistry.define('SharedSessionBridge', ['SharedSessionTransfer'], factory);
4
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function (Transfer) {
5
5
  'use strict';
6
6
  const MESSAGE = 'codex-overleaf/shared-sessions-v1';
7
7
  const LOCK_PORT = 'codex-overleaf/shared-session-lock-v1';
@@ -34,7 +34,7 @@
34
34
  }
35
35
  function assertScope(value) { capture(value.projectId, value.accountScopeId); }
36
36
  const cacheKey = (value, id) => value.accountScopeId + ':' + id;
37
- const lockKey = (kind, value) => kind === 'preferences' ? 'preferences' : 'state:' + value.accountScopeId;
37
+ const lockKey = (kind, value) => kind === 'review' ? 'review:' + JSON.stringify([value.accountScopeId, value.projectId, value.runId]) : kind === 'preferences' ? 'preferences' : 'state:' + value.accountScopeId;
38
38
  async function request(method, value, args = {}) {
39
39
  assertScope(value);
40
40
  const lock = heldLocks.get(lockKey('state', value));
@@ -51,23 +51,24 @@
51
51
 
52
52
  // Live ports own the locks. Worker/page termination releases them without
53
53
  // allowing a stale client to silently fall back to an origin-local lock.
54
- async function acquire(kind, value, work) {
54
+ async function acquire(kind, value, work, onLost) {
55
55
  assertScope(value);
56
56
  const key = lockKey(kind, value), port = chromeApi.runtime.connect({ name: LOCK_PORT });
57
57
  let timer, heartbeat, released = false, lock = null, rejectGrant;
58
58
  const granted = new Promise((resolve, reject) => {
59
59
  rejectGrant = reject;
60
60
  port.onMessage.addListener(message => {
61
+ if (message?.type === 'busy') { reject(failure('review_busy', 'Another page is reviewing this run. Wait for it to finish.')); return; }
61
62
  if (message?.type !== 'granted' || lock || released) return;
62
63
  clearTimeout(timer);
63
- lock = { token: message.token, lost: false };
64
+ lock = { token: message.token, deadlineAt: message.deadlineAt, lost: false };
64
65
  heldLocks.set(key, lock);
65
66
  resolve();
66
67
  });
67
68
  port.onDisconnect.addListener(() => {
68
69
  // Read lastError to consume the runtime's disconnected-port warning.
69
70
  void chromeApi.runtime.lastError;
70
- if (lock) lock.lost = true;
71
+ if (lock) { lock.lost = true; if (!released) onLost?.(lock.token); }
71
72
  if (!released) reject(failure('storage_lock_lost', 'The shared history lock disconnected.'));
72
73
  });
73
74
  timer = environment.setTimeout(() => reject(failure('storage_lock_timeout',
@@ -84,12 +85,27 @@
84
85
  port.postMessage({ type: 'acquire', kind, scope: value });
85
86
  await granted;
86
87
  assertScope(value);
87
- const result = await work();
88
+ const assertOwner = () => { assertScope(value); if (lock.lost) throw failure('storage_lock_lost', 'The shared review owner disconnected.'); };
89
+ assertOwner.token = lock.token;
90
+ assertOwner.markDispatched = async () => {
91
+ assertOwner();
92
+ const lease = await request('review-dispatched', value, { runId: value.runId, reviewToken: lock.token });
93
+ assertOwner(); assertOwner.deadlineAt = lease.deadlineAt; return lease.deadlineAt;
94
+ };
95
+ const result = await work(assertOwner);
88
96
  assertScope(value);
89
97
  if (lock.lost) throw failure('storage_lock_lost', 'The shared history lock was lost before completion.');
90
98
  return result;
91
99
  } finally {
92
100
  released = true;
101
+ if (kind === 'review' && lock && !lock.lost) {
102
+ await new Promise(resolve => {
103
+ const done = message => { if (message?.type === 'released') { port.onMessage.removeListener(done); environment.clearTimeout(releaseTimer); resolve(); } };
104
+ const releaseTimer = environment.setTimeout(resolve, 2000);
105
+ port.onMessage.addListener(done);
106
+ try { port.postMessage({ type: 'release-review', token: lock.token }); } catch (_) { resolve(); }
107
+ });
108
+ }
93
109
  environment.clearTimeout(timer);
94
110
  environment.clearInterval(heartbeat);
95
111
  if (heldLocks.get(key) === lock) heldLocks.delete(key);
@@ -144,7 +160,9 @@
144
160
  Object.defineProperty(record, BASE, { value: base, enumerable: true });
145
161
  return record;
146
162
  }
147
- 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) {
148
166
  const value = capture(projectId);
149
167
  await migrate(value);
150
168
  const result = []; let after = '';
@@ -169,31 +187,97 @@
169
187
  return Boolean(base && (session.updatedAt !== base.updatedAt || session.task !== base.task));
170
188
  }
171
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
+ }
172
202
  async function put(records, options = {}) {
173
203
  if (!records.length) return [];
174
204
  const value = capture(records[0].projectId, records[0].accountScopeId);
175
205
  if (records.some(record => record.projectId !== value.projectId || record.accountScopeId !== value.accountScopeId))
176
206
  throw failure('shared_store_scope_mismatch', 'A session batch must belong to one account and project.');
177
207
  return withWrite(value, async () => {
178
- const entries = records.map(record => ({
179
- record: clean(record),
180
- base: record[BASE] || baselines.get(cacheKey(value, record.id)) || null
181
- }));
182
- const written = await request('put', value, { entries, reviewRunIds: options.reviewRunIds || [] });
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
+ });
215
+ const transfer = await Transfer.encode(entries);
216
+ const written = await request(transfer.method, value, { ...transfer.payload, reviewRunIds: options.reviewRunIds || [] });
183
217
  // Keep the caller's submitted view as its merge baseline. A merged
184
218
  // remote change has not necessarily been applied to the visible UI.
185
- 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));
186
221
  return written;
187
222
  });
188
223
  }
189
- async function get(id) {
224
+ async function get(id, options = {}) {
190
225
  const value = capture();
191
226
  await migrate(value);
192
227
  const record = await request('get', value, { id });
193
- return tag(record, value, !heldLocks.has(lockKey('state', value)));
228
+ return tag(record, value, options.adopt !== false && !heldLocks.has(lockKey('state', value)));
194
229
  }
195
- async function erase(method, projectId = '', id) {
196
- const value = capture(projectId);
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
+ });
278
+ }
279
+ async function erase(method, projectId = '', id, accountScopeId) {
280
+ const value = capture(projectId, accountScopeId);
197
281
  return withWrite(value, () => request(method, value, { id }));
198
282
  }
199
283
  function decorate(api) {
@@ -211,7 +295,7 @@
211
295
  return record;
212
296
  });
213
297
  route('getAllSessions', () => list());
214
- 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));
215
299
  route('getAllByIndex', async (store, index, value) => {
216
300
  if (store !== 'sessions') return api.getAllByIndex(store, index, value);
217
301
  const records = await list(index === 'projectId' ? value : '');
@@ -219,14 +303,15 @@
219
303
  });
220
304
  route('claimSessionsForAccount', async (projectId, accountScopeId, deleted = []) => {
221
305
  capture(projectId, accountScopeId);
222
- return (await list(projectId)).filter(record => !deleted.includes(record.id));
306
+ return (await list(projectId, true)).filter(record => !deleted.includes(record.id));
223
307
  });
224
- route('putRecord', async (store, record) =>
225
- 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));
226
310
  route('putRecords', (store, records, options) =>
227
311
  store === 'sessions' ? put(records, options) : api.putRecords(store, records));
228
- route('deleteRecord', (store, id) =>
229
- store === 'sessions' ? erase('delete', '', id) : api.deleteRecord(store, id));
312
+ route('deleteRecord', (store, id, expectedScope) =>
313
+ store === 'sessions' ? erase('delete', expectedScope?.projectId || '', id, expectedScope?.accountScopeId)
314
+ : api.deleteRecord(store, id));
230
315
  route('deleteByIndex', async (store, index, value) => {
231
316
  if (store !== 'sessions') return api.deleteByIndex(store, index, value);
232
317
  if (index === 'projectId') return erase('clear-project', value);
@@ -243,7 +328,7 @@
243
328
  for (const record of await api.getAllSessions()) {
244
329
  if (record.accountScopeId === value.accountScopeId) await api.deleteRecord('sessions', record.id);
245
330
  }
246
- for (const store of Object.keys(api.STORES)) if (store !== 'sessions') await api.clearStore(store);
331
+ await api.clearAuxiliaryStores();
247
332
  baselines.clear();
248
333
  });
249
334
  route('listRecentProjectsAcrossAccount', async options => {
@@ -253,11 +338,17 @@
253
338
  });
254
339
  Object.assign(wrapped, {
255
340
  sharedSessionsEnabled: enabled,
341
+ withSharedReviewLock: async (input, work) => {
342
+ const value = { ...capture(input.projectId, input.accountScopeId), runId: input.runId };
343
+ return acquire('review', value, work, input.onLost);
344
+ },
345
+ hasSharedReviewOwner: input => request('review-owner', capture(input.projectId, input.accountScopeId), { runId: input.runId }),
256
346
  withSharedLock: withLock,
257
347
  readSharedProjectSnapshot: projectId => list(projectId, false),
258
348
  adoptSharedSessionBaseline: adopt,
259
349
  hasUnpersistedSharedSession: hasLocalChanges,
260
- wasSharedSessionStored: wasStored
350
+ wasSharedSessionStored: wasStored,
351
+ putSessionReviewState
261
352
  });
262
353
  return wrapped;
263
354
  }
@@ -0,0 +1,140 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodexOverleafModuleRegistry.define('SharedSessionTransfer', [], factory);
4
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function () {
5
+ 'use strict';
6
+ const MAX_WIRE_BYTES = 24 * 1024 * 1024;
7
+ // The old UTF-16 estimate admitted up to 3 UTF-8 bytes per code unit.
8
+ // Retain that finite Unicode window without enlarging ordinary ASCII writes.
9
+ const MAX_RECORD_BYTES = MAX_WIRE_BYTES * 3 / 2;
10
+ const MAX_RESPONSE_BYTES = MAX_RECORD_BYTES;
11
+ const MAX_DECODED_BYTES = 2 * MAX_RECORD_BYTES + 64 * 1024;
12
+ const error = (code, message) => Object.assign(new Error(message), { code });
13
+ const tooLarge = () => error('shared_session_too_large', 'The session batch exceeds the bounded shared storage transfer limit.');
14
+ const invalid = () => error('shared_session_transfer_invalid', 'The shared history transfer is invalid or incomplete.');
15
+ const byteLength = text => new TextEncoder().encode(text).byteLength;
16
+
17
+ function serialize(value, limit) {
18
+ // Count a lower bound during serialization, before a large aggregate string
19
+ // can be produced. The final UTF-8 check also includes commas and escaping.
20
+ let lowerBound = 0;
21
+ const text = JSON.stringify(value, function (key, item) {
22
+ if (key && !Array.isArray(this)) lowerBound += byteLength(JSON.stringify(key)) + 1;
23
+ if (typeof item === 'string') {
24
+ if (item.length > limit) throw tooLarge();
25
+ lowerBound += byteLength(JSON.stringify(item));
26
+ } else if (item === null) lowerBound += 4;
27
+ else if (typeof item === 'number') lowerBound += Number.isFinite(item) ? String(item).length : 4;
28
+ else if (typeof item === 'boolean') lowerBound += String(item).length;
29
+ else if (item && typeof item === 'object') lowerBound += 2;
30
+ if (lowerBound > limit) throw tooLarge();
31
+ return item;
32
+ });
33
+ if (typeof text !== 'string') throw invalid();
34
+ const bytes = byteLength(text);
35
+ if (bytes > limit) throw tooLarge();
36
+ return { text, bytes };
37
+ }
38
+
39
+ function validateRecord(record) {
40
+ const value = serialize(record, MAX_RECORD_BYTES);
41
+ if (value.bytes > MAX_WIRE_BYTES && value.text.length * 2 > MAX_WIRE_BYTES) throw tooLarge();
42
+ }
43
+
44
+ function validateResponse(records) { serialize(records, MAX_RESPONSE_BYTES); }
45
+
46
+ function validateEntries(entries) {
47
+ if (!Array.isArray(entries)) throw invalid();
48
+ for (const entry of entries) {
49
+ if (!entry || !entry.record || typeof entry.record !== 'object' || Array.isArray(entry.record)) throw invalid();
50
+ validateRecord(entry.record);
51
+ if (entry.base != null) {
52
+ if (typeof entry.base !== 'object' || Array.isArray(entry.base)) throw invalid();
53
+ // A previously stored merged baseline may be larger than new records;
54
+ // allow a bounded old base so a smaller candidate can repair it.
55
+ serialize(entry.base, MAX_DECODED_BYTES);
56
+ }
57
+ }
58
+ }
59
+
60
+ async function readLimited(stream, limit) {
61
+ const reader = stream.getReader(), chunks = [];
62
+ let length = 0;
63
+ try {
64
+ while (true) {
65
+ const result = await reader.read();
66
+ if (result.done) break;
67
+ length += result.value.byteLength;
68
+ if (length > limit) { await reader.cancel().catch(() => {}); throw tooLarge(); }
69
+ chunks.push(result.value);
70
+ }
71
+ } finally { reader.releaseLock(); }
72
+ const output = new Uint8Array(length); let offset = 0;
73
+ for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.byteLength; }
74
+ return output;
75
+ }
76
+
77
+ function toBase64(bytes) {
78
+ let text = '';
79
+ for (let i = 0; i < bytes.length; i += 0x8000) text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
80
+ return btoa(text);
81
+ }
82
+ function fromBase64(value) {
83
+ if (typeof value !== 'string' || !value.length || value.length % 4
84
+ || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) throw invalid();
85
+ const text = atob(value);
86
+ const bytes = new Uint8Array(text.length);
87
+ for (let i = 0; i < text.length; i++) bytes[i] = text.charCodeAt(i);
88
+ return bytes;
89
+ }
90
+
91
+ async function encode(entries, environment = globalThis) {
92
+ validateEntries(entries);
93
+ const raw = serialize(entries, MAX_DECODED_BYTES);
94
+ if (raw.bytes <= MAX_WIRE_BYTES || (raw.bytes <= MAX_RECORD_BYTES && raw.text.length * 2 <= MAX_WIRE_BYTES)) {
95
+ return { method: 'put', payload: { entries } };
96
+ }
97
+ if (typeof environment.CompressionStream !== 'function') {
98
+ throw error('shared_session_compression_unavailable',
99
+ 'Lossless transfer compression is unavailable. History remains in memory; retry with a supported browser.');
100
+ }
101
+ const input = new Blob([raw.text]).stream();
102
+ const packed = await readLimited(input.pipeThrough(new environment.CompressionStream('gzip')),
103
+ Math.floor((MAX_WIRE_BYTES - 1024) * 3 / 4));
104
+ const payload = { encoding: 'gzip-base64', version: 1, decodedBytes: raw.bytes, data: toBase64(packed) };
105
+ serialize(payload, MAX_WIRE_BYTES);
106
+ // A distinct method makes an older worker reject rather than acknowledge
107
+ // an absent `entries` array as a successful empty write.
108
+ return { method: 'put-compressed', payload };
109
+ }
110
+
111
+ async function decode(message, environment = globalThis) {
112
+ if (message.method !== 'put-compressed') {
113
+ const entries = message.method === 'import' ? [{ record: message.record }] : message.entries || [];
114
+ const raw = serialize(entries, MAX_RECORD_BYTES);
115
+ if (raw.bytes > MAX_WIRE_BYTES && raw.text.length * 2 > MAX_WIRE_BYTES) throw tooLarge();
116
+ validateEntries(entries); return entries;
117
+ }
118
+ const payload = { encoding: message.encoding, version: message.version, decodedBytes: message.decodedBytes, data: message.data };
119
+ serialize(payload, MAX_WIRE_BYTES);
120
+ if (message.entries != null || payload.encoding !== 'gzip-base64' || payload.version !== 1
121
+ || !Number.isSafeInteger(payload.decodedBytes) || payload.decodedBytes < 1 || payload.decodedBytes > MAX_DECODED_BYTES) throw invalid();
122
+ if (typeof environment.DecompressionStream !== 'function') throw error('shared_session_compression_unavailable',
123
+ 'Lossless transfer decompression is unavailable. Reload the extension and retry.');
124
+ let bytes;
125
+ try {
126
+ bytes = await readLimited(new Blob([fromBase64(payload.data)]).stream()
127
+ .pipeThrough(new environment.DecompressionStream('gzip')), MAX_DECODED_BYTES);
128
+ } catch (failure) {
129
+ if (['shared_session_too_large', 'shared_session_transfer_invalid'].includes(failure?.code)) throw failure;
130
+ throw invalid();
131
+ }
132
+ if (bytes.byteLength !== payload.decodedBytes) throw invalid();
133
+ let entries;
134
+ try { entries = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
135
+ catch (_error) { throw invalid(); }
136
+ validateEntries(entries);
137
+ return entries;
138
+ }
139
+ return { MAX_WIRE_BYTES, MAX_RECORD_BYTES, MAX_RESPONSE_BYTES, MAX_DECODED_BYTES, byteLength, validateRecord, validateResponse, encode, decode };
140
+ });
@@ -17,7 +17,9 @@
17
17
  if (typeof file?.path === 'string') {
18
18
  const path = normalizePath(file.path);
19
19
  if (path) {
20
- lookup.set(path, normalizeText(file.content));
20
+ // Patch offsets refer to this exact UTF-16 string. Normalize only
21
+ // for comparisons, never discard the source coordinate system.
22
+ lookup.set(path, String(file.content ?? ''));
21
23
  }
22
24
  }
23
25
  }
@@ -40,71 +42,181 @@
40
42
  };
41
43
  }
42
44
 
43
- const baseContent = baseFileLookup.get(filePath);
44
- if (!baseContent || baseContent.trim().length === 0) {
45
- if (typeof console !== 'undefined') {
46
- console.warn('[codex-overleaf] staleGuard: base empty for', filePath, '→ skipping check');
47
- }
48
- return { ok: true };
49
- }
45
+ const baseContent = normalizeText(baseFileLookup.get(filePath));
50
46
  const current = normalizeText(currentContent);
51
47
  if (current !== baseContent) {
52
- if (typeof console !== 'undefined') {
53
- console.warn('[codex-overleaf] staleGuard: STALE', filePath,
54
- 'base.length:', baseContent.length, 'current.length:', current.length,
55
- 'base[0..80]:', JSON.stringify(baseContent.slice(0, 80)),
56
- '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 };
57
51
  }
58
- const patchRangeFreshness = checkPatchRangeFreshness(operation, current);
59
- if (patchRangeFreshness) {
60
- return patchRangeFreshness;
61
- }
62
- return {
63
- ok: false,
64
- code: 'stale_snapshot',
65
- reasonKey: 'staleSnapshot',
66
- reasonParams: { filePath },
67
- reason: `${filePath} 在任务执行期间被你或协作者改过,Codex 没有覆盖它。请查看差异后重试。`
68
- };
52
+ return staleSourceChanged(filePath);
69
53
  }
70
54
 
71
55
  return { ok: true };
72
56
  }
73
57
 
74
- function checkPatchRangeFreshness(operation, currentContent) {
58
+ function staleSourceChanged(filePath) {
59
+ return {
60
+ ok: false,
61
+ code: 'stale_snapshot',
62
+ reasonKey: 'staleSnapshot',
63
+ reasonParams: { filePath },
64
+ reason: `${filePath} 在任务执行期间被你或协作者改过,Codex 没有覆盖它。请查看差异后重试。`
65
+ };
66
+ }
67
+
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.
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) {
75
104
  const patches = Array.isArray(operation?.patches) ? operation.patches : [];
76
- if (!patches.length) {
77
- return null;
105
+ const current = String(currentContent ?? '');
106
+ const path = normalizePath(operation?.path);
107
+ const base = baseFileLookup?.get(path);
108
+ if (!patches.length || typeof base !== 'string' || base === current) {
109
+ return { ok: true, patches };
110
+ }
111
+ const baseMap = lineEndingMap(base);
112
+ const currentMap = lineEndingMap(current);
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 };
78
127
  }
79
128
 
80
- const current = normalizeText(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
- };
129
+ const groups = [];
130
+ let previousTo = 0;
131
+ for (const patch of patches) {
132
+ const from = Number(patch?.from), to = Number(patch?.to);
133
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from < previousTo || to < from || to > base.length) {
134
+ return patchMappingFailure('invalid_patch');
92
135
  }
93
- if (current.slice(from, to) !== expected) {
94
- return {
95
- ok: false,
96
- code: 'stale_patch_range',
97
- reasonKey: 'stalePatchConflict',
98
- reason: 'Codex 要修改的具体位置已经被你或协作者改过,所以没有覆盖它。请查看差异后重试。'
99
- };
136
+ if (base.slice(from, to) !== String(patch.expected ?? '')) return patchMappingFailure('stale_patch');
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');
100
142
  }
143
+ // Expand only a split CRLF or a new pair formed across an edit edge.
144
+ // Other neighbouring newlines belong to the editor and stay untouched.
145
+ let start = from, end = to;
146
+ const insert = String(patch.insert ?? '');
147
+ if (base[from - 1] === '\r' && (base[from] === '\n' || insert.startsWith('\n') || (!insert && base[to] === '\n'))) start -= 1;
148
+ if (base[to] === '\n' && (base[to - 1] === '\r' || insert.endsWith('\r') || (!insert && base[from - 1] === '\r'))) end += 1;
149
+ const previous = groups[groups.length - 1];
150
+ if (previous && start <= previous.end) {
151
+ previous.end = Math.max(previous.end, end);
152
+ previous.patches.push({ ...patch, from, to });
153
+ } else groups.push({ start, end, patches: [{ ...patch, from, to }] });
101
154
  }
102
155
 
103
- return {
104
- ok: true,
105
- reconciled: true,
106
- strategy: 'patch-range'
107
- };
156
+ const mapped = [], sourceChanges = [];
157
+ for (const group of groups) {
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);
161
+ const expected = current.slice(from, to);
162
+ if (normalizeText(expected) !== normalizeText(base.slice(group.start, group.end))) {
163
+ return patchMappingFailure('stale_patch');
164
+ }
165
+ let insert = base.slice(group.start, group.end);
166
+ for (const patch of group.patches.slice().sort((left, right) => right.from - left.from)) {
167
+ if (insert.slice(patch.from - group.start, patch.to - group.start) !== String(patch.expected ?? '')) {
168
+ return patchMappingFailure('stale_patch');
169
+ }
170
+ insert = insert.slice(0, patch.from - group.start) + String(patch.insert ?? '') + insert.slice(patch.to - group.start);
171
+ }
172
+ sourceChanges.push({ from: group.start, to: group.end, insert });
173
+ const sourceEnding = nearestLineEnding(base, group.start);
174
+ const editorEnding = nearestLineEnding(current, from);
175
+ if (sourceEnding && editorEnding && sourceEnding !== editorEnding) {
176
+ insert = normalizeText(insert).replace(/\n/g, editorEnding);
177
+ }
178
+ // Leave equal surrounding text out of the dispatch/Track range.
179
+ let prefix = 0, oldEnd = expected.length, newEnd = insert.length;
180
+ while (prefix < oldEnd && prefix < newEnd && expected[prefix] === insert[prefix]) prefix += 1;
181
+ while (oldEnd > prefix && newEnd > prefix && expected[oldEnd - 1] === insert[newEnd - 1]) { oldEnd -= 1; newEnd -= 1; }
182
+ mapped.push({ from: from + prefix, to: from + oldEnd, expected: expected.slice(prefix, oldEnd), insert: insert.slice(prefix, newEnd) });
183
+ }
184
+ // Preserve source coordinates for line-ending-only conversion. A rebased
185
+ // entry instead advances to its actual verified editor post-image.
186
+ let sourceContent = base;
187
+ for (const patch of sourceChanges.reverse()) {
188
+ sourceContent = sourceContent.slice(0, patch.from) + patch.insert + sourceContent.slice(patch.to);
189
+ }
190
+ return { ok: true, patches: mapped, ...(!envelope ? { sourceContent } : {}) };
191
+ }
192
+
193
+ function patchMappingFailure(code) {
194
+ return { ok: false, code, reason: 'Codex 要修改的位置已经无法和当前 Overleaf 内容对齐,所以没有写入。请重新运行任务。' };
195
+ }
196
+
197
+ function lineEndingMap(text) {
198
+ const removed = [];
199
+ for (let index = 0; index < text.length - 1; index += 1) {
200
+ if (text[index] === '\r' && text[index + 1] === '\n') removed.push(index);
201
+ }
202
+ function countBefore(offset, logical) {
203
+ let low = 0, high = removed.length;
204
+ while (low < high) {
205
+ const middle = (low + high) >>> 1;
206
+ if (removed[middle] - (logical ? middle : 0) < offset) low = middle + 1;
207
+ else high = middle;
208
+ }
209
+ return low;
210
+ }
211
+ return { removed, length: text.length - removed.length,
212
+ toLogical: offset => offset - countBefore(offset, false),
213
+ toRaw: offset => offset + countBefore(offset, true) };
214
+ }
215
+
216
+ function nearestLineEnding(text, offset) {
217
+ let newline = text.indexOf('\n', offset);
218
+ if (newline < 0) newline = text.lastIndexOf('\n', offset - 1);
219
+ return newline < 0 ? '' : text[newline - 1] === '\r' ? '\r\n' : '\n';
108
220
  }
109
221
 
110
222
  function updateExpectedFileContent(baseFileLookup, filePath, content) {
@@ -112,7 +224,7 @@
112
224
  if (!baseFileLookup || !path) {
113
225
  return;
114
226
  }
115
- baseFileLookup.set(path, normalizeText(content));
227
+ baseFileLookup.set(path, String(content ?? ''));
116
228
  }
117
229
 
118
230
  function removeExpectedFile(baseFileLookup, filePath) {
@@ -152,6 +264,8 @@
152
264
  return {
153
265
  buildBaseFileLookup,
154
266
  checkOperationFreshness,
267
+ prepareOperationPatches,
268
+ prepareStoredInversePatches,
155
269
  updateExpectedFileContent,
156
270
  removeExpectedFile,
157
271
  moveExpectedFile,