codex-overleaf-link 2.5.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +78 -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 +11 -2
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +920 -276
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsPanel.js +18 -0
- package/extension/src/content/generated/content.bundle.js +4496 -924
- 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 +2 -2
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +42 -5
- package/extension/src/content/modelPicker.js +1 -0
- package/extension/src/content/nativeCompatibilityController.js +29 -10
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +2 -2
- package/extension/src/content/panelMaintenance.js +6 -2
- package/extension/src/content/panelRenderer.js +51 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +2 -1
- package/extension/src/content/recentProjects.js +4 -3
- 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 +9 -5
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +22 -20
- package/extension/src/content/runReviewController.js +94 -35
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +92 -23
- 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 +4 -3
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +35 -22
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/writebackRouter.js +24 -77
- package/extension/src/pageBridge.js +107 -66
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +350 -17
- package/extension/src/shared/auditRecords.js +2 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +6 -1
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +42 -3
- package/extension/src/shared/i18n.js +299 -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/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -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 +72 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +7 -1
- 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 +1106 -490
- 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 +126 -17
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/taskRunnerRuntime.js +18 -6
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +120 -15
- package/package.json +5 -8
|
@@ -189,6 +189,17 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
189
189
|
.catch(error => sendResponse({ ok: false, journals: [], error: getErrorMessage(error, 'Journal read failed.') }));
|
|
190
190
|
return true;
|
|
191
191
|
}
|
|
192
|
+
if (message?.type === 'codex-overleaf/run-journal/known') {
|
|
193
|
+
if (!isAllowedOverleafSender(sender)) {
|
|
194
|
+
sendResponse({ ok: false, known: [] });
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
knownRunJournals(Array.isArray(message.requestIds) ? message.requestIds.map(String).slice(0, 64) : [],
|
|
198
|
+
Array.isArray(message.owners) ? message.owners.slice(0, 64) : [])
|
|
199
|
+
.then(result => sendResponse({ ok: true, ...result }))
|
|
200
|
+
.catch(error => sendResponse({ ok: false, known: [], error: getErrorMessage(error, 'Journal read failed.') }));
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
192
203
|
if (message?.type === 'codex-overleaf/run-journal/ack') {
|
|
193
204
|
if (!isAllowedOverleafSender(sender)) {
|
|
194
205
|
sendResponse({ ok: false });
|
|
@@ -266,6 +277,9 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
266
277
|
return;
|
|
267
278
|
}
|
|
268
279
|
if (message.type === 'release') {
|
|
280
|
+
const current = ownerBindings.get(portConnection);
|
|
281
|
+
if (!current || ['requestId', 'projectKey', 'clientRunId', 'sessionId'].some(key => current[key] !== message[key])
|
|
282
|
+
|| (current.browserOnly && current.ownerToken !== message.ownerToken)) return;
|
|
269
283
|
ownerBindings.delete(portConnection);
|
|
270
284
|
clearTimeout(ownerCancelTimers.get(requestId));
|
|
271
285
|
ownerCancelTimers.delete(requestId);
|
|
@@ -278,9 +292,20 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
278
292
|
documentId: String(portConnection.sender?.documentId || ''),
|
|
279
293
|
tabId: portConnection.sender?.tab?.id,
|
|
280
294
|
clientRunId: String(message.clientRunId || ''),
|
|
281
|
-
sessionId: String(message.sessionId || '')
|
|
295
|
+
sessionId: String(message.sessionId || ''),
|
|
296
|
+
browserOnly: message.browserOnly === true,
|
|
297
|
+
ownerToken: String(message.ownerToken || '')
|
|
282
298
|
};
|
|
299
|
+
if (binding.browserOnly && (!binding.projectKey || !binding.clientRunId || !binding.sessionId || !binding.ownerToken)) return;
|
|
300
|
+
if (binding.browserOnly && managedUpdateExecutionLocked) {
|
|
301
|
+
try { portConnection.postMessage?.({ type: 'bound', ...binding, ok: false }); } catch (_error) { /* port disconnected */ }
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
283
304
|
ownerBindings.set(portConnection, binding);
|
|
305
|
+
if (binding.browserOnly) {
|
|
306
|
+
try { portConnection.postMessage?.({ type: 'bound', ...binding }); } catch (_error) { /* port disconnected */ }
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
284
309
|
restoreRunJournal(binding).then(journal => {
|
|
285
310
|
if (ownerBindings.get(portConnection) !== binding || !journal || runJournals.get(requestId) !== journal) return;
|
|
286
311
|
clearTimeout(ownerCancelTimers.get(requestId));
|
|
@@ -295,7 +320,7 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
295
320
|
portConnection.onDisconnect.addListener(() => {
|
|
296
321
|
const binding = ownerBindings.get(portConnection);
|
|
297
322
|
ownerBindings.delete(portConnection);
|
|
298
|
-
if (!binding?.requestId) {
|
|
323
|
+
if (!binding?.requestId || binding.browserOnly) {
|
|
299
324
|
return;
|
|
300
325
|
}
|
|
301
326
|
const timer = setTimeout(() => interruptOwnerlessRun(binding), 150);
|
|
@@ -395,7 +420,8 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
395
420
|
payload = { ...payload, params: { ...payload.params,
|
|
396
421
|
bootstrapRevision: String(globalThis.__CODEX_OVERLEAF_BOOTSTRAP_REVISION__ || '') } };
|
|
397
422
|
}
|
|
398
|
-
if (managedBootstrapRuntime && payload?.method === 'update.apply'
|
|
423
|
+
if (managedBootstrapRuntime && (payload?.method === 'update.apply'
|
|
424
|
+
|| (payload?.method === 'update.rollback' && payload?.params?.reasonCode === 'update_user_rollback'))) {
|
|
399
425
|
const gate = await verifyManagedUpdateSafety();
|
|
400
426
|
if (!gate.ok) {
|
|
401
427
|
return gate;
|
|
@@ -450,6 +476,7 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
450
476
|
}
|
|
451
477
|
const pendingState = globalThis.CodexOverleafNativeBridge?.getPendingState?.() || {};
|
|
452
478
|
const backgroundBlockers = Number(pendingState.executionRequests || 0) > 0
|
|
479
|
+
|| Array.from(ownerBindings.values()).some(binding => binding.browserOnly)
|
|
453
480
|
? ['background_execution_pending']
|
|
454
481
|
: [];
|
|
455
482
|
const blockers = [
|
|
@@ -995,6 +1022,9 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
995
1022
|
clientRunId: String(params.clientRunId || ''),
|
|
996
1023
|
sessionId: String(params.clientSessionId || ''),
|
|
997
1024
|
task: sanitizeJournalString(params.task).slice(0, 12000),
|
|
1025
|
+
...(params.operation === 'compact' ? {
|
|
1026
|
+
operation: 'compact', threadId: String(params.threadId || '').slice(0, 160)
|
|
1027
|
+
} : {}),
|
|
998
1028
|
mode: String(params.mode || ''),
|
|
999
1029
|
model: String(params.model || ''),
|
|
1000
1030
|
ownerTabId: pendingRequest.tabId,
|
|
@@ -1176,6 +1206,51 @@ if (!globalThis.CodexOverleafBackgroundUpdateSafety) {
|
|
|
1176
1206
|
return entries.slice(-16).map(([_key, value]) => value);
|
|
1177
1207
|
}
|
|
1178
1208
|
|
|
1209
|
+
// Browser-only retries outlive their original Native journal. Protect only
|
|
1210
|
+
// a live port whose request/project/session/run matches the queried record.
|
|
1211
|
+
async function knownRunJournals(requestIds, owners = []) {
|
|
1212
|
+
await Promise.allSettled(Array.from(journalLoads.values(), load => load.promise));
|
|
1213
|
+
await Promise.allSettled(Array.from(journalWrites.values()));
|
|
1214
|
+
const storage = chrome.storage?.session;
|
|
1215
|
+
if (!storage) throw new Error('Run journal storage is unavailable.');
|
|
1216
|
+
const values = requestIds.length ? await storage.get(requestIds.map(id => RUN_JOURNAL_PREFIX + id)) : {};
|
|
1217
|
+
// A persisted startup snapshot may lag behind its owner's Native dispatch.
|
|
1218
|
+
// Keep that same project/session/run alive through the request-id transition.
|
|
1219
|
+
const matches = (candidate, binding) => (candidate?.preDispatch === true
|
|
1220
|
+
? ['projectKey', 'clientRunId', 'sessionId'] : ['requestId', 'projectKey', 'clientRunId', 'sessionId'])
|
|
1221
|
+
.every(key => Boolean(candidate?.[key]) && candidate[key] === binding?.[key]);
|
|
1222
|
+
const known = requestIds.filter(id => runJournals.has(id) || Boolean(values?.[RUN_JOURNAL_PREFIX + id])
|
|
1223
|
+
|| owners.some(candidate => candidate?.requestId === id && Array.from(ownerBindings.values()).some(binding => matches(candidate, binding))));
|
|
1224
|
+
const missing = owners.filter(candidate => requestIds.includes(candidate?.requestId) && !known.includes(candidate.requestId));
|
|
1225
|
+
if (!missing.length) return { known, ownersChecked: true, preDispatchChecked: true };
|
|
1226
|
+
// A worker restart drops ports before hidden pages can process reconnect
|
|
1227
|
+
// timers. The page's existing health receiver can still report its binding.
|
|
1228
|
+
const unknown = () => ({ known, ownersChecked: false, preDispatchChecked: false, retryAfterMs: 1000 });
|
|
1229
|
+
const tabs = await chrome.tabs.query({ url: MANAGED_OVERLEAF_MATCHES }).catch(() => null);
|
|
1230
|
+
if (!Array.isArray(tabs)) return unknown();
|
|
1231
|
+
const projects = new Set(missing.map(candidate => candidate.projectKey));
|
|
1232
|
+
const relevant = tabs.filter(tab => isManagedEditorTab(tab) && !tab.discarded && tab.status !== 'unloaded'
|
|
1233
|
+
&& projects.has(new URL(tab.url).pathname.split('/')[2]));
|
|
1234
|
+
const responses = await Promise.all(relevant.map(async tab => {
|
|
1235
|
+
let timer;
|
|
1236
|
+
try {
|
|
1237
|
+
return await Promise.race([
|
|
1238
|
+
chrome.tabs.sendMessage(tab.id, { type: 'codex-overleaf/runtime-health-probe' }, { frameId: 0 }),
|
|
1239
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(null), 1500); })
|
|
1240
|
+
]);
|
|
1241
|
+
} catch (_error) { return null; }
|
|
1242
|
+
finally { clearTimeout(timer); }
|
|
1243
|
+
}));
|
|
1244
|
+
const needsStartupInventory = missing.some(candidate => candidate.preDispatch === true);
|
|
1245
|
+
if (responses.some(response => response?.ok !== true || ![1, 2].includes(response.runOwnerProtocol)
|
|
1246
|
+
|| (needsStartupInventory && response.runOwnerProtocol !== 2) || !Array.isArray(response.runOwners))) return unknown();
|
|
1247
|
+
for (const candidate of missing) {
|
|
1248
|
+
if (responses.some(response => response.runOwners.some(binding => matches(candidate, binding)))
|
|
1249
|
+
|| Array.from(ownerBindings.values()).some(binding => matches(candidate, binding))) known.push(candidate.requestId);
|
|
1250
|
+
}
|
|
1251
|
+
return { known, ownersChecked: true, preDispatchChecked: true };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1179
1254
|
function pruneInMemoryRunJournals() {
|
|
1180
1255
|
if (runJournals.size <= 16) {
|
|
1181
1256
|
return;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
const clone = value => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
17
17
|
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
18
18
|
const failure = (code, message) => Object.assign(new Error(message), { code });
|
|
19
|
+
const conflict = () => failure('shared_session_conflict', 'The same session was edited in another tab. Refresh its history before retrying; the local draft was kept.');
|
|
19
20
|
function part(value) {
|
|
20
21
|
if (typeof value !== 'string' || !value.trim() || value.length > 256 || /[\0\r\n]/.test(value))
|
|
21
22
|
throw failure('shared_store_scope_invalid', 'The shared session identity is invalid.');
|
|
@@ -71,7 +72,59 @@
|
|
|
71
72
|
}
|
|
72
73
|
return result;
|
|
73
74
|
}
|
|
74
|
-
throw
|
|
75
|
+
throw conflict();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function titleValue(record) {
|
|
79
|
+
return { title: record.title || '', titleSource: record.titleSource === 'manual' ? 'manual' : 'auto' };
|
|
80
|
+
}
|
|
81
|
+
function manualTitleChange(base, next) {
|
|
82
|
+
return next.titleSource === 'manual' ? next.title !== base.title : base.titleSource === 'manual';
|
|
83
|
+
}
|
|
84
|
+
function mergeTitle(base, current, incoming) {
|
|
85
|
+
const before = titleValue(base), stored = titleValue(current), proposed = titleValue(incoming);
|
|
86
|
+
if (equal(proposed, before)) return stored;
|
|
87
|
+
if (equal(stored, before) || equal(stored, proposed)) return proposed;
|
|
88
|
+
// Auto derivations (including legacy normalization) carry no rename intent.
|
|
89
|
+
if (!manualTitleChange(before, proposed)) return stored;
|
|
90
|
+
if (!manualTitleChange(before, stored)) return proposed;
|
|
91
|
+
// The pair is one intent; merging its scalar members separately could
|
|
92
|
+
// combine a peer rename with this page's reset to auto.
|
|
93
|
+
throw conflict();
|
|
94
|
+
}
|
|
95
|
+
function draftConsumption(record) {
|
|
96
|
+
const marker = record.draftConsumption;
|
|
97
|
+
return record.task === '' && typeof marker?.task === 'string' && marker.task
|
|
98
|
+
&& typeof marker.runId === 'string' && marker.runId
|
|
99
|
+
&& (record.runs || []).some(run => run.id === marker.runId && !run.forkSnapshot
|
|
100
|
+
&& run.runProjectId === record.projectId) ? marker : null;
|
|
101
|
+
}
|
|
102
|
+
function mergeSession(base, current, incoming) {
|
|
103
|
+
const currentConsumption = draftConsumption(current), incomingConsumption = draftConsumption(incoming);
|
|
104
|
+
const consumed = marker => marker && marker.task === base.task && !equal(marker, base.draftConsumption);
|
|
105
|
+
let task;
|
|
106
|
+
if (equal(incoming.task, base.task)) task = current.task;
|
|
107
|
+
else if (equal(current.task, base.task) || equal(current.task, incoming.task)) task = incoming.task;
|
|
108
|
+
else if (consumed(incomingConsumption)) task = current.task;
|
|
109
|
+
else if (consumed(currentConsumption)) task = incoming.task;
|
|
110
|
+
else task = mergeValue(base.task, current.task, incoming.task, 'task');
|
|
111
|
+
|
|
112
|
+
const result = { ...mergeTitle(base, current, incoming) };
|
|
113
|
+
if (task !== undefined) result.task = clone(task);
|
|
114
|
+
// Summaries are derived metadata, never a second draft edit. Prefer the
|
|
115
|
+
// stored summary when both sides have the winning task (including legacy forms).
|
|
116
|
+
const taskOwner = equal(task, current.task) ? current : incoming;
|
|
117
|
+
if (taskOwner.safeTaskSummary !== undefined) result.safeTaskSummary = clone(taskOwner.safeTaskSummary);
|
|
118
|
+
if (task === '' && (incomingConsumption || currentConsumption))
|
|
119
|
+
result.draftConsumption = clone(consumed(incomingConsumption)
|
|
120
|
+
? incomingConsumption : currentConsumption || incomingConsumption);
|
|
121
|
+
const derived = new Set(['title', 'titleSource', 'task', 'safeTaskSummary', 'draftConsumption', 'sharedRevision']);
|
|
122
|
+
for (const key of new Set([...Object.keys(base), ...Object.keys(current), ...Object.keys(incoming)])) {
|
|
123
|
+
if (derived.has(key)) continue;
|
|
124
|
+
const value = mergeValue(base[key], current[key], incoming[key], key);
|
|
125
|
+
if (value !== undefined) Object.defineProperty(result, key, { value, enumerable: true, configurable: true, writable: true });
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
75
128
|
}
|
|
76
129
|
|
|
77
130
|
function createService(options = {}) {
|
|
@@ -232,7 +285,7 @@
|
|
|
232
285
|
if (!base) throw failure('shared_session_conflict', 'Reload the session before updating existing history.');
|
|
233
286
|
// Keep the established first-durable-review-decision policy.
|
|
234
287
|
const reviewed = options.mergeSessionReviewState?.(candidate, existing.record, message.reviewRunIds || []) || candidate;
|
|
235
|
-
next =
|
|
288
|
+
next = mergeSession(base, existing.record, reviewed);
|
|
236
289
|
} else if (base && !importing) {
|
|
237
290
|
throw failure('shared_session_deleted', 'The session no longer exists.');
|
|
238
291
|
}
|
|
@@ -37,8 +37,11 @@
|
|
|
37
37
|
'codex-overleaf/consent-update-cancel',
|
|
38
38
|
'codex-overleaf/consent-update-recover',
|
|
39
39
|
'codex-overleaf/consent-update-dismiss',
|
|
40
|
-
'codex-overleaf/consent-update-reload'
|
|
40
|
+
'codex-overleaf/consent-update-reload',
|
|
41
|
+
'codex-overleaf/consent-update-rollback'
|
|
41
42
|
]);
|
|
43
|
+
const USER_ROLLBACK_REASON = 'update_user_rollback';
|
|
44
|
+
const RECOVERABLE_TRANSACTION_STATES = new Set(['awaiting_health', 'bootstrap_pending', 'bootstrap_rolling_back']);
|
|
42
45
|
|
|
43
46
|
const policy = root.CodexOverleafUpdateConsent;
|
|
44
47
|
const revocation = root.CodexOverleafUpdateRevocation;
|
|
@@ -156,6 +159,8 @@
|
|
|
156
159
|
return dismissCompletedUpdate();
|
|
157
160
|
case 'codex-overleaf/consent-update-reload':
|
|
158
161
|
return reloadInstalledRuntime();
|
|
162
|
+
case 'codex-overleaf/consent-update-rollback':
|
|
163
|
+
return restorePreviousVersion();
|
|
159
164
|
default:
|
|
160
165
|
throw codedError('unknown_update_action', 'Unknown update action.');
|
|
161
166
|
}
|
|
@@ -174,14 +179,14 @@
|
|
|
174
179
|
code: '', message: '', blocker: '', blockers: [], cooldownUntil: 0, postponeUntil: LEGACY_GUARD }, { operation });
|
|
175
180
|
try {
|
|
176
181
|
const result = await withTimeout(requestNative('update.check', {
|
|
177
|
-
operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || ''
|
|
182
|
+
operationId: operation.id, currentVersion: currentVersion(), etag: currentState.etag || '', manual: manual === true
|
|
178
183
|
}), CHECK_REQUEST_TIMEOUT_MS, codedError('update_check_timeout', 'The managed update check did not complete in time.'));
|
|
179
184
|
assertOperation(operation);
|
|
180
185
|
const checkedAt = Date.now();
|
|
181
186
|
if (result.available) {
|
|
182
187
|
const latestVersion = result.latestVersion || currentState.latestVersion;
|
|
183
188
|
await setConsentState({ ...consent,
|
|
184
|
-
...(latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
|
|
189
|
+
...(manual || latestVersion !== consent.snoozedVersion ? { snoozedVersion: '', snoozedUntil: 0 } : {}),
|
|
185
190
|
lastPromptedVersion: latestVersion,
|
|
186
191
|
lastPromptedAt: manual ? checkedAt : consent.lastPromptedAt
|
|
187
192
|
}, { operation });
|
|
@@ -198,7 +203,8 @@
|
|
|
198
203
|
currentVersion: result.currentVersion || currentVersion(), latestVersion: result.latestVersion,
|
|
199
204
|
managed: true, etag: result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
200
205
|
postponeUntil: LEGACY_GUARD, code: result.reason === 'prerelease_promotion_requires_manual_update'
|
|
201
|
-
? result.reason :
|
|
206
|
+
? result.reason : result.code === 'update_bootstrap_source_mismatch'
|
|
207
|
+
? result.code : 'update_bootstrap_upgrade_required', message: '', cooldownUntil: 0,
|
|
202
208
|
manualReason: result.manualInstall?.reason || {}, cancelRequested: false
|
|
203
209
|
}, { operation });
|
|
204
210
|
} else {
|
|
@@ -207,9 +213,11 @@
|
|
|
207
213
|
latestVersion: result.latestVersion || result.currentVersion || currentVersion(),
|
|
208
214
|
// A 304 without a verified Native candidate is a cache miss. Drop
|
|
209
215
|
// its validator so the next normal check can obtain fresh bytes.
|
|
210
|
-
|
|
216
|
+
// A skipped version also needs fresh bytes when checked explicitly.
|
|
217
|
+
etag: ['not_modified', 'cooldown'].includes(result.reason) ? '' : result.etag || currentState.etag || '', lastCheckedAt: checkedAt,
|
|
211
218
|
postponeUntil: LEGACY_GUARD, transactionId: '',
|
|
212
|
-
code: result.reason === 'cooldown'
|
|
219
|
+
code: result.reason === 'cooldown'
|
|
220
|
+
? (result.cooldownReason === USER_ROLLBACK_REASON ? 'update_skipped_after_rollback' : 'update_cooldown') : '',
|
|
213
221
|
cooldownUntil: result.reason === 'cooldown' ? Number(result.cooldownUntil || 0) : 0,
|
|
214
222
|
manualReason: {}, message: '', cancelRequested: false
|
|
215
223
|
}, { operation });
|
|
@@ -508,7 +516,7 @@
|
|
|
508
516
|
async function cancelUpdateCore() {
|
|
509
517
|
const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
|
|
510
518
|
if (['applying', 'awaiting_health', 'rolling_back'].includes(state.state)
|
|
511
|
-
|| activeOperation?.phase
|
|
519
|
+
|| ['applying', 'rolling_back'].includes(activeOperation?.phase) || state.recoveryPending) {
|
|
512
520
|
throw codedError('update_revoke_too_late', 'The update is being installed or recovered.');
|
|
513
521
|
}
|
|
514
522
|
const operation = activeOperation;
|
|
@@ -563,6 +571,7 @@
|
|
|
563
571
|
|
|
564
572
|
|
|
565
573
|
function reconcileRecoveryState() {
|
|
574
|
+
if (activeOperation?.phase === 'rolling_back') return getUpdateState();
|
|
566
575
|
if (recoveryPromise) return recoveryPromise;
|
|
567
576
|
recoveryPromise = (async () => {
|
|
568
577
|
if (!await waitForBootstrapOwnership()) return getUpdateState();
|
|
@@ -652,10 +661,35 @@
|
|
|
652
661
|
if (transaction?.state === 'rolled_back' && status.installedAligned
|
|
653
662
|
&& status.activeVersion === transaction.sourceVersion
|
|
654
663
|
&& (!state.transactionId || transaction.id === state.transactionId)) {
|
|
655
|
-
const
|
|
664
|
+
const userRestore = transaction.reasonCode === USER_ROLLBACK_REASON;
|
|
665
|
+
if ((userRestore || state.code === 'update_rollback_unconfirmed')
|
|
666
|
+
&& !root.CodexOverleafUpdateRuntimeIdentity?.verifiedRollback(status)) {
|
|
667
|
+
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
668
|
+
}
|
|
669
|
+
if (userRestore) {
|
|
670
|
+
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
671
|
+
const tabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES });
|
|
672
|
+
await assertRecoveryOwner(recoveryOwner);
|
|
673
|
+
await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: [],
|
|
674
|
+
[MANUAL_RELOAD_TABS_KEY]: pending?.targetVersion === transaction.sourceVersion ? pending : {
|
|
675
|
+
targetVersion: transaction.sourceVersion, tabIds: tabs.filter(isUsableEditorTab).map(tab => tab.id)
|
|
676
|
+
} });
|
|
677
|
+
}
|
|
678
|
+
const value = { state: 'rolled_back', currentVersion: transaction.sourceVersion,
|
|
656
679
|
transactionId: transaction.id, recoveryPending: false, cancelRequested: false,
|
|
657
680
|
code: transaction.reasonCode || 'update_rolled_back', message: 'The previous version has been restored.'
|
|
658
|
-
}
|
|
681
|
+
};
|
|
682
|
+
const restored = userRestore
|
|
683
|
+
? (await setUpdateAndConsentState({ ...value, latestVersion: transaction.targetVersion,
|
|
684
|
+
postponeUntil: LEGACY_GUARD, etag: '', cooldownUntil: 0
|
|
685
|
+
}, policy.normalizeConsentState({ snoozedVersion: transaction.targetVersion,
|
|
686
|
+
snoozedUntil: Date.now() + SNOOZE_MS }), recoveryOptions)).updateState
|
|
687
|
+
: await setUpdateState(value, recoveryOptions);
|
|
688
|
+
runtimeStatus = null;
|
|
689
|
+
if (userRestore) {
|
|
690
|
+
try { await requireReloadSafePoint(); } catch (_error) { return restored; }
|
|
691
|
+
}
|
|
692
|
+
if (recoveryOwner.generation !== operationGeneration) return getUpdateState();
|
|
659
693
|
setTimeout(() => chrome.runtime.reload(), 0);
|
|
660
694
|
return restored;
|
|
661
695
|
}
|
|
@@ -765,9 +799,15 @@
|
|
|
765
799
|
now: Date.now()
|
|
766
800
|
});
|
|
767
801
|
view.runtime = runtime;
|
|
802
|
+
// Native decides availability: a committed pair with its source still on
|
|
803
|
+
// disk, or any unsettled installation that recovery must finish anyway.
|
|
804
|
+
view.restorePrevious = !state.cancelRequested && !activeOperation
|
|
805
|
+
&& (runtime.rollbackAvailable?.sourceVersion || (state.recoveryPending && state.transactionId))
|
|
806
|
+
? { version: runtime.rollbackAvailable?.sourceVersion || '' } : null;
|
|
807
|
+
view.actions = { ...view.actions, restorePrevious: Boolean(view.restorePrevious) };
|
|
768
808
|
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
769
809
|
const pagesPending = runtime.state === 'aligned' && pending?.targetVersion === runtime.installedVersion && pending.tabIds?.length;
|
|
770
|
-
if (runtime.state === 'reload_required' || pagesPending) {
|
|
810
|
+
if (!state.recoveryPending && (runtime.state === 'reload_required' || pagesPending)) {
|
|
771
811
|
view.showPanel = true;
|
|
772
812
|
view.state = { ...state, state: pagesPending ? 'reload_tabs_required' : 'reload_required',
|
|
773
813
|
currentVersion: runtime.extensionVersion, latestVersion: runtime.installedVersion,
|
|
@@ -784,9 +824,9 @@
|
|
|
784
824
|
runtimeStatusRead = withTimeout(requestNative('update.status'), 4000,
|
|
785
825
|
codedError('update_status_timeout', 'Installed version inspection timed out.')).then(status => {
|
|
786
826
|
runtimeStatusAt = Date.now();
|
|
787
|
-
runtimeStatus = root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
|
|
827
|
+
runtimeStatus = { ...root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
|
|
788
828
|
extensionVersion: chrome.runtime.getManifest().version, runtimeVersion: currentVersion()
|
|
789
|
-
});
|
|
829
|
+
}), rollbackAvailable: status.rollbackAvailable || null };
|
|
790
830
|
return runtimeStatus;
|
|
791
831
|
}).catch(() => ({ state: 'unknown' })).finally(() => { runtimeStatusRead = null; });
|
|
792
832
|
return runtimeStatusRead;
|
|
@@ -808,6 +848,9 @@
|
|
|
808
848
|
}
|
|
809
849
|
|
|
810
850
|
async function reloadInstalledRuntime() {
|
|
851
|
+
if ((await getUpdateState()).recoveryPending) {
|
|
852
|
+
throw codedError('update_rollback_unconfirmed', 'Confirm update recovery before reloading.');
|
|
853
|
+
}
|
|
811
854
|
const runtime = await readRuntimeStatus(true);
|
|
812
855
|
if (runtime.state === 'aligned') { await finishManualRuntimeReload(); return getView(); }
|
|
813
856
|
if (runtime.state !== 'reload_required') throw codedError('update_reload_unavailable', 'No verified installed update is awaiting reload.');
|
|
@@ -819,7 +862,80 @@
|
|
|
819
862
|
return getView();
|
|
820
863
|
}
|
|
821
864
|
|
|
865
|
+
// Returns a committed update, or an update whose installation or recovery is
|
|
866
|
+
// unsettled, to the source pair Native still holds. Native owns the decision;
|
|
867
|
+
// the browser only supplies the transaction it observed and reloads after an
|
|
868
|
+
// exact readback.
|
|
869
|
+
async function restorePreviousVersion() {
|
|
870
|
+
if (activeOperation || cancellationPromise) {
|
|
871
|
+
throw codedError('update_transaction_in_progress', 'Another update operation is in progress.');
|
|
872
|
+
}
|
|
873
|
+
const operation = beginOperation();
|
|
874
|
+
operation.phase = 'rolling_back';
|
|
875
|
+
try {
|
|
876
|
+
const status = await requestNative('update.status');
|
|
877
|
+
assertOperation(operation);
|
|
878
|
+
const transaction = status.transaction;
|
|
879
|
+
const committed = transaction?.state === 'committed' && status.rollbackAvailable?.transactionId === transaction.id;
|
|
880
|
+
if (!transaction?.id || (!committed && !RECOVERABLE_TRANSACTION_STATES.has(transaction.state))) {
|
|
881
|
+
throw codedError('update_rollback_unavailable', 'No installed update can be restored to its previous version.');
|
|
882
|
+
}
|
|
883
|
+
// Page refresh happens after restoration, so it waits for the same saved
|
|
884
|
+
// and idle point as a reload of an installed update.
|
|
885
|
+
const tabs = await requireReloadSafePoint();
|
|
886
|
+
assertOperation(operation);
|
|
887
|
+
const generation = operation.generation;
|
|
888
|
+
clearIdleRetryTimer();
|
|
889
|
+
await setUpdateState({ state: 'rolling_back', transactionId: transaction.id,
|
|
890
|
+
currentVersion: transaction.targetVersion, latestVersion: transaction.targetVersion,
|
|
891
|
+
recoveryPending: false, cancelRequested: false, operationId: '', code: '', message: ''
|
|
892
|
+
}, { observed: true, generation });
|
|
893
|
+
try {
|
|
894
|
+
// Old source runtimes consume this key through their saved/idle check.
|
|
895
|
+
// The fixed Bootstrap's legacy list would refresh tabs unconditionally.
|
|
896
|
+
await chrome.storage.local.set({ [UPDATE_RELOAD_TABS_KEY]: [],
|
|
897
|
+
[MANUAL_RELOAD_TABS_KEY]: { targetVersion: transaction.sourceVersion, tabIds: tabs.map(tab => tab.id) } });
|
|
898
|
+
assertOperation(operation);
|
|
899
|
+
const rolledBack = await requestNative('update.rollback', { transactionId: transaction.id,
|
|
900
|
+
expectedState: transaction.state, reasonCode: USER_ROLLBACK_REASON });
|
|
901
|
+
assertOperation(operation);
|
|
902
|
+
const readback = await requestNative('update.status');
|
|
903
|
+
assertOperation(operation);
|
|
904
|
+
if (rolledBack.transactionId !== transaction.id || rolledBack.state !== 'rolled_back'
|
|
905
|
+
|| rolledBack.version !== transaction.sourceVersion || readback.transaction?.id !== transaction.id
|
|
906
|
+
|| readback.transaction.sourceVersion !== transaction.sourceVersion
|
|
907
|
+
|| readback.transaction.targetVersion !== transaction.targetVersion
|
|
908
|
+
|| !root.CodexOverleafUpdateRuntimeIdentity?.verifiedRollback({ ...readback,
|
|
909
|
+
transaction: { ...readback.transaction, bootstrapRevision: transaction.bootstrapRevision } })
|
|
910
|
+
|| !['extensionVersion', 'nativeVersion', 'extensionSha256', 'nativeSha256', 'bootstrapSha256']
|
|
911
|
+
.every(key => readback.restoredPair[key] === rolledBack.restoredPair?.[key])) {
|
|
912
|
+
throw codedError('update_rollback_unconfirmed', 'Rollback returned incomplete evidence.');
|
|
913
|
+
}
|
|
914
|
+
} catch (error) {
|
|
915
|
+
await setUpdateState({ state: 'failed', transactionId: transaction.id, recoveryPending: true,
|
|
916
|
+
cancelRequested: false, code: safeCode(error), message: safeMessage(error)
|
|
917
|
+
}, { observed: true, generation }).catch(() => {});
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
await setUpdateAndConsentState({ state: 'rolled_back', transactionId: transaction.id,
|
|
921
|
+
currentVersion: transaction.sourceVersion, latestVersion: transaction.targetVersion,
|
|
922
|
+
recoveryPending: false, cancelRequested: false, blocker: '', blockers: [],
|
|
923
|
+
postponeUntil: LEGACY_GUARD, etag: '', cooldownUntil: 0,
|
|
924
|
+
code: USER_ROLLBACK_REASON, message: 'You restored the previous version.'
|
|
925
|
+
}, policy.normalizeConsentState({ snoozedVersion: transaction.targetVersion,
|
|
926
|
+
snoozedUntil: Date.now() + SNOOZE_MS }), { observed: true, generation });
|
|
927
|
+
runtimeStatus = null;
|
|
928
|
+
// The disk restore is complete even if new work now prevents a reload.
|
|
929
|
+
// Keep the existing Reload action available without attempting rollback again.
|
|
930
|
+
try { await requireReloadSafePoint(); } catch (_error) { return getView(); }
|
|
931
|
+
assertOperation(operation);
|
|
932
|
+
setTimeout(() => chrome.runtime.reload(), 0);
|
|
933
|
+
return getView();
|
|
934
|
+
} finally { finishOperation(operation); }
|
|
935
|
+
}
|
|
936
|
+
|
|
822
937
|
async function finishManualRuntimeReload() {
|
|
938
|
+
if ((await getUpdateState()).recoveryPending) return;
|
|
823
939
|
const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
|
|
824
940
|
if (!pending || !Array.isArray(pending.tabIds)) return;
|
|
825
941
|
const runtime = await readRuntimeStatus(true);
|