codex-overleaf-link 2.6.0 → 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 +9 -9
- package/README.zh-CN.md +9 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +70 -0
- package/extension/src/content/composerAttachments.js +35 -8
- package/extension/src/content/contentRuntime.js +200 -51
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +129 -42
- package/extension/src/content/generated/content.bundle.js +1283 -331
- package/extension/src/content/generated/content.bundle.meta.json +3 -3
- package/extension/src/content/localSkillsPanel.js +4 -2
- package/extension/src/content/markdownText.js +11 -9
- package/extension/src/content/modelPicker.js +28 -19
- package/extension/src/content/nativeCompatibilityController.js +23 -9
- package/extension/src/content/pageBridgeClient.js +5 -5
- package/extension/src/content/panelMaintenance.js +4 -0
- package/extension/src/content/panelRenderer.js +4 -1
- package/extension/src/content/providerSettingsDialog.js +26 -18
- package/extension/src/content/recentProjects.js +26 -16
- package/extension/src/content/runQueueScheduler.js +19 -9
- package/extension/src/content/runResultActions.js +51 -4
- package/extension/src/content/runReviewController.js +9 -3
- package/extension/src/content/scopedPersistencePanelState.js +34 -25
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sharedSessionViewSync.js +9 -1
- package/extension/src/content/writebackOrchestrator.js +266 -50
- package/extension/src/content/writingStyleSettings.js +70 -17
- 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/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +152 -40
- package/extension/src/pageBridge.js +17 -7
- package/extension/src/shared/agentTranscript.js +23 -2
- package/extension/src/shared/auditRecords.js +1 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/composerAttachmentDraft.js +22 -7
- package/extension/src/shared/failureReasons.js +15 -0
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/staleGuard.js +58 -0
- package/extension/src/shared/storageMigration.js +104 -1
- package/extension/styles/panel.css +191 -63
- package/native-host/src/codexSessionRunner.js +1 -0
- 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/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +11 -3
- package/native-host/src/updateManager.js +24 -6
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +1 -1
|
@@ -246,6 +246,8 @@ function undoOwner(run) {
|
|
|
246
246
|
async function executeUndo(runId) {
|
|
247
247
|
let run = findRunRecord(runId);
|
|
248
248
|
if (run) Object.assign(run, Modules.UndoOperations.prepareRunUndo(run));
|
|
249
|
+
const mirrorOwner = { projectId: getRunProjectIdForWriteback(run), runAccountScopeId: run?.runAccountScopeId || getAccountScopeId?.() || '',
|
|
250
|
+
sessionId: getState().activeSessionId };
|
|
249
251
|
const assertCurrent = undoOwner(run);
|
|
250
252
|
let assertInputs = checkpointGuard(run, assertCurrent);
|
|
251
253
|
if (!getRunUndoCount(run) || run.undoStatus === 'applied') {
|
|
@@ -338,9 +340,11 @@ async function executeUndo(runId) {
|
|
|
338
340
|
// Undo remains bound to its original project, including after navigation.
|
|
339
341
|
runProjectId: getRunProjectIdForWriteback(run)
|
|
340
342
|
});
|
|
343
|
+
// The Page may have changed the original document even if this view moved
|
|
344
|
+
// while awaiting its reply. Fence that project's mirror before rechecking.
|
|
345
|
+
writebackOrchestrator.invalidateMirrorAfterUndo(runId, mirrorOwner.projectId, result, mirrorOwner);
|
|
341
346
|
run = assertInputs();
|
|
342
347
|
const undoApplied = isUndoResultEffectivelyApplied(run, result);
|
|
343
|
-
writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
|
|
344
348
|
appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
|
|
345
349
|
appendRunRecordEvent(runId, {
|
|
346
350
|
title: tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
|
|
@@ -369,6 +373,8 @@ async function executeUndo(runId) {
|
|
|
369
373
|
}
|
|
370
374
|
|
|
371
375
|
async function executeTrackedUndo(runId, run) {
|
|
376
|
+
const mirrorOwner = { projectId: getRunProjectIdForWriteback(run), runAccountScopeId: run?.runAccountScopeId || getAccountScopeId?.() || '',
|
|
377
|
+
sessionId: getState().activeSessionId };
|
|
372
378
|
const assertCurrent = undoOwner(run);
|
|
373
379
|
let assertInputs = checkpointGuard(run, assertCurrent);
|
|
374
380
|
const fileUndoOperations = Modules.UndoOperations.getFileUndoOperations(run);
|
|
@@ -452,8 +458,8 @@ async function executeTrackedUndo(runId, run) {
|
|
|
452
458
|
operation, result: { ok: false, code: 'undo_operation_failed', reason: error.message }
|
|
453
459
|
})) };
|
|
454
460
|
}
|
|
455
|
-
run = assertInputs();
|
|
456
461
|
result = Modules.UndoOperations.mergeFileUndoResult(result, createdResult, fileUndoOperations);
|
|
462
|
+
run = assertInputs();
|
|
457
463
|
}
|
|
458
464
|
appendRunRecordEvent(runId, {
|
|
459
465
|
title: fileUndoOperations.length ? tr('undoResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }) : trackedUndo
|
|
@@ -484,7 +490,7 @@ async function executeTrackedUndo(runId, run) {
|
|
|
484
490
|
await applyLegacyUndoSettlement(runId, result.ok === true && !result.skipped?.length ? 'applied' : 'partial', result);
|
|
485
491
|
} finally {
|
|
486
492
|
// Known Page changes require invalidation even when a durable checkpoint fails.
|
|
487
|
-
if (result) writebackOrchestrator.invalidateMirrorAfterUndo(runId,
|
|
493
|
+
if (result) writebackOrchestrator.invalidateMirrorAfterUndo(runId, mirrorOwner.projectId, result, mirrorOwner);
|
|
488
494
|
}
|
|
489
495
|
}
|
|
490
496
|
|
|
@@ -134,6 +134,19 @@
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
function projectPreferenceValues(input, prefs, projectId) {
|
|
138
|
+
return {
|
|
139
|
+
experimentalOtByProject: input.normalizeExperimentalOtByProject(prefs.experimentalOtByProject)[projectId] === true,
|
|
140
|
+
reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject?.[projectId],
|
|
141
|
+
governanceRulesByProject: input.normalizeGovernanceRulesByProject({
|
|
142
|
+
[projectId]: prefs.governanceRulesByProject?.[projectId] || {}
|
|
143
|
+
})[projectId],
|
|
144
|
+
customInstructionsByProject: input.normalizeCustomInstructionsByProject({
|
|
145
|
+
[projectId]: prefs.customInstructionsByProject?.[projectId] || ''
|
|
146
|
+
})[projectId]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
137
150
|
async function persistPanelState(input) {
|
|
138
151
|
var state = cloneSnapshot(input.state || {});
|
|
139
152
|
var compactState = cloneSnapshot(input.compactState || {});
|
|
@@ -163,6 +176,11 @@
|
|
|
163
176
|
var reviewRuns = (input.reviewRunIds || []).map(id =>
|
|
164
177
|
(state.sessions || []).flatMap(session => session.runs || []).find(run => run.id === id));
|
|
165
178
|
if (reviewRuns.some(run => !run)) throw reviewPersistenceError();
|
|
179
|
+
var preferenceBaseline = input.projectPreferenceBaseline
|
|
180
|
+
|| Migration.getProjectPreferenceBaseline?.(accountScopeId, projectId);
|
|
181
|
+
var preferenceWrite = input.projectPreferenceWrite || Migration.captureProjectPreferenceWrite?.({
|
|
182
|
+
...input, state: state, baseline: preferenceBaseline, accountScopeId: accountScopeId, projectId: projectId
|
|
183
|
+
});
|
|
166
184
|
var latestPrefs = typeof Migration.loadPrefs === 'function'
|
|
167
185
|
? await Migration.loadPrefs(accountScopeId, projectId)
|
|
168
186
|
: {};
|
|
@@ -176,32 +194,23 @@
|
|
|
176
194
|
projectId,
|
|
177
195
|
compactState.activeSessionId || state.activeSessionId || ''
|
|
178
196
|
);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{},
|
|
196
|
-
latestPrefs && latestPrefs.customInstructionsByProject || {}
|
|
197
|
-
);
|
|
198
|
-
if (customProjectId) {
|
|
199
|
-
prefs.customInstructionsByProject[customProjectId] =
|
|
200
|
-
Object.prototype.hasOwnProperty.call(currentCustom, customProjectId)
|
|
201
|
-
? currentCustom[customProjectId]
|
|
202
|
-
: '';
|
|
197
|
+
var currentPreferences = projectPreferenceValues(input, state, projectId);
|
|
198
|
+
if (preferenceWrite) {
|
|
199
|
+
Object.assign(prefs, Migration.projectPreferencePatch(preferenceWrite, accountScopeId, projectId));
|
|
200
|
+
} else {
|
|
201
|
+
var baselinePreferences = projectPreferenceValues(input, preferenceBaseline || latestPrefs || {}, projectId);
|
|
202
|
+
for (var name of Object.keys(currentPreferences)) {
|
|
203
|
+
prefs[name] = JSON.stringify(currentPreferences[name]) === JSON.stringify(baselinePreferences[name])
|
|
204
|
+
? {} : { [projectId]: currentPreferences[name] };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
var savedPrefs = await Migration.savePrefs(prefs, accountScopeId, projectId);
|
|
208
|
+
if (savedPrefs) {
|
|
209
|
+
var fields = preferenceWrite ? Migration.completeProjectPreferenceWrite(preferenceWrite, savedPrefs)
|
|
210
|
+
: Object.keys(currentPreferences);
|
|
211
|
+
var applied = input.applyProjectPreferences?.(savedPrefs, state, fields);
|
|
212
|
+
if (!preferenceWrite && applied !== false) Migration.adoptProjectPreferenceBaseline?.(savedPrefs, accountScopeId, projectId);
|
|
203
213
|
}
|
|
204
|
-
await Migration.savePrefs(prefs, accountScopeId, projectId);
|
|
205
214
|
var persistenceMeta = input.persistenceContext?.nextMeta || {};
|
|
206
215
|
var queueTombstones = persistenceMeta.queueTombstones || {};
|
|
207
216
|
var sessionTombstones = persistenceMeta.sessionTombstones || {};
|
|
@@ -133,6 +133,28 @@
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
async function attachInstruction(action, selected) {
|
|
137
|
+
const selection = await attach('edit', selected);
|
|
138
|
+
if (!selection) return;
|
|
139
|
+
const task = deps.getPanel()?.querySelector('[data-task]');
|
|
140
|
+
if (!task) return;
|
|
141
|
+
if (task.value.trim()) {
|
|
142
|
+
deps.toast?.(tx('Kept your draft. The selection is attached for editing.', '已保留你的草稿,选区已附上,仅编辑此处。'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const instructions = {
|
|
146
|
+
polish: tx('Polish the selected passage for clarity and flow. Keep the meaning, citations, labels and math unchanged.',
|
|
147
|
+
'润色选中段落,让表达更清晰流畅,保持原意以及引用、标签、公式不变。'),
|
|
148
|
+
shorten: tx('Shorten the selected passage by about 20%. Keep citations, labels, math and the main claim unchanged.',
|
|
149
|
+
'将选中段落精简约 20%,保留引用、标签、公式和核心论点不变。'),
|
|
150
|
+
translate: tx('Translate the selected passage into English. Keep LaTeX commands, citations, labels and math unchanged.',
|
|
151
|
+
'将选中段落翻译成英文,保留 LaTeX 命令、引用、标签和公式不变。')
|
|
152
|
+
};
|
|
153
|
+
task.value = instructions[action];
|
|
154
|
+
task.setSelectionRange(task.value.length, task.value.length);
|
|
155
|
+
task.dispatchEvent(new Event('input', { bubbles: true }));
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
function editorFor(node) {
|
|
137
159
|
const element = node?.nodeType === 1 ? node : node?.parentElement;
|
|
138
160
|
const editor = element?.closest?.('.cm-editor');
|
|
@@ -164,6 +186,9 @@
|
|
|
164
186
|
toolbar.setAttribute('aria-label', tx('Selected text actions', '选区操作'));
|
|
165
187
|
toolbar.querySelector('[data-selection-action="reference"]').textContent = tx('Add to Chat', '加入对话');
|
|
166
188
|
toolbar.querySelector('[data-selection-action="edit"]').textContent = tx('Edit Selection', '仅编辑此处');
|
|
189
|
+
toolbar.querySelector('[data-selection-action="polish"]').textContent = tx('Polish', '润色');
|
|
190
|
+
toolbar.querySelector('[data-selection-action="shorten"]').textContent = tx('Shorten', '精简');
|
|
191
|
+
toolbar.querySelector('[data-selection-action="translate"]').textContent = tx('Translate', '翻译');
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
function ensureToolbar() {
|
|
@@ -174,8 +199,8 @@
|
|
|
174
199
|
toolbar.setAttribute('role', 'toolbar');
|
|
175
200
|
toolbar.hidden = true;
|
|
176
201
|
if (typeof toolbar.showPopover === 'function') toolbar.setAttribute('popover', 'manual');
|
|
177
|
-
for (const mode of ['reference', 'edit']) {
|
|
178
|
-
if (mode === 'edit') {
|
|
202
|
+
for (const mode of ['reference', 'edit', 'polish', 'shorten', 'translate']) {
|
|
203
|
+
if (mode === 'edit' || mode === 'polish') {
|
|
179
204
|
const divider = document.createElement('span');
|
|
180
205
|
divider.className = 'codex-selection-action-divider';
|
|
181
206
|
divider.setAttribute('aria-hidden', 'true');
|
|
@@ -186,7 +211,9 @@
|
|
|
186
211
|
button.dataset.selectionAction = mode;
|
|
187
212
|
button.addEventListener('click', () => {
|
|
188
213
|
const selected = candidate;
|
|
189
|
-
if (selected)
|
|
214
|
+
if (!selected) return;
|
|
215
|
+
if (mode === 'reference' || mode === 'edit') void attach(mode, selected);
|
|
216
|
+
else void attachInstruction(mode, selected);
|
|
190
217
|
});
|
|
191
218
|
toolbar.append(button);
|
|
192
219
|
}
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
function applySnapshot(options, sessions) {
|
|
11
11
|
const previous = options.getState(), scope = options.getScope();
|
|
12
|
-
const
|
|
12
|
+
const input = sessions.length ? { ...previous, sessions } : {
|
|
13
|
+
...previous, session: null, sessions: [], runs: [], activeSessionId: '',
|
|
14
|
+
task: '', focusFiles: [], selectionContext: null, composerAttachments: []
|
|
15
|
+
};
|
|
16
|
+
const next = options.normalize(input, { restoreRunningRuns: false });
|
|
13
17
|
const unchangedSnapshot = JSON.stringify(next);
|
|
14
18
|
options.setState(next);
|
|
15
19
|
try {
|
|
@@ -88,6 +92,10 @@
|
|
|
88
92
|
}
|
|
89
93
|
const unsubscribe = Bridge.subscribe(change => {
|
|
90
94
|
const scope = getScope();
|
|
95
|
+
if (options.isDashboard?.()) {
|
|
96
|
+
if (scope.accountScopeId && change.accountScopeId === scope.accountScopeId) options.refreshDashboard?.();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
91
99
|
if (!change.projectId || change.projectId === scope.projectId) {
|
|
92
100
|
retryCount = 0; pending = true; schedule();
|
|
93
101
|
}
|
|
@@ -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
|
|
@@ -418,9 +573,35 @@
|
|
|
418
573
|
// zero-write run (every operation skipped) has nothing to save-verify, so
|
|
419
574
|
// skip the ~5s probe and its misleading "could not verify saved" warning
|
|
420
575
|
// (v1.6.2).
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|
|
424
605
|
const saveVerification = confirmation.verification;
|
|
425
606
|
await settleWritebackRecovery?.(applied, project, saveVerification, saveTarget, runRequireReviewing);
|
|
426
607
|
if (appliedPaths.length) {
|
|
@@ -432,24 +613,11 @@
|
|
|
432
613
|
// pending promise is exposed so undoRun and the next runTask barrier on
|
|
433
614
|
// it — a mirror.sync landing AFTER an undo would otherwise push the
|
|
434
615
|
// pre-undo snapshot as the local baseline.
|
|
435
|
-
|
|
436
|
-
pendingMirrorRefresh = refreshProjectMirrorAfterWriteback(project, applied, saveVerification, { target: saveTarget, snapshot: confirmation.snapshot })
|
|
616
|
+
trackWritebackMirror(refreshProjectMirrorAfterWriteback(project, applied, saveVerification, { target: saveTarget, snapshot: confirmation.snapshot })
|
|
437
617
|
.catch(async error => {
|
|
438
618
|
appendRunRecordEvent(saveTarget.recordId, { kind: 'technical', title: 'Mirror refresh requires a fresh read', status: 'info', detail: { reason: error.message } });
|
|
439
619
|
return invalidateWritebackMirror(saveTarget);
|
|
440
|
-
})
|
|
441
|
-
.then(state => {
|
|
442
|
-
if (typeof onMirrorRefreshSettled !== 'function') return;
|
|
443
|
-
return onMirrorRefreshSettled({
|
|
444
|
-
recordId: mirrorSettlementTarget?.recordId || '',
|
|
445
|
-
sessionId: mirrorSettlementTarget?.sessionId || '',
|
|
446
|
-
runProjectId: mirrorSettlementTarget?.runProjectId || '',
|
|
447
|
-
state: state || 'not-attempted'
|
|
448
|
-
});
|
|
449
|
-
})
|
|
450
|
-
.finally(() => {
|
|
451
|
-
pendingMirrorRefresh = null;
|
|
452
|
-
});
|
|
620
|
+
}), saveTarget);
|
|
453
621
|
const compileSummary = appliedPaths.length
|
|
454
622
|
? await autoRecompileAfterWriteback(appliedPaths, saveVerification, {
|
|
455
623
|
autoRecompile: options.autoRecompile,
|
|
@@ -522,7 +690,9 @@
|
|
|
522
690
|
// the run by inspecting the `applied.skipped` entries' failure codes.
|
|
523
691
|
// Existing call sites that only read `summaryLine` / `hasSkippedOperations`
|
|
524
692
|
// / `audit` are unaffected.
|
|
693
|
+
operations,
|
|
525
694
|
applied,
|
|
695
|
+
saveVerification,
|
|
526
696
|
settlement: buildSettlement({
|
|
527
697
|
operations,
|
|
528
698
|
applyResult: applied,
|
|
@@ -575,25 +745,64 @@
|
|
|
575
745
|
else appendRunEvent({ title, status: 'warning' });
|
|
576
746
|
}
|
|
577
747
|
|
|
578
|
-
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;
|
|
579
763
|
let invalidated = false;
|
|
764
|
+
let failureReason = 'Native host did not confirm mirror invalidation.';
|
|
765
|
+
let timer;
|
|
580
766
|
try {
|
|
581
767
|
if (target.runProjectId) {
|
|
582
|
-
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;
|
|
583
779
|
invalidated = response?.ok === true && response.result?.invalidated === true;
|
|
780
|
+
if (!invalidated && response?.error?.message) failureReason = response.error.message;
|
|
584
781
|
}
|
|
585
|
-
} catch (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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';
|
|
593
798
|
}
|
|
594
|
-
if (
|
|
799
|
+
if (target.runProjectId && !pendingFence) await recordMirrorReuseFence(target);
|
|
800
|
+
if (invalidated && !pageInvalidated) failureReason = 'The page did not confirm snapshot invalidation.';
|
|
595
801
|
const event = {
|
|
596
|
-
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.',
|
|
597
806
|
'本地项目缓存未能重置。请检查本地连接后再开始下一轮任务。'),
|
|
598
807
|
status: 'warning'
|
|
599
808
|
};
|
|
@@ -638,6 +847,7 @@
|
|
|
638
847
|
if (!Array.isArray(applied?.applied) || !applied.applied.length) {
|
|
639
848
|
return 'not-attempted';
|
|
640
849
|
}
|
|
850
|
+
const mirrorFence = await refreshMirrorReuseFence(target.runProjectId, target.runAccountScopeId);
|
|
641
851
|
|
|
642
852
|
if (!confirmation.snapshot && await tryConfirmMirrorWriteback(applied, target.runProjectId)) {
|
|
643
853
|
appendRunEvent({
|
|
@@ -689,6 +899,7 @@
|
|
|
689
899
|
}
|
|
690
900
|
});
|
|
691
901
|
if (response?.ok) {
|
|
902
|
+
await settleFullMirrorSync(target.runProjectId, syncedProject, mirrorFence, response);
|
|
692
903
|
appendRunEvent({
|
|
693
904
|
title: tx('Local Codex workspace refreshed. The next run will start from the latest Overleaf content.', '已刷新本地 Codex workspace,下一轮会从最新 Overleaf 内容开始。'),
|
|
694
905
|
status: 'completed'
|
|
@@ -916,6 +1127,8 @@
|
|
|
916
1127
|
getCurrentRunView,
|
|
917
1128
|
} = deps);
|
|
918
1129
|
assetTransferBroker = deps.assetTransferBroker;
|
|
1130
|
+
mirrorFenceStore = deps.mirrorFenceStore;
|
|
1131
|
+
getAccountScopeId = deps.getAccountScopeId;
|
|
919
1132
|
stageWritebackRecovery = deps.stageWritebackRecovery;
|
|
920
1133
|
settleWritebackRecovery = deps.settleWritebackRecovery;
|
|
921
1134
|
onMirrorRefreshSettled = deps.onMirrorRefreshSettled;
|
|
@@ -925,6 +1138,9 @@
|
|
|
925
1138
|
applySyncChangesToOverleaf,
|
|
926
1139
|
resolveCompileLogContext,
|
|
927
1140
|
invalidateMirrorAfterUndo,
|
|
1141
|
+
getMirrorReuseFence,
|
|
1142
|
+
refreshMirrorReuseFence,
|
|
1143
|
+
syncFencedMirror,
|
|
928
1144
|
getPendingMirrorRefresh: () => pendingMirrorRefresh
|
|
929
1145
|
};
|
|
930
1146
|
}
|