codex-overleaf-link 2.5.1 → 2.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +148 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -21,6 +21,12 @@
21
21
  let appendRunRecordEvent;
22
22
  // In-flight background mirror refresh (v1.7.5) — see the writeback path.
23
23
  let pendingMirrorRefresh = null;
24
+ const mirrorSettlementOwners = new Map();
25
+ const mirrorReuseFences = new Map();
26
+ const mirrorFenceWrites = new Map();
27
+ const mirrorFenceRevisions = new Map();
28
+ const MIRROR_INVALIDATION_TIMEOUT_MS = 20000;
29
+ const MIRROR_FENCE_STORAGE_TIMEOUT_MS = 2000;
24
30
  let appendChangeSummary;
25
31
  let appendCompletionReport;
26
32
  let appendOperationsPreview;
@@ -31,6 +37,8 @@
31
37
  let callPageBridge;
32
38
  let sendBackgroundNative;
33
39
  let getCurrentProjectId;
40
+ let getAccountScopeId;
41
+ let mirrorFenceStore;
34
42
  let resetContextProject;
35
43
  let sanitizeRunProjectSnapshot;
36
44
  let getAssistantAnswerForCurrentRun;
@@ -72,27 +80,158 @@
72
80
  : null;
73
81
  }
74
82
 
