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.
- package/README.md +149 -398
- package/README.zh-CN.md +153 -404
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +148 -3
- package/extension/src/backgroundSharedSessions.js +55 -2
- package/extension/src/backgroundUpdateCoordinator.js +128 -12
- package/extension/src/content/activeTurnControl.js +164 -7
- package/extension/src/content/composerAttachments.js +46 -10
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +1114 -321
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +147 -42
- package/extension/src/content/generated/content.bundle.js +5761 -1237
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/localSkillsPanel.js +6 -4
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +53 -14
- package/extension/src/content/modelPicker.js +29 -19
- package/extension/src/content/nativeCompatibilityController.js +52 -19
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +7 -7
- package/extension/src/content/panelMaintenance.js +10 -2
- package/extension/src/content/panelRenderer.js +55 -28
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +28 -19
- package/extension/src/content/recentProjects.js +30 -19
- package/extension/src/content/runActivitySummary.js +122 -14
- package/extension/src/content/runCompletionMeta.js +194 -6
- package/extension/src/content/runController.js +2 -1
- package/extension/src/content/runFailureNotice.js +8 -1
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +27 -13
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +73 -24
- package/extension/src/content/runReviewController.js +102 -37
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +124 -46
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sensitivePreflight.js +14 -1
- package/extension/src/content/sessionManager.js +49 -3
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +15 -2
- package/extension/src/content/settingsWorkbench.js +127 -89
- package/extension/src/content/sharedSessionViewSync.js +13 -4
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +301 -72
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +172 -113
- package/extension/src/pageBridge.js +124 -73
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +372 -18
- package/extension/src/shared/auditRecords.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +27 -7
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +57 -3
- package/extension/src/shared/i18n.js +315 -29
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/sessionState.js +63 -4
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/sharedSessionBridge.js +81 -13
- package/extension/src/shared/staleGuard.js +130 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +111 -2
- package/extension/src/shared/storageRunActions.js +62 -8
- package/extension/src/shared/undoOperations.js +126 -28
- package/extension/src/shared/updateRuntimeIdentity.js +16 -2
- package/extension/src/shared/writebackSettlement.js +21 -3
- package/extension/styles/panel.css +1293 -549
- package/native-host/src/bootstrapUpdate.js +52 -16
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +10 -1
- package/native-host/src/chatCompletionsBridge.js +29 -2
- package/native-host/src/codexPromptAssembly.js +86 -118
- package/native-host/src/codexSessionRunner.js +127 -17
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +28 -8
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +144 -21
- package/native-host/src/writingStyleRuntime.js +1 -1
- 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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 || '',
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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 (
|
|
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:
|
|
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 (
|
|
756
|
-
logResult =
|
|
978
|
+
} catch (error) {
|
|
979
|
+
logResult = { ok: false, reason: error.message || 'compile_log_fetch_failed' };
|
|
757
980
|
}
|
|
758
|
-
const compile = result
|
|
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:
|
|
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
|
|
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
|
}
|