codex-overleaf-link 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -392
- package/README.zh-CN.md +154 -393
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- 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/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
(function initBootstrapMigration(root) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const UPDATE_STATE_KEY = 'codex-overleaf-managed-update-state-v1';
|
|
5
|
+
const RETRY_ALARM = 'codex-overleaf-bootstrap-migration-retry';
|
|
6
|
+
const RETRY_MS = 4000;
|
|
7
|
+
const MAX_WAIT_MS = 2 * 60 * 1000;
|
|
8
|
+
let readyPromise;
|
|
9
|
+
let lockedOwner;
|
|
10
|
+
let setExecutionLocked;
|
|
11
|
+
|
|
12
|
+
function init(options) {
|
|
13
|
+
if (readyPromise) return readyPromise;
|
|
14
|
+
readyPromise = prepare(options);
|
|
15
|
+
return readyPromise;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function prepare(options) {
|
|
19
|
+
let transaction;
|
|
20
|
+
let reloadProtected = false;
|
|
21
|
+
let rollbackState = 'bootstrap_pending';
|
|
22
|
+
const revision = String(root.__CODEX_OVERLEAF_BOOTSTRAP_REVISION__ || '');
|
|
23
|
+
const request = async (method, params = {}) => {
|
|
24
|
+
const response = await options.request({ id: crypto.randomUUID(), method,
|
|
25
|
+
params: { ...params, bootstrapRevision: revision } });
|
|
26
|
+
if (!response?.ok) throw Object.assign(new Error(response?.error?.message || 'Managed update request failed.'),
|
|
27
|
+
{ code: response?.error?.code || 'update_health_failed' });
|
|
28
|
+
return response.result || {};
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
const status = await request('update.status');
|
|
32
|
+
transaction = status.transaction;
|
|
33
|
+
if (transaction?.state !== 'bootstrap_pending') return { continueInitialization: true };
|
|
34
|
+
options.setExecutionLocked(true);
|
|
35
|
+
lockedOwner = { id: transaction.id, targetVersion: transaction.targetVersion, revision };
|
|
36
|
+
setExecutionLocked = options.setExecutionLocked;
|
|
37
|
+
if (!transaction.id || transaction.targetVersion !== root.CodexOverleafCompatibility?.BUILD_TARGET_VERSION) {
|
|
38
|
+
throw codedError('The running update does not match the pending Bootstrap transaction.');
|
|
39
|
+
}
|
|
40
|
+
reloadProtected = await options.protectReloadTabs(transaction);
|
|
41
|
+
if (!reloadProtected) throw codedError('The previous page refresh list could not be protected.');
|
|
42
|
+
await writeState({ state: 'awaiting_health', transactionId: transaction.id,
|
|
43
|
+
latestVersion: transaction.targetVersion, code: '', message: '' });
|
|
44
|
+
|
|
45
|
+
// The first reload may leave old content contexts alive. Recheck the same
|
|
46
|
+
// idle/save contract before replacing their loader and reloading again.
|
|
47
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
48
|
+
while (true) {
|
|
49
|
+
const gate = await options.verifySafety({ transactionId: transaction.id, targetVersion: transaction.targetVersion,
|
|
50
|
+
sourceVersion: transaction.sourceVersion, expectedState: 'bootstrap_pending' });
|
|
51
|
+
if (gate?.ok) break;
|
|
52
|
+
if (Date.now() >= deadline) throw codedError('The Bootstrap update did not reach a safe point.');
|
|
53
|
+
await writeState({ blockers: gate?.error?.blockers || ['busy'] });
|
|
54
|
+
await waitForRetry();
|
|
55
|
+
const refreshed = await request('update.status');
|
|
56
|
+
if (refreshed.transaction?.id !== transaction.id || refreshed.transaction?.state !== 'bootstrap_pending') {
|
|
57
|
+
throw Object.assign(codedError('The pending Bootstrap transaction changed before activation.'), { restoredStatus: refreshed });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = await request('update.bootstrap', { transactionId: transaction.id });
|
|
62
|
+
if (result.transactionId !== transaction.id || result.targetVersion !== transaction.targetVersion) {
|
|
63
|
+
throw codedError('The Bootstrap activation result does not match the pending update.');
|
|
64
|
+
}
|
|
65
|
+
await writeState({ blockers: [], blocker: '' });
|
|
66
|
+
if (result.state === 'reload_required') {
|
|
67
|
+
// Only one reload per worker. Native owns the persistent attempt limit
|
|
68
|
+
// so a broken target cannot create a loop across worker restarts.
|
|
69
|
+
chrome.runtime.reload();
|
|
70
|
+
return { continueInitialization: false, state: result.state };
|
|
71
|
+
}
|
|
72
|
+
if (result.state !== 'awaiting_health' || revision !== '3.1' || result.bootstrapRevision !== revision) {
|
|
73
|
+
throw codedError('The updated Bootstrap did not provide its loaded revision.');
|
|
74
|
+
}
|
|
75
|
+
rollbackState = 'awaiting_health';
|
|
76
|
+
if (!await options.releaseReloadTabs(transaction, 'awaiting_health')) throw codedError('The original page identities could not be confirmed.');
|
|
77
|
+
return { continueInitialization: true, state: result.state };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (!transaction || transaction.state !== 'bootstrap_pending') return { continueInitialization: true };
|
|
80
|
+
if (!reloadProtected) {
|
|
81
|
+
await writeState({ state: 'failed', recoveryPending: true, code: 'update_recovery_pending' }).catch(() => {});
|
|
82
|
+
return { continueInitialization: false, state: 'failed' };
|
|
83
|
+
}
|
|
84
|
+
const code = /^[a-z0-9_]{1,80}$/.test(error?.code || '') ? error.code : 'update_health_failed';
|
|
85
|
+
await writeState({ state: 'rolling_back', code }).catch(() => {});
|
|
86
|
+
let restored = false;
|
|
87
|
+
try {
|
|
88
|
+
let status = error.restoredStatus;
|
|
89
|
+
let rollback = status && { transactionId: status.transaction?.id, state: status.transaction?.state,
|
|
90
|
+
version: status.activeVersion, restoredPair: status.restoredPair };
|
|
91
|
+
if (!hasRestoredSource(transaction, rollback, status)) {
|
|
92
|
+
rollback = await request('update.rollback', { transactionId: transaction.id,
|
|
93
|
+
expectedState: rollbackState, reasonCode: code });
|
|
94
|
+
status = await request('update.status');
|
|
95
|
+
}
|
|
96
|
+
if (!hasRestoredSource(transaction, rollback, status)) throw codedError('Bootstrap rollback is unconfirmed.');
|
|
97
|
+
await writeState({ state: 'rolled_back', currentVersion: transaction.sourceVersion,
|
|
98
|
+
transactionId: transaction.id, recoveryPending: false, blockers: [], blocker: '', code });
|
|
99
|
+
restored = true;
|
|
100
|
+
while (true) {
|
|
101
|
+
try {
|
|
102
|
+
// A new document must never inherit the old document's refresh
|
|
103
|
+
// authorization. The source worker sees an empty legacy list;
|
|
104
|
+
// the guarded evidence remains for a later natural/user refresh.
|
|
105
|
+
if (await options.hasReplacedReloadDocument(transaction)) break;
|
|
106
|
+
const gate = await options.verifySafety({ transactionId: transaction.id, targetVersion: transaction.targetVersion,
|
|
107
|
+
sourceVersion: transaction.sourceVersion, expectedState: 'rolled_back' });
|
|
108
|
+
if (gate?.ok && await options.releaseReloadTabs(transaction, 'rolled_back')) break;
|
|
109
|
+
} catch (_) { /* Restored files stay restored while a page probe retries. */ }
|
|
110
|
+
await waitForRetry();
|
|
111
|
+
}
|
|
112
|
+
chrome.runtime.reload();
|
|
113
|
+
return { continueInitialization: false, state: 'rolled_back' };
|
|
114
|
+
} catch (_rollbackError) {
|
|
115
|
+
if (restored) return { continueInitialization: false, state: 'rolled_back' };
|
|
116
|
+
await writeState({ state: 'failed', recoveryPending: true, transactionId: transaction.id,
|
|
117
|
+
code: 'update_recovery_pending', message: 'The installed update result has not been confirmed.' }).catch(() => {});
|
|
118
|
+
return { continueInitialization: false, state: 'failed' };
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
await chrome.alarms?.clear?.(RETRY_ALARM).catch?.(() => {});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function writeState(patch) {
|
|
126
|
+
const stored = await chrome.storage.local.get(UPDATE_STATE_KEY);
|
|
127
|
+
const state = root.CodexOverleafManagedUpdateProjection.transition(stored[UPDATE_STATE_KEY] || {}, patch,
|
|
128
|
+
{ currentVersion: chrome.runtime.getManifest().version });
|
|
129
|
+
await chrome.storage.local.set({ [UPDATE_STATE_KEY]: state });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function waitForRetry() {
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
let settled = false;
|
|
135
|
+
let timer;
|
|
136
|
+
const finish = () => {
|
|
137
|
+
if (settled) return;
|
|
138
|
+
settled = true;
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
chrome.alarms?.onAlarm?.removeListener?.(onAlarm);
|
|
141
|
+
resolve();
|
|
142
|
+
};
|
|
143
|
+
const onAlarm = alarm => { if (alarm?.name === RETRY_ALARM) finish(); };
|
|
144
|
+
chrome.alarms?.onAlarm?.addListener?.(onAlarm);
|
|
145
|
+
chrome.alarms?.create?.(RETRY_ALARM, { delayInMinutes: 0.5 });
|
|
146
|
+
timer = setTimeout(finish, RETRY_MS);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasRestoredSource(transaction, rollback, status) {
|
|
151
|
+
const pair = status?.restoredPair;
|
|
152
|
+
return rollback?.transactionId === transaction.id && rollback.state === 'rolled_back'
|
|
153
|
+
&& rollback.version === transaction.sourceVersion && status?.transaction?.id === transaction.id
|
|
154
|
+
&& status.transaction.state === 'rolled_back' && status.activeVersion === transaction.sourceVersion
|
|
155
|
+
&& status.installedAligned === true && pair?.extensionVersion === transaction.sourceVersion
|
|
156
|
+
&& pair?.nativeVersion === transaction.sourceVersion
|
|
157
|
+
&& /^[0-9a-f]{64}$/.test(pair.extensionSha256 || '') && /^[0-9a-f]{64}$/.test(pair.nativeSha256 || '')
|
|
158
|
+
&& /^[0-9a-f]{64}$/.test(pair.bootstrapSha256 || '')
|
|
159
|
+
&& ['extensionVersion', 'nativeVersion', 'extensionSha256', 'nativeSha256', 'bootstrapSha256']
|
|
160
|
+
.every(key => pair[key] === rollback.restoredPair?.[key]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function codedError(message) {
|
|
164
|
+
return Object.assign(new Error(message), { code: 'update_health_failed' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function observeConfirmation(params, response) {
|
|
168
|
+
if (!lockedOwner || !response?.ok || response.result?.state !== 'committed'
|
|
169
|
+
|| response.result.version !== lockedOwner.targetVersion || params?.transactionId !== lockedOwner.id
|
|
170
|
+
|| params.extensionVersion !== lockedOwner.targetVersion || params.nativeVersion !== lockedOwner.targetVersion
|
|
171
|
+
|| params.bootstrapRevision !== lockedOwner.revision || lockedOwner.revision !== '3.1') return;
|
|
172
|
+
lockedOwner = null;
|
|
173
|
+
setExecutionLocked(false);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
root.CodexOverleafBootstrapMigration = Object.freeze({
|
|
177
|
+
init,
|
|
178
|
+
observeConfirmation,
|
|
179
|
+
ready: () => readyPromise || Promise.resolve({ continueInitialization: true })
|
|
180
|
+
});
|
|
181
|
+
})(globalThis);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
(function (root, factory) {
|
|
2
|
-
const api = factory(
|
|
2
|
+
const api = factory(typeof module === 'object' && module.exports
|
|
3
|
+
? require('./shared/sharedSessionTransfer') : root.CodexOverleafSharedSessionTransfer);
|
|
3
4
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
5
|
if (root) root.CodexOverleafSharedSessionsBackground = api;
|
|
5
|
-
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
6
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function (Transfer) {
|
|
6
7
|
'use strict';
|
|
7
8
|
const MESSAGE = 'codex-overleaf/shared-sessions-v1';
|
|
8
9
|
const LOCK_PORT = 'codex-overleaf/shared-session-lock-v1';
|
|
10
|
+
const REVIEW_LEASE_PREFIX = 'codex-overleaf-review-lease-v1:';
|
|
9
11
|
const CHANGE_KEY = 'codex-overleaf-shared-sessions-changed-v1';
|
|
10
12
|
const HOSTS = ['overleaf.com', 'www.overleaf.com', 'cn.overleaf.com'];
|
|
11
13
|
const META_PREFIX = 'codex-overleaf-scoped-persistence-v1:';
|
|
@@ -14,16 +16,18 @@
|
|
|
14
16
|
const clone = value => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
15
17
|
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
16
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.');
|
|
17
20
|
function part(value) {
|
|
18
21
|
if (typeof value !== 'string' || !value.trim() || value.length > 256 || /[\0\r\n]/.test(value))
|
|
19
22
|
throw failure('shared_store_scope_invalid', 'The shared session identity is invalid.');
|
|
20
23
|
return value;
|
|
21
24
|
}
|
|
22
25
|
function scope(input) {
|
|
23
|
-
return { accountScopeId: part(input?.accountScopeId), projectId: typeof input?.projectId === 'string' ? input.projectId : '' };
|
|
26
|
+
return { accountScopeId: part(input?.accountScopeId), projectId: typeof input?.projectId === 'string' ? input.projectId : '', ...(input?.runId ? { runId: part(input.runId) } : {}) };
|
|
24
27
|
}
|
|
25
28
|
function keyFor(kind, value) {
|
|
26
|
-
return kind === '
|
|
29
|
+
return kind === 'review' ? 'review:' + JSON.stringify([value.accountScopeId, part(value.projectId), part(value.runId)])
|
|
30
|
+
: kind === 'preferences' ? 'preferences'
|
|
27
31
|
: 'state:' + encodeURIComponent(value.accountScopeId);
|
|
28
32
|
}
|
|
29
33
|
function metaKey(value) {
|
|
@@ -68,7 +72,59 @@
|
|
|
68
72
|
}
|
|
69
73
|
return result;
|
|
70
74
|
}
|
|
71
|
-
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;
|
|
72
128
|
}
|
|
73
129
|
|
|
74
130
|
function createService(options = {}) {
|
|
@@ -137,7 +193,7 @@
|
|
|
137
193
|
const stored = item.value;
|
|
138
194
|
if (value.projectId && stored.projectId !== value.projectId) { item.continue(); return; }
|
|
139
195
|
const record = { ...stored.record, sharedRevision: stored.revision };
|
|
140
|
-
const size = JSON.stringify(record)
|
|
196
|
+
const size = Transfer.byteLength(JSON.stringify(record));
|
|
141
197
|
if (result.length && (result.length >= 8 || bytes + size > MAX_BYTES)) {
|
|
142
198
|
resolve({ records: result, next: last }); return;
|
|
143
199
|
}
|
|
@@ -162,10 +218,9 @@
|
|
|
162
218
|
appliedOperations: [], trackedChangeCaptures: [], undoStatus: '' })) };
|
|
163
219
|
}
|
|
164
220
|
async function writeMany(message, value, importing, origin, sender) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
throw failure('shared_session_too_large', 'The session batch exceeds the shared storage transfer limit.');
|
|
221
|
+
// Reject a lost owner before doing any bounded decompression work.
|
|
222
|
+
ensureLock(message, value, sender);
|
|
223
|
+
const entries = (await Transfer.decode(message)).map(entry => ({ record: clone(entry.record), base: clone(entry.base) }));
|
|
169
224
|
for (const { record, base } of entries) {
|
|
170
225
|
if (!record || record.accountScopeId !== value.accountScopeId || record.projectId !== value.projectId)
|
|
171
226
|
throw failure('shared_store_scope_mismatch', 'The session does not belong to the captured account and project.');
|
|
@@ -197,6 +252,14 @@
|
|
|
197
252
|
putAt(entry.record, entry.base);
|
|
198
253
|
}
|
|
199
254
|
};
|
|
255
|
+
function appendResult(record, revision) {
|
|
256
|
+
// Merge may grow beyond either input. Reject inside this transaction,
|
|
257
|
+
// before returning success or committing any sibling record.
|
|
258
|
+
Transfer.validateRecord(record);
|
|
259
|
+
const result = { ...record, sharedRevision: revision };
|
|
260
|
+
Transfer.validateResponse([...results, result]);
|
|
261
|
+
results.push(result);
|
|
262
|
+
}
|
|
200
263
|
function putAt(candidate, base, copy = false) {
|
|
201
264
|
const key = [value.accountScopeId, candidate.id];
|
|
202
265
|
const tombstone = deleted.get(key);
|
|
@@ -214,7 +277,7 @@
|
|
|
214
277
|
let next = candidate;
|
|
215
278
|
if (importing && existing) {
|
|
216
279
|
if (equal(existing.record, candidate) || copy) {
|
|
217
|
-
|
|
280
|
+
appendResult(existing.record, existing.revision); return;
|
|
218
281
|
}
|
|
219
282
|
putAt(recoveryCopy(candidate, origin), null, true); return;
|
|
220
283
|
}
|
|
@@ -222,14 +285,14 @@
|
|
|
222
285
|
if (!base) throw failure('shared_session_conflict', 'Reload the session before updating existing history.');
|
|
223
286
|
// Keep the established first-durable-review-decision policy.
|
|
224
287
|
const reviewed = options.mergeSessionReviewState?.(candidate, existing.record, message.reviewRunIds || []) || candidate;
|
|
225
|
-
next =
|
|
288
|
+
next = mergeSession(base, existing.record, reviewed);
|
|
226
289
|
} else if (base && !importing) {
|
|
227
290
|
throw failure('shared_session_deleted', 'The session no longer exists.');
|
|
228
291
|
}
|
|
229
292
|
const revision = (existing?.revision || 0) + 1;
|
|
293
|
+
appendResult(next, revision);
|
|
230
294
|
store.put({ accountScopeId: value.accountScopeId, projectId: value.projectId,
|
|
231
295
|
id: next.id, record: next, revision });
|
|
232
|
-
results.push({ ...next, sharedRevision: revision });
|
|
233
296
|
} catch (error) { abort(error); }
|
|
234
297
|
};
|
|
235
298
|
};
|
|
@@ -264,9 +327,41 @@
|
|
|
264
327
|
}
|
|
265
328
|
await done;
|
|
266
329
|
}
|
|
330
|
+
const reviewLeaseKey = (key, token) => REVIEW_LEASE_PREFIX + key + ':' + token;
|
|
331
|
+
async function activeReviewLease(key) {
|
|
332
|
+
const prefix = REVIEW_LEASE_PREFIX + key + ':', records = await chromeApi.storage.local.get(null);
|
|
333
|
+
const active = [], expired = [];
|
|
334
|
+
for (const [id, lease] of Object.entries(records)) {
|
|
335
|
+
if (!id.startsWith(prefix)) continue;
|
|
336
|
+
const valid = lease && typeof lease.token === 'string' && lease.token.length > 0 && lease.token.length <= 256
|
|
337
|
+
&& id === reviewLeaseKey(key, lease.token) && Number.isSafeInteger(lease.deadlineAt) && lease.deadlineAt > 0;
|
|
338
|
+
if (!valid) throw failure('review_lease_invalid', 'Undo ownership storage could not be verified.');
|
|
339
|
+
if (lease.deadlineAt > Date.now()) active.push(lease); else expired.push(id);
|
|
340
|
+
}
|
|
341
|
+
// Immutable token rows can only be removed by their exact expired key.
|
|
342
|
+
await Promise.all(expired.map(id => chromeApi.storage.local.remove(id).catch(() => {})));
|
|
343
|
+
return active.sort((a, b) => b.deadlineAt - a.deadlineAt)[0];
|
|
344
|
+
}
|
|
267
345
|
async function handle(message, sender) {
|
|
268
346
|
const origin = senderOrigin(sender), value = scope(message.scope);
|
|
269
347
|
const method = message.method;
|
|
348
|
+
if (method === 'review-dispatched') {
|
|
349
|
+
const key = keyFor('review', { ...value, runId: part(message.runId) }), held = locks.get(key);
|
|
350
|
+
if (!held || held.disconnected || held.token !== message.reviewToken || held.tabId !== sender.tab.id)
|
|
351
|
+
throw failure('storage_lock_lost', 'The shared Undo owner was lost before dispatch.');
|
|
352
|
+
const deadlineAt = held.deadlineAt || (held.deadlineAt = Date.now() + 120000);
|
|
353
|
+
held.inFlight += 1;
|
|
354
|
+
try {
|
|
355
|
+
await chromeApi.storage.local.set({ [reviewLeaseKey(key, held.token)]: { token: held.token, deadlineAt } });
|
|
356
|
+
if (held.disconnected) throw failure('storage_lock_lost', 'The shared Undo owner disconnected before dispatch.');
|
|
357
|
+
return { deadlineAt };
|
|
358
|
+
} finally { held.inFlight -= 1; if (held.disconnected && !held.inFlight) held.release(); }
|
|
359
|
+
}
|
|
360
|
+
if (method === 'review-owner') {
|
|
361
|
+
const key = keyFor('review', { ...value, runId: part(message.runId) });
|
|
362
|
+
const lease = await activeReviewLease(key);
|
|
363
|
+
return { busy: locks.has(key) || lease?.deadlineAt > Date.now(), deadlineAt: lease?.deadlineAt || 0 };
|
|
364
|
+
}
|
|
270
365
|
if (method === 'list') return list(value, typeof message.after === 'string' ? message.after : '');
|
|
271
366
|
if (method === 'get') {
|
|
272
367
|
const record = await readOne('sessions', [value.accountScopeId, part(message.id)]);
|
|
@@ -284,7 +379,7 @@
|
|
|
284
379
|
tx.objectStore('migrations').put({ ...value, origin, complete: true, at: Date.now() });
|
|
285
380
|
await done; return { complete: true };
|
|
286
381
|
}
|
|
287
|
-
if (method === 'put' || method === 'import') {
|
|
382
|
+
if (method === 'put' || method === 'put-compressed' || method === 'import') {
|
|
288
383
|
const result = await writeMany(message, value, method === 'import', origin, sender);
|
|
289
384
|
if (result.length) await notify(value, message.clientId);
|
|
290
385
|
return result;
|
|
@@ -327,12 +422,30 @@
|
|
|
327
422
|
queues.delete(key);
|
|
328
423
|
}
|
|
329
424
|
port.onMessage.addListener(message => {
|
|
425
|
+
if (message?.type === 'release-review') {
|
|
426
|
+
const held = locks.get(heldKey);
|
|
427
|
+
if (held?.port !== port || held.token !== message.token || !heldKey.startsWith('review:')) return;
|
|
428
|
+
held.inFlight += 1;
|
|
429
|
+
void chromeApi.storage.local.remove(reviewLeaseKey(heldKey, held.token)).then(() => {
|
|
430
|
+
held.inFlight -= 1; held.release(); if (!closed) port.postMessage({ type: 'released' });
|
|
431
|
+
}, () => { held.inFlight -= 1; port.disconnect(); if (held.disconnected && !held.inFlight) held.release(); });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
330
434
|
if (message?.type === 'ping') { port.postMessage({ type: 'pong' }); return; }
|
|
331
435
|
if (message?.type !== 'acquire' || job) return;
|
|
332
436
|
try {
|
|
333
|
-
const value = scope(message.scope), kind = message.kind === 'preferences' ? 'preferences' : 'state';
|
|
437
|
+
const value = scope(message.scope), kind = message.kind === 'review' ? 'review' : message.kind === 'preferences' ? 'preferences' : 'state';
|
|
334
438
|
heldKey = keyFor(kind, value);
|
|
335
|
-
|
|
439
|
+
if (kind === 'review' && locks.has(heldKey)) { port.postMessage({ type: 'busy' }); return; }
|
|
440
|
+
job = { port, tabId: port.sender.tab.id, closed: () => closed, grant: token => {
|
|
441
|
+
if (kind !== 'review') { port.postMessage({ type: 'granted', token }); return; }
|
|
442
|
+
const held = locks.get(heldKey); held.inFlight += 1;
|
|
443
|
+
void (async () => {
|
|
444
|
+
const existing = await activeReviewLease(heldKey);
|
|
445
|
+
if (existing?.deadlineAt > Date.now()) { locks.get(heldKey)?.release(); if (!closed) port.postMessage({ type: 'busy' }); return; }
|
|
446
|
+
if (!closed) port.postMessage({ type: 'granted', token });
|
|
447
|
+
})().catch(() => port.disconnect()).finally(() => { held.inFlight -= 1; if (held.disconnected && !held.inFlight) held.release(); });
|
|
448
|
+
} };
|
|
336
449
|
if (!queues.has(heldKey)) queues.set(heldKey, []);
|
|
337
450
|
queues.get(heldKey).push(job);
|
|
338
451
|
pump(heldKey);
|