75
- function invalidateMirrorAfterUndo(runId, projectId, result = {}) {
76
- if (!projectId || (result?.changedDocument !== true && !result?.applied?.length)) return Promise.resolve('not-attempted');
77
- const pending = Promise.resolve(pendingMirrorRefresh).catch(() => {}).then(async () => {
78
- const response = await sendBackgroundNative({ method: 'mirror.invalidate', params: { projectId } });
79
- if (!response?.ok || response.result?.invalidated !== true) {
80
- throw new Error(response?.error?.message || 'Native host did not confirm mirror invalidation.');
83
+ function mirrorFenceScope(projectId, accountScopeId = getAccountScopeId?.() || '') {
84
+ return { projectId, accountScopeId };
85
+ }
86
+
87
+ function mirrorFenceKey(scope) {
88
+ return JSON.stringify([scope.accountScopeId, scope.projectId]);
89
+ }
90
+
91
+ function getMirrorReuseFence(projectId, accountScopeId) {
92
+ return mirrorReuseFences.get(mirrorFenceKey(mirrorFenceScope(projectId, accountScopeId))) || null;
93
+ }
94
+
95
+ function updateMirrorReuseFence(scope, generations, unknown = false) {
96
+ const key = mirrorFenceKey(scope), current = mirrorReuseFences.get(key);
97
+ const ids = [...new Set(generations)].sort();
98
+ if (current && current.unknown === unknown && current.generations.join(':') === ids.join(':')) return current;
99
+ const fence = ids.length || unknown ? { scope, generations: ids, unknown, syncAttempted: false } : null;
100
+ if (fence) mirrorReuseFences.set(key, fence);
101
+ else mirrorReuseFences.delete(key);
102
+ mirrorFenceRevisions.set(key, (mirrorFenceRevisions.get(key) || 0) + 1);
103
+ return fence;
104
+ }
105
+
106
+ async function awaitMirrorFenceStorage(work) {
107
+ let timer;
108
+ try {
109
+ return await Promise.race([
110
+ Promise.resolve().then(work),
111
+ new Promise((_resolve, reject) => {
112
+ timer = setTimeout(() => reject(new Error('Mirror fence storage timed out.')), MIRROR_FENCE_STORAGE_TIMEOUT_MS);
113
+ })
114
+ ]);
115
+ } finally { clearTimeout(timer); }
116
+ }
117
+
118
+ async function refreshMirrorReuseFence(projectId, accountScopeId) {
119
+ const scope = mirrorFenceScope(projectId, accountScopeId), key = mirrorFenceKey(scope);
120
+ if (!mirrorFenceStore) return getMirrorReuseFence(projectId, scope.accountScopeId);
121
+ const revision = mirrorFenceRevisions.get(key) || 0;
122
+ try {
123
+ const stored = await awaitMirrorFenceStorage(() => mirrorFenceStore.getMirrorReuseFences(scope));
124
+ const writes = mirrorFenceWrites.get(key) || new Map();
125
+ const changed = revision !== (mirrorFenceRevisions.get(key) || 0);
126
+ const generations = new Set(stored.filter(id => !writes.get(id)?.retired));
127
+ for (const [id, write] of writes) {
128
+ if (!write.retired && (!write.persisted || changed)) generations.add(id);
81
129
  }
82
- if (getCurrentProjectId() === projectId) {
83
- resetContextProject();
84
- await callPageBridge('invalidateProjectSnapshot', { invalidateFileList: true });
130
+ // A read started before a local write or newer read cannot erase that evidence.
131
+ if (changed) for (const id of mirrorReuseFences.get(key)?.generations || []) {
132
+ if (!writes.get(id)?.retired) generations.add(id);
85
133
  }
86
- return 'invalidated';
87
- }).catch(error => {
88
- appendRunRecordEvent?.(runId, {
89
- status: 'warning',
90
- title: tx(
91
- `Overleaf undo finished, but local mirror invalidation failed: ${error.message}. Refresh this project or reconnect the native host before running again.`,
92
- `Overleaf 撤销已完成,但本地镜像失效处理失败:${error.message}。请刷新项目或重新连接 Native Host 后再运行。`
93
- )
94
- });
95
- return 'failed';
134
+ return updateMirrorReuseFence(scope, [...generations]);
135
+ } catch (_error) {
136
+ return updateMirrorReuseFence(scope, mirrorReuseFences.get(key)?.generations || [], true);
137
+ }
138
+ }
139
+
140
+ async function recordMirrorReuseFence(target) {
141
+ const scope = mirrorFenceScope(target.runProjectId, target.runAccountScopeId);
142
+ const key = mirrorFenceKey(scope);
143
+ const generation = globalThis.crypto?.randomUUID?.()
144
+ || Date.now().toString(36) + '-' + Math.random().toString(36).slice(2) + '-' + Math.random().toString(36).slice(2);
145
+ const writes = mirrorFenceWrites.get(key) || new Map();
146
+ mirrorFenceWrites.set(key, writes);
147
+ const write = { persisted: false, retired: false, clearAfterWrite: false };
148
+ writes.set(generation, write);
149
+ updateMirrorReuseFence(scope, [...(mirrorReuseFences.get(key)?.generations || []), generation]);
150
+ const token = { scope, generation };
151
+ if (!mirrorFenceStore) return token;
152
+ const pending = Promise.resolve().then(() => mirrorFenceStore.setMirrorReuseFence(scope, generation));
153
+ pending.then(() => {
154
+ write.persisted = true;
155
+ mirrorFenceRevisions.set(key, (mirrorFenceRevisions.get(key) || 0) + 1);
156
+ // A slow set may finish after a successful full sync already removed its key.
157
+ if (write.clearAfterWrite) {
158
+ awaitMirrorFenceStorage(() => mirrorFenceStore.clearMirrorReuseFences(scope, [generation]))
159
+ .then(() => { write.retired = true; }).catch(() => {});
160
+ }
161
+ }, () => {});
162
+ try { await awaitMirrorFenceStorage(() => pending); }
163
+ catch (_error) { /* The existing invalidation warning and memory fence remain. */ }
164
+ return token;
165
+ }
166
+
167
+ async function clearMirrorReuseFenceGenerations(scope, generations) {
168
+ const writes = mirrorFenceWrites.get(mirrorFenceKey(scope));
169
+ for (const id of generations) if (writes?.has(id)) writes.get(id).clearAfterWrite = true;
170
+ const clearing = Promise.resolve().then(() => mirrorFenceStore?.clearMirrorReuseFences(scope, generations));
171
+ clearing.then(() => {
172
+ for (const id of generations) if (writes?.has(id)) writes.get(id).retired = true;
173
+ }, () => {});
174
+ await awaitMirrorFenceStorage(() => clearing);
175
+ }
176
+
177
+ async function settleFullMirrorSync(projectId, project, fence, response, options = {}) {
178
+ if (!fence || fence.unknown || getMirrorReuseFence(projectId, fence.scope.accountScopeId) !== fence || response?.ok !== true
179
+ || project?.capabilities?.fullProjectSnapshot !== true
180
+ || project.capabilities.contentComplete === false || project.capabilities.skipped?.length) return false;
181
+ try {
182
+ options.assertCurrent?.();
183
+ const status = await (options.sendNative || sendBackgroundNative)({ method: 'mirror.status', params: { projectId } });
184
+ options.assertCurrent?.();
185
+ const current = await refreshMirrorReuseFence(projectId, fence.scope.accountScopeId);
186
+ options.assertCurrent?.();
187
+ if (getCurrentProjectId() !== projectId || current !== fence
188
+ || status?.ok !== true || status.result?.exists !== true || status.result.dirty === true) return false;
189
+ await clearMirrorReuseFenceGenerations(fence.scope, fence.generations);
190
+ options.assertCurrent?.();
191
+ if (getMirrorReuseFence(projectId, fence.scope.accountScopeId) === fence) updateMirrorReuseFence(fence.scope, []);
192
+ return !(await refreshMirrorReuseFence(projectId, fence.scope.accountScopeId));
193
+ } catch (_error) {
194
+ options.assertCurrent?.();
195
+ return false;
196
+ }
197
+ }
198
+
199
+ async function syncFencedMirror(projectId, project, options = {}) {
200
+ const fence = options.fence;
201
+ if (!fence || fence.unknown || getMirrorReuseFence(projectId, fence.scope.accountScopeId) !== fence || fence.syncAttempted
202
+ || project?.id !== projectId || project.capabilities?.fullProjectSnapshot !== true
203
+ || project.capabilities.contentComplete === false || project.capabilities.skipped?.length) return false;
204
+ options.assertCurrent();
205
+ // Only the next full-read path adds a sync; a failed attempt keeps reuse
206
+ // fenced without adding another request on every subsequent run.
207
+ fence.syncAttempted = true;
208
+ try {
209
+ const response = await options.sendNative({ method: 'mirror.sync', params: { projectId, project } });
210
+ options.assertCurrent();
211
+ return await settleFullMirrorSync(projectId, project, fence, response, options);
212
+ } catch (_error) {
213
+ options.assertCurrent();
214
+ return false;
215
+ }
216
+ }
217
+
218
+ function trackWritebackMirror(work, target, onSettled) {
219
+ const key = JSON.stringify([target.runProjectId, target.sessionId, target.recordId]);
220
+ const owner = {};
221
+ mirrorSettlementOwners.set(key, owner);
222
+ const refresh = Promise.resolve(work).then(async state => {
223
+ if (mirrorSettlementOwners.get(key) !== owner) return state;
224
+ onSettled?.(state);
225
+ await onMirrorRefreshSettled?.({ ...target, state: state || 'not-attempted' });
226
+ return state;
227
+ }).finally(() => {
228
+ if (mirrorSettlementOwners.get(key) === owner) mirrorSettlementOwners.delete(key);
229
+ });
230
+ // Keep earlier work in the barrier even if this attempt finishes first or
231
+ // fails. Only the latest composed barrier may clear the shared slot.
232
+ const pending = Promise.allSettled([pendingMirrorRefresh, refresh]).then(([, result]) => {
233
+ if (result.status === 'rejected') throw result.reason;
234
+ return result.value;
96
235
  }).finally(() => {
97
236
  if (pendingMirrorRefresh === pending) pendingMirrorRefresh = null;
98
237
  });
@@ -100,6 +239,21 @@
100
239
  return pending;
101
240
  }
102
241
 
242
+ function invalidateMirrorAfterUndo(runId, projectId, result = {}, owner = {}) {
243
+ if (!projectId || (result?.changedDocument !== true && !result?.applied?.length)) return Promise.resolve('not-attempted');
244
+ const target = { recordId: runId, runProjectId: projectId,
245
+ runAccountScopeId: owner.runAccountScopeId || getAccountScopeId?.() || '', sessionId: owner.sessionId || '' };
246
+ const pending = Promise.resolve(pendingMirrorRefresh).catch(() => {})
247
+ .then(() => invalidateWritebackMirror(target, {
248
+ pageFirst: true, nativeTimeoutMs: MIRROR_INVALIDATION_TIMEOUT_MS, undo: true
249
+ }))
250
+ .then(state => state === 'not-attempted' ? 'invalidated' : state).finally(() => {
251
+ if (pendingMirrorRefresh === pending) pendingMirrorRefresh = null;
252
+ });
253
+ pendingMirrorRefresh = pending;
254
+ return pending;
255
+ }
256
+
103
257
  function selectionViolationNextStep() {
104
258
  return tx('Select the intended region again and request only changes within it. Valid written parts can be undone.',
105
259
  '请重新选择目标范围,仅请求修改该范围。已写入的合法部分可撤销。');
@@ -117,7 +271,8 @@
117
271
 
118
272
  async function applySyncChangesToOverleaf(syncChanges = [], project = {}, options = {}) {
119
273
  const activeRun = getCurrentRunView?.();
120
- const saveTarget = { runProjectId: activeRun?.runProjectId || '', recordId: activeRun?.recordId || '', sessionId: activeRun?.sessionId || '' };
274
+ const saveTarget = { runProjectId: activeRun?.runProjectId || '', runAccountScopeId: activeRun?.runAccountScopeId || getAccountScopeId?.() || '',
275
+ recordId: activeRun?.recordId || '', sessionId: activeRun?.sessionId || '' };
121
276
  const runMode = options.mode || getState().mode;
122
277
  const runRequireReviewing = typeof options.requireReviewing === 'boolean'
123
278
  ? options.requireReviewing
@@ -130,7 +285,7 @@
130
285
  const skipped = withheldOnly.map(buildWithheldEntry);
131
286
  const hasSelectionViolation = withheldOnly.some(change => change.reason === 'selection_scope_violation');
132
287
  const applied = { ok: false, applied: [], skipped };
133
- appendCompletionReport({ conclusion: assistantMessage || tx('Some generated changes were blocked.', '部分生成的改动已被阻止。'),
288
+ appendCompletionReport({ conclusionKey: options.assistantMessageKey || (!assistantMessage ? 'withheld_changes' : undefined), conclusion: assistantMessage || tx('Some generated changes were blocked.', '部分生成的改动已被阻止。'),
134
289
  status: 'failed', operations: [], applyResults: [applied], mode: runMode,
135
290
  nextStep: hasSelectionViolation
136
291
  ? selectionViolationNextStep()
@@ -151,7 +306,7 @@
151
306
  ),
152
307
  status: 'warning'
153
308
  });
154
- appendCompletionReport({
309
+ appendCompletionReport({ conclusionKey: options.assistantMessageKey || (!assistantMessage ? 'ask_ignored_changes' : undefined),
155
310
  conclusion: assistantMessage || tx(
156
311
  'Codex finished in Ask mode. Any local file changes were ignored and were not synced to Overleaf.',
157
312
  'Codex 已在 Ask 模式完成;本地文件改动已忽略,未同步到 Overleaf。'
@@ -228,8 +383,15 @@
228
383
  title: tx('Codex did not produce file changes that need to sync back to Overleaf.', 'Codex 没有产生需要同步回 Overleaf 的文件改动。'),
229
384
  status: 'completed'
230
385
  });
231
- appendCompletionReport({
232
- conclusion: assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
386
+ const blockedConclusion = assistantMessage
387
+ ? `${assistantMessage}\n\n${tx('Sync note: no changes were written to Overleaf; every change was blocked before writing.', '同步提示:没有向 Overleaf 写入任何改动;所有改动都在写入前被拦下。')}`
388
+ : tx('No changes were written to Overleaf; every change was blocked before writing.', '没有向 Overleaf 写入任何改动;所有改动都在写入前被拦下。');
389
+ appendCompletionReport({ conclusionKey: additionalSkippedEntries.length ? undefined
390
+ : options.assistantMessageKey || (!assistantMessage ? 'no_sync_changes' : undefined),
391
+ conclusion: additionalSkippedEntries.length ? blockedConclusion
392
+ : assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
393
+ conclusionSource: additionalSkippedEntries.length ? { origin: 'product', key: 'writeback',
394
+ outcome: 'blocked_sync', prefixLength: assistantMessage.length } : undefined,
233
395
  status: additionalSkippedEntries.length ? 'failed' : runMode === 'ask' ? tr('modeAsk') : 'completed',
234
396
  failure: blockedFailure,
235
397
  operations: [],
@@ -295,7 +457,7 @@
295
457
  title: tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
296
458
  status: 'completed'
297
459
  });
298
- appendCompletionReport({
460
+ appendCompletionReport({ conclusionKey: options.assistantMessageKey || (!assistantMessage ? 'no_approved_changes' : undefined),
299
461
  conclusion: assistantMessage || tx('No approved changes were written back to Overleaf.', '没有已确认的改动写回 Overleaf。'),
300
462
  status: additionalSkippedEntries.length ? 'failed' : runMode === 'ask' ? tr('modeAsk') : 'completed',
301
463
  failure: approvalFailure,
@@ -326,8 +488,8 @@
326
488
  if (!reviewing.ok) {
327
489
  const blocked = buildReviewingBlockedApplyResult(operations, reviewing);
328
490
  appendApplyResult(blocked);
329
- appendCompletionReport({
330
- conclusion: tx(
491
+ appendCompletionReport({ conclusionKey: 'reviewing_unavailable',
492
+ conclusion: reviewing.code === 'reviewing_unavailable' ? tr('failureReason_reviewing_unavailable_user') : tx(
331
493
  'No files were written: Track is enabled, but Codex could not verify Overleaf Reviewing/Track Changes.',
332
494
  '这轮没有写入:已开启“留痕”要求,但 Codex 没能确认 Overleaf 正在用 Reviewing/Track Changes。'
333
495
  ),
@@ -335,7 +497,7 @@
335
497
  operations,
336
498
  applyResults: [blocked],
337
499
  mode: runMode,
338
- nextStep: tx(
500
+ nextStep: reviewing.code === 'reviewing_unavailable' ? tr('failureReason_reviewing_unavailable_next') : tx(
339
501
  'Switch Overleaf to Reviewing/Track Changes manually and rerun, or turn off Track before writing.',
340
502
  '请在 Overleaf 手动切到 Reviewing/Track Changes 后重新运行,或关闭“留痕”再写入。'
341
503
  )
@@ -411,11 +573,37 @@
411
573
  // zero-write run (every operation skipped) has nothing to save-verify, so
412
574
  // skip the ~5s probe and its misleading "could not verify saved" warning
413
575
  // (v1.6.2).
414
- const confirmation = appliedPaths.length
415
- ? await verifyPostWriteSaveState(applied, project, saveTarget)
416
- : { verification: { ok: false, state: 'not_checked', reason: 'No applied writeback operations were returned.' } };
576
+ let confirmation;
577
+ try {
578
+ confirmation = appliedPaths.length
579
+ ? await verifyPostWriteSaveState(applied, project, saveTarget)
580
+ : { verification: { ok: false, state: 'not_checked', reason: 'No applied writeback operations were returned.' } };
581
+ } catch (error) {
582
+ if (!appliedPaths.length || (error?.code !== 'codex_cancelled' && error?.name !== 'AbortError')) throw error;
583
+ const saveVerification = { ok: false, state: 'unknown_timeout', signal: 'unavailable',
584
+ reason: 'Save confirmation was cancelled.', recoveryReason: 'codex_cancelled', attempts: [] };
585
+ // Cancellation cannot erase the returned writes or the recovery needed to
586
+ // check their save state. Carry the same evidence into the cancelled report.
587
+ error.writebackOutcome = { operations, applied, saveVerification,
588
+ settlement: buildSettlement({ operations, applyResult: applied, saveVerification, mirror: { state: 'unknown' } }) };
589
+ // Fence the Page cache now; bound the Native wait so an unanswered
590
+ // invalidation cannot strand the next run or recovery behind the barrier.
591
+ trackWritebackMirror(invalidateWritebackMirror(saveTarget, {
592
+ pageFirst: true, nativeTimeoutMs: MIRROR_INVALIDATION_TIMEOUT_MS
593
+ }), saveTarget,
594
+ state => {
595
+ error.writebackOutcome.settlement = buildSettlement({ operations, applyResult: applied, saveVerification, mirror: { state } });
596
+ });
597
+ try {
598
+ await settleWritebackRecovery?.(applied, project, saveVerification, saveTarget, runRequireReviewing);
599
+ } catch (recoveryError) {
600
+ appendRunRecordEvent?.(saveTarget.recordId, { kind: 'technical', status: 'warning',
601
+ title: tx('Writeback recovery persistence failed', '写回恢复信息保存失败'), detail: { reason: recoveryError.message } });
602
+ }
603
+ throw error;
604
+ }
417
605
  const saveVerification = confirmation.verification;
418
- await settleWritebackRecovery?.(applied, project, saveVerification, saveTarget);
606
+ await settleWritebackRecovery?.(applied, project, saveVerification, saveTarget, runRequireReviewing);
419
607
  if (appliedPaths.length) {
420
608
  appendPostWriteSaveVerificationWarning(saveVerification, saveTarget);
421
609
  appendRunEvent({ kind: 'technical', title: 'Post-write save confirmation', status: 'info', detail: saveVerification });
@@ -425,24 +613,11 @@
425
613
  // pending promise is exposed so undoRun and the next runTask barrier on
426
614
  // it — a mirror.sync landing AFTER an undo would otherwise push the
427
615
  // pre-undo snapshot as the local baseline.
428
- const mirrorSettlementTarget = saveTarget;
429
- pendingMirrorRefresh = refreshProjectMirrorAfterWriteback(project, applied, saveVerification, { target: saveTarget, snapshot: confirmation.snapshot })
616
+ trackWritebackMirror(refreshProjectMirrorAfterWriteback(project, applied, saveVerification, { target: saveTarget, snapshot: confirmation.snapshot })
430
617
  .catch(async error => {
431
618
  appendRunRecordEvent(saveTarget.recordId, { kind: 'technical', title: 'Mirror refresh requires a fresh read', status: 'info', detail: { reason: error.message } });
432
619
  return invalidateWritebackMirror(saveTarget);
433
- })
434
- .then(state => {
435
- if (typeof onMirrorRefreshSettled !== 'function') return;
436
- return onMirrorRefreshSettled({
437
- recordId: mirrorSettlementTarget?.recordId || '',
438
- sessionId: mirrorSettlementTarget?.sessionId || '',
439
- runProjectId: mirrorSettlementTarget?.runProjectId || '',
440
- state: state || 'not-attempted'
441
- });
442
- })
443
- .finally(() => {
444
- pendingMirrorRefresh = null;
445
- });
620
+ }), saveTarget);
446
621
  const compileSummary = appliedPaths.length
447
622
  ? await autoRecompileAfterWriteback(appliedPaths, saveVerification, {
448
623
  autoRecompile: options.autoRecompile,
@@ -453,7 +628,7 @@
453
628
  title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`),
454
629
  status: 'failed'
455
630
  });
456
- return buildPostWriteCompileSummary({ error });
631
+ return compileAdapter.buildPostWriteCompileSummary({ error });
457
632
  })
458
633
  : null;
459
634
  const hasSkippedApplyResult = skippedEntries.length > 0;
@@ -485,6 +660,10 @@
485
660
  : tx('Review the synced file in Overleaf.', '请在 Overleaf 中查看同步后的文件。');
486
661
  appendCompletionReport({
487
662
  conclusion: appendCompileSummaryToConclusion(writebackConclusion, compileSummary),
663
+ conclusionSource: { origin: 'product', key: 'writeback',
664
+ outcome: !hasConfirmedApplyResult ? 'unconfirmed_sync' : hasSkippedApplyResult ? 'partial_sync' : 'synced',
665
+ prefixLength: assistantMessage.length, prefixKey: options.assistantMessageKey, hasCompile: Boolean(compileSummary), compileStatus: compileSummary?.status || '',
666
+ compileReason: compileAdapter.localizeCompileReason(compileSummary?.reason || '', getLocale()), errors: compileSummary?.errors?.slice(0, 3) || [], errorCount: compileSummary?.errors?.length || 0, warningCount: compileSummary?.warnings?.length || 0 },
488
667
  status: writebackIncomplete ? 'failed' : 'completed',
489
668
  operations,
490
669
  applyResults: [applied],
@@ -511,7 +690,9 @@
511
690
  // the run by inspecting the `applied.skipped` entries' failure codes.
512
691
  // Existing call sites that only read `summaryLine` / `hasSkippedOperations`
513
692
  // / `audit` are unaffected.
693
+ operations,
514
694
  applied,
695
+ saveVerification,
515
696
  settlement: buildSettlement({
516
697
  operations,
517
698
  applyResult: applied,
@@ -564,25 +745,64 @@
564
745
  else appendRunEvent({ title, status: 'warning' });
565
746
  }
566
747
 
567
- async function invalidateWritebackMirror(target = {}) {
748
+ async function invalidateWritebackMirror(target = {}, options = {}) {
749
+ const invalidatePage = async () => {
750
+ if (getCurrentProjectId() !== target.runProjectId
751
+ || (target.runAccountScopeId && getAccountScopeId?.() !== target.runAccountScopeId)) return true;
752
+ try {
753
+ resetContextProject();
754
+ const response = await callPageBridge('invalidateProjectSnapshot', { invalidateFileList: true, runProjectId: target.runProjectId });
755
+ return response?.ok === true;
756
+ } catch (_error) { return false; }
757
+ };
758
+ const pageInvalidation = options.pageFirst === true ? invalidatePage() : null;
759
+ // The local mirror barrier is not shared with other tabs. Publish this
760
+ // cancellation's fence before waiting for Native, including its handshake.
761
+ const pendingFence = options.pageFirst === true && target.runProjectId
762
+ ? await recordMirrorReuseFence(target) : null;
568
763
  let invalidated = false;
764
+ let failureReason = 'Native host did not confirm mirror invalidation.';
765
+ let timer;
569
766
  try {
570
767
  if (target.runProjectId) {
571
- const response = await sendBackgroundNative({ method: 'mirror.invalidate', params: { projectId: target.runProjectId } });
768
+ const pending = sendBackgroundNative({ method: 'mirror.invalidate', params: { projectId: target.runProjectId } });
769
+ // A rejection after the bound has nobody left to observe it.
770
+ pending.catch(() => {});
771
+ const response = options.nativeTimeoutMs
772
+ ? await Promise.race([
773
+ pending,
774
+ new Promise(resolve => {
775
+ timer = setTimeout(() => resolve(null), options.nativeTimeoutMs);
776
+ })
777
+ ])
778
+ : await pending;
572
779
  invalidated = response?.ok === true && response.result?.invalidated === true;
780
+ if (!invalidated && response?.error?.message) failureReason = response.error.message;
573
781
  }
574
- } catch (_error) { /* An unavailable host cannot acknowledge invalidation. */ }
575
- let pageInvalidated = getCurrentProjectId() !== target.runProjectId;
576
- if (!pageInvalidated) {
577
- resetContextProject();
578
- try {
579
- const response = await callPageBridge('invalidateProjectSnapshot', { invalidateFileList: true, runProjectId: target.runProjectId });
580
- pageInvalidated = response?.ok === true;
581
- } catch (_error) { /* Leave the failure visible if both caches cannot be fenced. */ }
782
+ } catch (error) { failureReason = error?.message || failureReason; }
783
+ finally {
784
+ if (timer !== undefined) clearTimeout(timer);
785
+ }
786
+ const pageInvalidated = await (pageInvalidation || invalidatePage());
787
+ if (invalidated && pageInvalidated) {
788
+ if (pendingFence) {
789
+ try {
790
+ await clearMirrorReuseFenceGenerations(pendingFence.scope, [pendingFence.generation]);
791
+ const current = getMirrorReuseFence(target.runProjectId, pendingFence.scope.accountScopeId);
792
+ if (current) updateMirrorReuseFence(pendingFence.scope,
793
+ current.generations.filter(id => id !== pendingFence.generation), current.unknown);
794
+ await refreshMirrorReuseFence(target.runProjectId, pendingFence.scope.accountScopeId);
795
+ } catch (_error) { /* Retain the fence if its durable cleanup is unconfirmed. */ }
796
+ }
797
+ return 'not-attempted';
582
798
  }
583
- if (invalidated && pageInvalidated) return 'not-attempted';
799
+ if (target.runProjectId && !pendingFence) await recordMirrorReuseFence(target);
800
+ if (invalidated && !pageInvalidated) failureReason = 'The page did not confirm snapshot invalidation.';
584
801
  const event = {
585
- title: tx('The local project cache could not be reset. Check the local connection before starting another task.',
802
+ title: options.undo === true ? tx(
803
+ `Overleaf undo finished, but local mirror invalidation failed: ${failureReason}. Refresh this project or reconnect the native host before running again.`,
804
+ `Overleaf 撤销已完成,但本地镜像失效处理失败:${failureReason}。请刷新项目或重新连接 Native Host 后再运行。`
805
+ ) : tx('The local project cache could not be reset. Check the local connection before starting another task.',
586
806
  '本地项目缓存未能重置。请检查本地连接后再开始下一轮任务。'),
587
807
  status: 'warning'
588
808
  };
@@ -627,6 +847,7 @@
627
847
  if (!Array.isArray(applied?.applied) || !applied.applied.length) {
628
848
  return 'not-attempted';
629
849
  }
850
+ const mirrorFence = await refreshMirrorReuseFence(target.runProjectId, target.runAccountScopeId);
630
851
 
631
852
  if (!confirmation.snapshot && await tryConfirmMirrorWriteback(applied, target.runProjectId)) {
632
853
  appendRunEvent({
@@ -678,6 +899,7 @@
678
899
  }
679
900
  });
680
901
  if (response?.ok) {
902
+ await settleFullMirrorSync(target.runProjectId, syncedProject, mirrorFence, response);
681
903
  appendRunEvent({
682
904
  title: tx('Local Codex workspace refreshed. The next run will start from the latest Overleaf content.', '已刷新本地 Codex workspace,下一轮会从最新 Overleaf 内容开始。'),
683
905
  status: 'completed'
@@ -747,40 +969,40 @@
747
969
  let logResult = null;
748
970
  try {
749
971
  logResult = await callPageBridge('getCompileLog', {
972
+ captureId: result.compile?.captureId,
750
973
  triggerIfStale: false,
751
974
  maxAgeMs: 30000,
752
975
  waitForSaveMs: 0,
753
976
  runProjectId: options.runProjectId || getCurrentRunView()?.runProjectId || getCurrentProjectId()
754
977
  });
755
- } catch (_error) {
756
- logResult = null;
978
+ } catch (error) {
979
+ logResult = { ok: false, reason: error.message || 'compile_log_fetch_failed' };
757
980
  }
758
- const compile = result.compile;
981
+ const compile = compileAdapter.buildPostWriteCompileSummary({ result, logResult });
982
+ compile.errors = compile.errors.map(value => CompileAdapter.localizeCompileReason(value, getLocale()));
759
983
  if (compile?.status === 'success') {
760
984
  appendRunEvent({ title: tx('Compile succeeded.', '编译成功。'), status: 'completed',
761
985
  activity: activity('completed', [...(logResult?.errors || []), ...(logResult?.warnings || [])].slice(0, 5).map(CompileAdapter.formatCompileDiagnosticForSummary).join('\n')) });
762
986
  } else if (compile?.status === 'triggered') {
763
- appendRunEvent({ title: tx('Overleaf compile was triggered. The page will continue showing progress.', '已触发 Overleaf 编译;页面会继续显示编译进度。'), status: 'completed', activity: activity('triggered') });
987
+ appendRunEvent({ title: tx('Overleaf compile was triggered. The page will continue showing progress.', '已触发 Overleaf 编译;页面会继续显示编译进度。'), status: 'completed', activity: activity('triggered', compile.reason) });
764
988
  } else {
765
- appendRunEvent({ title: tx(`Compile finished with status: ${compile?.status || 'unknown'}`, `编译完成,状态:${compile?.status || '未知'}`), status: 'completed', activity: activity(compile?.status === 'failed' ? 'failed' : 'unknown') });
989
+ appendRunEvent({ title: compile.status === 'unconfirmed' ? tr('runCompileUnconfirmed')
990
+ : tx(`Compile finished with status: ${compile.status}`, `编译完成,状态:${compile.status}`),
991
+ status: 'completed', activity: activity(compile.status === 'failed' ? 'failed' : 'unknown', compile.errors.join('\n') || compile.reason) });
766
992
  }
767
- return buildPostWriteCompileSummary({ result, logResult });
993
+ return compile;
768
994
  } else {
769
995
  const reason = CompileAdapter.localizeCompileReason(result?.reason, getLocale()) || tx('unknown reason', '未知原因');
770
996
  appendRunEvent({ title: tx(`Post-write compile did not succeed: ${reason}`, `写后编译未成功:${reason}`), status: 'failed', activity: activity('failed', reason) });
771
- return buildPostWriteCompileSummary({ result });
997
+ return compileAdapter.buildPostWriteCompileSummary({ result });
772
998
  }
773
999
  } catch (error) {
774
1000
  const reason = CompileAdapter.localizeCompileReason(error.message, getLocale());
775
1001
  appendRunEvent({ title: tx(`Post-write compile failed: ${reason}`, `写后编译出错:${reason}`), status: 'failed', activity: activity('failed', reason) });
776
- return buildPostWriteCompileSummary({ error });
1002
+ return compileAdapter.buildPostWriteCompileSummary({ error });
777
1003
  }
778
1004
  }
779
1005
 
780
- function buildPostWriteCompileSummary(input = {}) {
781
- return compileAdapter.buildPostWriteCompileSummary(input);
782
- }
783
-
784
1006
  // Use the same normalized failure selection as settlement and its UI, while
785
1007
  // preserving the selected producer's full evidence on the historical report.
786
1008
  function promoteWritebackSkippedFailure(applied) {
@@ -814,6 +1036,8 @@
814
1036
  `已触发写后编译;最新日志显示 ${warnings.length} 个警告。`
815
1037
  )
816
1038
  : tx('Post-write compile was triggered; Overleaf may still be updating the result.', '已触发写后编译;Overleaf 可能仍在更新结果。');
1039
+ } else if (compileSummary.status === 'unconfirmed') {
1040
+ summary = tr('runCompileUnconfirmed');
817
1041
  } else if (compileSummary.status === 'failed') {
818
1042
  const reason = compileAdapter.localizeCompileReason(compileSummary.reason, getLocale());
819
1043
  summary = tx(
@@ -903,6 +1127,8 @@
903
1127
  getCurrentRunView,
904
1128
  } = deps);
905
1129
  assetTransferBroker = deps.assetTransferBroker;
1130
+ mirrorFenceStore = deps.mirrorFenceStore;
1131
+ getAccountScopeId = deps.getAccountScopeId;
906
1132
  stageWritebackRecovery = deps.stageWritebackRecovery;
907
1133
  settleWritebackRecovery = deps.settleWritebackRecovery;
908
1134
  onMirrorRefreshSettled = deps.onMirrorRefreshSettled;
@@ -912,6 +1138,9 @@
912
1138
  applySyncChangesToOverleaf,
913
1139
  resolveCompileLogContext,
914
1140
  invalidateMirrorAfterUndo,
1141
+ getMirrorReuseFence,
1142
+ refreshMirrorReuseFence,
1143
+ syncFencedMirror,
915
1144
  getPendingMirrorRefresh: () => pendingMirrorRefresh
916
1145
  };
917
1146
  }