codex-overleaf-link 2.2.1 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +25 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/runtime-manifest.json +5 -71
  4. package/extension/src/background.js +67 -63
  5. package/extension/src/backgroundUpdateCoordinator.js +128 -66
  6. package/extension/src/content/applyResultFormatters.js +2 -3
  7. package/extension/src/content/composerPanel.js +0 -2
  8. package/extension/src/content/contentRuntime.js +757 -1558
  9. package/extension/src/content/contextTray.js +4 -6
  10. package/extension/src/content/diagnosticsController.js +1 -2
  11. package/extension/src/content/diffReviewPanel.js +14 -13
  12. package/extension/src/content/generated/content.bundle.js +41605 -0
  13. package/extension/src/content/generated/content.bundle.meta.json +103 -0
  14. package/extension/src/content/legacyGlobalRegistry.js +129 -0
  15. package/extension/src/content/markdownText.js +17 -17
  16. package/extension/src/content/mathText.js +155 -9
  17. package/extension/src/content/modelPicker.js +1 -1
  18. package/extension/src/content/modelPickerSupport.js +10 -5
  19. package/extension/src/content/moduleRegistryKernel.js +125 -0
  20. package/extension/src/content/nativeCompatibilityController.js +313 -0
  21. package/extension/src/content/otWarmMirror.js +8 -6
  22. package/extension/src/content/pageBridgeClient.js +224 -0
  23. package/extension/src/content/panelMaintenance.js +8 -3
  24. package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
  25. package/extension/src/content/projectProviderSelection.js +4 -12
  26. package/extension/src/content/projectSessionCleanup.js +3 -3
  27. package/extension/src/content/providerSettingsCoordinator.js +59 -26
  28. package/extension/src/content/providerSettingsDialog.js +30 -18
  29. package/extension/src/content/recentProjects.js +15 -23
  30. package/extension/src/content/runGuidanceController.js +1 -1
  31. package/extension/src/content/runQueueScheduler.js +129 -29
  32. package/extension/src/content/runSettlementPersistence.js +51 -0
  33. package/extension/src/content/runTimelineView.js +17 -8
  34. package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
  35. package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
  36. package/extension/src/content/scopedPersistencePanelState.js +111 -0
  37. package/extension/src/content/sessionManager.js +15 -12
  38. package/extension/src/content/sessionPersistence.js +67 -8
  39. package/extension/src/content/updateNotice.js +10 -4
  40. package/extension/src/content/writebackOrchestrator.js +89 -68
  41. package/extension/src/contentScript.js +5 -1
  42. package/extension/src/page/trackedChangesLifecycle.js +16 -5
  43. package/extension/src/page/writebackRouter.js +6 -2
  44. package/extension/src/pageBridge.js +47 -98
  45. package/extension/src/shared/agentTranscript.js +18 -8
  46. package/extension/src/shared/compatibility.js +1 -1
  47. package/extension/src/shared/governanceRules.js +1 -1
  48. package/extension/src/shared/i18n.js +2 -4
  49. package/extension/src/shared/managedUpdateProjection.js +337 -0
  50. package/extension/src/shared/nativeRequestIdentity.js +60 -0
  51. package/extension/src/shared/otText.js +0 -19
  52. package/extension/src/shared/pageRpcContract.js +136 -0
  53. package/extension/src/shared/runExecutionSnapshot.js +187 -0
  54. package/extension/src/shared/runExecutionSnapshotCodec.js +171 -0
  55. package/extension/src/shared/runInputQueue.js +81 -34
  56. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
  57. package/extension/src/shared/scopedPersistenceQueuePolicy.js +200 -0
  58. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  59. package/extension/src/shared/scopedPersistenceTransaction.js +241 -0
  60. package/extension/src/shared/sessionState.js +110 -59
  61. package/extension/src/shared/settlementFacts.js +351 -0
  62. package/extension/src/shared/storageDb.js +25 -10
  63. package/extension/src/shared/storageMigration.js +275 -61
  64. package/extension/src/shared/storageSessionClaims.js +60 -0
  65. package/extension/src/shared/updateConsent.js +4 -0
  66. package/extension/src/shared/updateRevocationIntent.js +109 -0
  67. package/extension/src/shared/writebackEvidenceProjection.js +150 -0
  68. package/extension/src/shared/writebackOperationEvidence.js +153 -0
  69. package/extension/src/shared/writebackSettlement.js +727 -0
  70. package/extension/styles/panel.css +18 -8
  71. package/native-host/src/codexPromptAssembly.js +1 -1
  72. package/native-host/src/codexRuntimeIdentity.js +1 -1
  73. package/native-host/src/codexSessionRunner.js +7 -13
  74. package/native-host/src/index.js +3 -69
  75. package/native-host/src/managedInstall.js +50 -8
  76. package/native-host/src/nativeTransportEnvelope.js +148 -0
  77. package/native-host/src/taskRunnerRuntime.js +14 -84
  78. package/native-host/src/updateManager.js +36 -1
  79. package/package.json +13 -5
@@ -0,0 +1,187 @@
1
+ (function initCodexOverleafRunExecutionSnapshot(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory(require('./providerProfiles'), require('./runExecutionSnapshotCodec'));
4
+ } else {
5
+ root.CodexOverleafModuleRegistry.define('RunExecutionSnapshot', [
6
+ 'ProviderProfiles',
7
+ 'RunExecutionSnapshotCodec'
8
+ ], factory);
9
+ }
10
+ })(typeof window !== 'undefined' ? window : globalThis, function runExecutionSnapshotFactory(ProviderProfiles, Codec) {
11
+ 'use strict';
12
+
13
+ function create(input = {}) {
14
+ const providerId = Codec.normalizeText(input.providerId) || 'builtin';
15
+ return Codec.normalizeSnapshot(input, {
16
+ source: input.source || 'submitted',
17
+ requireProviderRevision: providerId !== 'builtin'
18
+ });
19
+ }
20
+
21
+ function capture(input = {}, options = {}) {
22
+ return Codec.normalizeSnapshot(input, options);
23
+ }
24
+
25
+ function captureRawQueueTuple(rawQueueItem = {}, fallbackInput = {}) {
26
+ const rawPayload = rawQueueItem?.payload && typeof rawQueueItem.payload === 'object'
27
+ ? rawQueueItem.payload
28
+ : rawQueueItem;
29
+ const persisted = rawQueueItem?.executionSnapshot || rawPayload?.executionSnapshot;
30
+ if (persisted && typeof persisted === 'object') {
31
+ return Codec.normalizeSnapshot(persisted, {
32
+ source: persisted.source || 'submitted',
33
+ migrateLegacyConfirm: true
34
+ });
35
+ }
36
+ const raw = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
37
+ const fallback = fallbackInput && typeof fallbackInput === 'object' ? fallbackInput : {};
38
+ const captured = {};
39
+ let inferred = false;
40
+ for (const field of Codec.TUPLE_FIELDS) {
41
+ if (Codec.hasOwn(raw, field)) captured[field] = Codec.cloneValue(raw[field]);
42
+ else {
43
+ captured[field] = Codec.cloneValue(fallback[field]);
44
+ inferred = true;
45
+ }
46
+ }
47
+ const providerId = Codec.normalizeText(captured.providerId) || 'builtin';
48
+ if (providerId !== 'builtin' && !Codec.normalizeRevision(captured.providerRevision)
49
+ && (!Codec.hasOwn(raw, 'providerRevision') || captured.providerRevision === '' || captured.providerRevision == null)) {
50
+ captured.providerRevision = fallback.providerRevision;
51
+ inferred = true;
52
+ }
53
+ return Codec.normalizeSnapshot(captured, {
54
+ source: inferred ? 'legacy-inferred' : 'legacy-captured',
55
+ migrateLegacyConfirm: true
56
+ });
57
+ }
58
+
59
+ function resolveForExecution(value = {}, options = {}) {
60
+ const persisted = value.executionSnapshot || value;
61
+ const snapshot = Codec.normalizeSnapshot(persisted, {
62
+ source: persisted.source || 'submitted',
63
+ migrateLegacyConfirm: true
64
+ });
65
+ if (snapshot.providerId === 'builtin') return Codec.normalizeSnapshot(snapshot, { source: snapshot.source });
66
+ const catalog = options.catalog || {};
67
+ const provider = ProviderProfiles?.getProviderById
68
+ ? ProviderProfiles.getProviderById(catalog, snapshot.providerId)
69
+ : (catalog.providers || []).find(item => item?.id === snapshot.providerId);
70
+ if (!provider) throw Codec.snapshotError('provider_not_found', 'The selected provider no longer exists.');
71
+ const currentRevision = Codec.normalizeRevision(provider.revision);
72
+ if (!currentRevision) throw Codec.snapshotError('provider_revision_conflict', 'The selected provider has no usable revision.');
73
+ if (!snapshot.providerRevision) {
74
+ if (snapshot.source !== 'legacy-inferred') {
75
+ throw Codec.snapshotError('provider_revision_conflict', 'The selected provider revision was not captured for this run.');
76
+ }
77
+ return Codec.normalizeSnapshot({ ...snapshot, providerRevision: currentRevision }, {
78
+ source: 'legacy-inferred',
79
+ requireProviderRevision: true
80
+ });
81
+ }
82
+ if (snapshot.providerRevision !== currentRevision) {
83
+ throw Codec.snapshotError('provider_revision_conflict', 'The selected provider changed after this run was submitted.');
84
+ }
85
+ return Codec.normalizeSnapshot(snapshot, { source: snapshot.source, requireProviderRevision: true });
86
+ }
87
+
88
+ function snapshotOf(value = {}) {
89
+ const persisted = value.executionSnapshot || value;
90
+ return Codec.normalizeSnapshot(persisted, {
91
+ source: persisted.source || 'submitted',
92
+ migrateLegacyConfirm: true
93
+ });
94
+ }
95
+
96
+ function cloneSnapshot(value = {}) {
97
+ const snapshot = snapshotOf(value);
98
+ return { ...snapshot, focusFiles: [...snapshot.focusFiles] };
99
+ }
100
+
101
+ function toLegacyQueueTuple(value = {}) {
102
+ const snapshot = snapshotOf(value);
103
+ return {
104
+ mode: snapshot.mode,
105
+ providerId: snapshot.providerId,
106
+ providerRevision: snapshot.providerRevision,
107
+ model: snapshot.model,
108
+ reasoningEffort: snapshot.reasoningEffort,
109
+ speedTier: snapshot.speedTier,
110
+ autoRecompile: snapshot.autoRecompile,
111
+ requireReviewing: snapshot.requireReviewing,
112
+ focusFiles: [...snapshot.focusFiles]
113
+ };
114
+ }
115
+
116
+ function toQueuePayload(value = {}) {
117
+ const snapshot = snapshotOf(value);
118
+ return { executionSnapshot: cloneSnapshot(snapshot), ...toLegacyQueueTuple(snapshot) };
119
+ }
120
+
121
+ function toRunCompatibilityTuple(value = {}) {
122
+ const snapshot = snapshotOf(value);
123
+ return {
124
+ ...toLegacyQueueTuple(snapshot),
125
+ providerRevision: snapshot.providerId === 'builtin' ? 0 : Number(snapshot.providerRevision),
126
+ executionSnapshot: cloneSnapshot(snapshot)
127
+ };
128
+ }
129
+
130
+ function projectForDisplay(value = {}) {
131
+ const snapshot = snapshotOf(value);
132
+ return {
133
+ providerId: snapshot.providerId,
134
+ model: snapshot.model,
135
+ reasoningEffort: snapshot.reasoningEffort,
136
+ speedTier: snapshot.speedTier,
137
+ mode: snapshot.mode,
138
+ focusFiles: [...snapshot.focusFiles]
139
+ };
140
+ }
141
+
142
+ function equalsExecutionConfig(left = {}, right = {}) {
143
+ const leftTuple = toLegacyQueueTuple(left);
144
+ const rightTuple = toLegacyQueueTuple(right);
145
+ return Codec.TUPLE_FIELDS.every(field =>
146
+ JSON.stringify(leftTuple[field]) === JSON.stringify(rightTuple[field])
147
+ );
148
+ }
149
+
150
+ function applyToState(state = {}, value = {}) {
151
+ const snapshot = snapshotOf(value);
152
+ return {
153
+ ...state,
154
+ ...toLegacyQueueTuple(snapshot),
155
+ providerRevision: snapshot.providerId === 'builtin' ? 0 : Number(snapshot.providerRevision)
156
+ };
157
+ }
158
+
159
+ function toProviderSelection(value = {}) {
160
+ const snapshot = snapshotOf(value);
161
+ return {
162
+ providerId: snapshot.providerId,
163
+ providerRevision: snapshot.providerId === 'builtin' ? 0 : Number(snapshot.providerRevision)
164
+ };
165
+ }
166
+
167
+ return {
168
+ SCHEMA_VERSION: Codec.SCHEMA_VERSION,
169
+ TUPLE_FIELDS: Codec.TUPLE_FIELDS,
170
+ applyToState,
171
+ capture,
172
+ captureRawQueueTuple,
173
+ cloneSnapshot,
174
+ create,
175
+ equalsExecutionConfig,
176
+ fromQueuePayload: captureRawQueueTuple,
177
+ normalizeRevision: Codec.normalizeRevision,
178
+ normalizeSnapshot: Codec.normalizeSnapshot,
179
+ projectForDisplay,
180
+ resolveForExecution,
181
+ toLegacyQueueTuple,
182
+ toProviderSelection,
183
+ toQueuePayload,
184
+ toRunCompatibilityTuple,
185
+ validate: Codec.validate
186
+ };
187
+ });
@@ -0,0 +1,171 @@
1
+ (function initCodexOverleafRunExecutionSnapshotCodec(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafRunExecutionSnapshotCodec = factory();
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function runExecutionSnapshotCodecFactory() {
8
+ 'use strict';
9
+
10
+ const SCHEMA_VERSION = 1;
11
+ const VALID_MODES = new Set(['ask', 'auto']);
12
+ const VALID_SOURCES = new Set(['submitted', 'legacy-captured', 'legacy-inferred']);
13
+ const TUPLE_FIELDS = Object.freeze([
14
+ 'mode', 'providerId', 'providerRevision', 'model', 'reasoningEffort',
15
+ 'speedTier', 'autoRecompile', 'requireReviewing', 'focusFiles'
16
+ ]);
17
+ const SECRET_FIELD_PATTERN = /(?:api.?key|authorization|password|secret|access.?token|refresh.?token)/i;
18
+
19
+ function normalizeSnapshot(value = {}, options = {}) {
20
+ const input = value && typeof value === 'object' ? value : {};
21
+ assertNoSecretFields(input);
22
+ if (input.mode === 'confirm' && options.migrateLegacyConfirm !== true) {
23
+ throw snapshotError('suggest_mode_removed', 'Suggest mode has been removed. Choose Ask or Auto.');
24
+ }
25
+ const providerId = normalizeText(input.providerId) || 'builtin';
26
+ const revision = normalizeRevisionResult(input.providerRevision);
27
+ if (!revision.ok) throw snapshotError('provider_revision_conflict', revision.error);
28
+ const snapshot = {
29
+ schemaVersion: SCHEMA_VERSION,
30
+ mode: input.mode === 'auto' ? 'auto' : 'ask',
31
+ providerId,
32
+ providerRevision: providerId === 'builtin' ? '' : revision.value,
33
+ model: normalizeText(input.model).slice(0, 160),
34
+ reasoningEffort: normalizeText(input.reasoningEffort).slice(0, 32),
35
+ speedTier: input.speedTier === 'fast' ? 'fast' : 'standard',
36
+ autoRecompile: input.autoRecompile !== false,
37
+ requireReviewing: input.requireReviewing !== false,
38
+ focusFiles: normalizePaths(input.focusFiles),
39
+ capturedAt: normalizeTimestamp(input.capturedAt),
40
+ source: VALID_SOURCES.has(options.source || input.source)
41
+ ? (options.source || input.source)
42
+ : 'submitted'
43
+ };
44
+ const result = validate(snapshot, options);
45
+ if (!result.ok) throw snapshotError(result.errors[0].code, result.errors[0].message);
46
+ snapshot.focusFiles = Object.freeze([...snapshot.focusFiles]);
47
+ return Object.freeze(snapshot);
48
+ }
49
+
50
+ function validate(value = {}, options = {}) {
51
+ const errors = [];
52
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
53
+ return fail('invalid_execution_snapshot', 'Execution snapshot must be an object.');
54
+ }
55
+ if (value.schemaVersion !== undefined && Number(value.schemaVersion) !== SCHEMA_VERSION) {
56
+ errors.push(errorItem('invalid_execution_snapshot', `Unsupported execution snapshot schema version: ${value.schemaVersion}`));
57
+ }
58
+ if (!VALID_MODES.has(value.mode)) {
59
+ errors.push(errorItem('invalid_execution_snapshot', 'Execution snapshot mode is invalid.'));
60
+ }
61
+ const providerId = normalizeText(value.providerId);
62
+ if (!providerId) errors.push(errorItem('invalid_execution_snapshot', 'Execution snapshot provider is missing.'));
63
+ const revision = normalizeRevisionResult(value.providerRevision);
64
+ if (!revision.ok) {
65
+ errors.push(errorItem('provider_revision_conflict', revision.error));
66
+ } else if (providerId === 'builtin' && revision.value) {
67
+ errors.push(errorItem('provider_revision_conflict', 'Built-in Codex cannot carry a provider revision.'));
68
+ } else if (providerId !== 'builtin' && options.requireProviderRevision === true && !revision.value) {
69
+ errors.push(errorItem('provider_revision_conflict', 'The selected provider revision was not available when the run was submitted.'));
70
+ }
71
+ if (!Array.isArray(value.focusFiles) || value.focusFiles.length > 100) {
72
+ errors.push(errorItem('invalid_execution_snapshot', 'Execution snapshot focus files are invalid.'));
73
+ }
74
+ try {
75
+ assertNoSecretFields(value);
76
+ } catch (error) {
77
+ errors.push(errorItem(error.code, error.message));
78
+ }
79
+ return { ok: errors.length === 0, errors };
80
+ }
81
+
82
+ function normalizeRevision(value) {
83
+ const result = normalizeRevisionResult(value);
84
+ return result.ok ? result.value : '';
85
+ }
86
+
87
+ function normalizeRevisionResult(value) {
88
+ if (value === undefined || value === null || value === '') return { ok: true, value: '' };
89
+ const text = typeof value === 'string' ? value.trim() : String(value);
90
+ if (!text) return { ok: true, value: '' };
91
+ const number = Number(text);
92
+ return Number.isSafeInteger(number) && number >= 0
93
+ ? { ok: true, value: String(number) }
94
+ : { ok: false, value: '', error: 'Provider revision must be a finite non-negative integer.' };
95
+ }
96
+
97
+ function normalizePaths(value) {
98
+ const seen = new Set();
99
+ const result = [];
100
+ for (const raw of Array.isArray(value) ? value : []) {
101
+ const path = normalizeProjectPath(raw);
102
+ if (!path || seen.has(path)) continue;
103
+ seen.add(path);
104
+ result.push(path);
105
+ if (result.length >= 100) break;
106
+ }
107
+ return result;
108
+ }
109
+
110
+ function normalizeProjectPath(value) {
111
+ const result = [];
112
+ for (const segment of String(value || '').replace(/\0/g, '').replace(/\\/g, '/').replace(/^\/+/, '').split('/')) {
113
+ if (!segment || segment === '.') continue;
114
+ if (segment === '..') return '';
115
+ result.push(segment);
116
+ }
117
+ return result.join('/').trim();
118
+ }
119
+
120
+ function normalizeTimestamp(value) {
121
+ if (typeof value === 'string' && value) return value;
122
+ if (Number.isFinite(Number(value)) && Number(value) > 0) return new Date(Number(value)).toISOString();
123
+ return new Date().toISOString();
124
+ }
125
+
126
+ function assertNoSecretFields(value) {
127
+ for (const key of Object.keys(value && typeof value === 'object' ? value : {})) {
128
+ if (SECRET_FIELD_PATTERN.test(key)) {
129
+ throw snapshotError('invalid_execution_snapshot', `Execution snapshot cannot contain secret field "${key}".`);
130
+ }
131
+ }
132
+ }
133
+
134
+ function cloneValue(value) {
135
+ return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
136
+ }
137
+
138
+ function normalizeText(value) {
139
+ return typeof value === 'string' ? value.trim() : '';
140
+ }
141
+
142
+ function hasOwn(value, key) {
143
+ return Object.prototype.hasOwnProperty.call(value, key);
144
+ }
145
+
146
+ function snapshotError(code, message) {
147
+ const error = new Error(message);
148
+ error.code = code;
149
+ return error;
150
+ }
151
+
152
+ function errorItem(code, message) {
153
+ return { code, message };
154
+ }
155
+
156
+ function fail(code, message) {
157
+ return { ok: false, errors: [errorItem(code, message)] };
158
+ }
159
+
160
+ return {
161
+ SCHEMA_VERSION,
162
+ TUPLE_FIELDS,
163
+ cloneValue,
164
+ hasOwn,
165
+ normalizeRevision,
166
+ normalizeSnapshot,
167
+ normalizeText,
168
+ snapshotError,
169
+ validate
170
+ };
171
+ });
@@ -1,15 +1,16 @@
1
1
  (function initCodexOverleafRunInputQueue(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('./runExecutionSnapshot'));
4
4
  } else {
5
- root.CodexOverleafRunInputQueue = factory();
5
+ root.CodexOverleafModuleRegistry.define('RunInputQueue', ['RunExecutionSnapshot'], factory);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function runInputQueueFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function runInputQueueFactory(RunExecutionSnapshot) {
8
8
  'use strict';
9
9
 
10
10
  const MAX_ITEMS = 20;
11
11
  const MAX_TEXT_CHARS = 12000;
12
12
  const MAX_QUEUE_BYTES = 128 * 1024;
13
+ const MAX_PERSISTED_QUEUE_BYTES = 512 * 1024;
13
14
  const ACTIVE_STATUSES = new Set(['claimed', 'executing', 'steering']);
14
15
  const VALID_STATUSES = new Set(['queued', 'paused', ...ACTIVE_STATUSES]);
15
16
 
@@ -27,6 +28,7 @@
27
28
  if (options.recoverActive === true && ACTIVE_STATUSES.has(status)) {
28
29
  status = 'paused';
29
30
  }
31
+ const executionSnapshot = normalizeExecutionSnapshot(raw, options.fallbackInput);
30
32
  normalized.push({
31
33
  id,
32
34
  clientUserMessageId: String(raw.clientUserMessageId || id),
@@ -38,7 +40,8 @@
38
40
  linkedRunId: status === 'executing' ? String(raw.linkedRunId || '') : '',
39
41
  claimToken: status === 'claimed' || status === 'executing' ? String(raw.claimToken || '') : '',
40
42
  pauseReason: status === 'paused' ? String(raw.pauseReason || '') : '',
41
- payload: normalizePayload(raw.payload)
43
+ executionSnapshot: RunExecutionSnapshot.cloneSnapshot(executionSnapshot),
44
+ payload: RunExecutionSnapshot.toQueuePayload(executionSnapshot)
42
45
  });
43
46
  if (normalized.length >= MAX_ITEMS) {
44
47
  break;
@@ -63,6 +66,7 @@
63
66
  const nowFactory = deps.now || (() => new Date().toISOString());
64
67
  const id = String(input.id || idFactory());
65
68
  const timestamp = String(nowFactory());
69
+ const executionSnapshot = normalizeExecutionSnapshot(input, input.fallbackInput);
66
70
  const item = {
67
71
  id,
68
72
  clientUserMessageId: String(input.clientUserMessageId || id),
@@ -74,10 +78,11 @@
74
78
  linkedRunId: '',
75
79
  claimToken: '',
76
80
  pauseReason: input.status === 'paused' ? String(input.pauseReason || '') : '',
77
- payload: normalizePayload(input.payload)
81
+ executionSnapshot: RunExecutionSnapshot.cloneSnapshot(executionSnapshot),
82
+ payload: RunExecutionSnapshot.toQueuePayload(executionSnapshot)
78
83
  };
79
84
  const next = [...queue, item];
80
- if (estimateBytes(next) > MAX_QUEUE_BYTES) {
85
+ if (estimateLegacyBytes(next) > MAX_QUEUE_BYTES || estimateBytes(next) > MAX_PERSISTED_QUEUE_BYTES) {
81
86
  return failure(queue, 'queue_too_large');
82
87
  }
83
88
  return { ok: true, queue: next, item };
@@ -114,13 +119,37 @@
114
119
  return { ok: false, queue, item: null };
115
120
  }
116
121
  const tokenFactory = deps.randomUUID || (() => 'claim_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8));
117
- const item = {
122
+ let item = {
118
123
  ...queue[index],
119
124
  status: 'claimed',
120
125
  claimToken: String(tokenFactory()),
121
126
  updatedAt: new Date().toISOString(),
122
127
  pauseReason: ''
123
128
  };
129
+ try {
130
+ const captured = RunExecutionSnapshot.normalizeSnapshot(
131
+ item.executionSnapshot || item.payload?.executionSnapshot || item.payload,
132
+ { source: item.executionSnapshot?.source || item.payload?.executionSnapshot?.source || 'submitted' }
133
+ );
134
+ const snapshot = deps.resolveExecutionSnapshot instanceof Function
135
+ ? deps.resolveExecutionSnapshot(captured)
136
+ : captured;
137
+ item = {
138
+ ...item,
139
+ executionSnapshot: RunExecutionSnapshot.cloneSnapshot(snapshot),
140
+ payload: RunExecutionSnapshot.toQueuePayload(snapshot)
141
+ };
142
+ } catch (error) {
143
+ return {
144
+ ok: false,
145
+ queue,
146
+ item: null,
147
+ error: {
148
+ code: error?.code || 'provider_revision_conflict',
149
+ message: error?.message || ''
150
+ }
151
+ };
152
+ }
124
153
  queue[index] = item;
125
154
  return { ok: true, queue, item };
126
155
  }
@@ -160,21 +189,14 @@
160
189
  return normalizeQueue(items).map(item => item.id === id ? updater(item) : item);
161
190
  }
162
191
 
163
- function normalizePayload(payload = {}) {
164
- return {
165
- mode: ['ask', 'confirm', 'auto'].includes(payload.mode) ? payload.mode : 'ask',
166
- providerId: String(payload.providerId || 'builtin').slice(0, 160),
167
- providerRevision: String(payload.providerRevision || '').slice(0, 160),
168
- model: String(payload.model || '').slice(0, 160),
169
- reasoningEffort: String(payload.reasoningEffort || '').slice(0, 32),
170
- speedTier: payload.speedTier === 'fast' ? 'fast' : 'standard',
171
- autoRecompile: payload.autoRecompile !== false,
172
- requireReviewing: payload.requireReviewing !== false,
173
- focusFiles: (Array.isArray(payload.focusFiles) ? payload.focusFiles : [])
174
- .map(path => String(path || '').replace(/\\/g, '/').replace(/^\/+/, '').trim())
175
- .filter(Boolean)
176
- .slice(0, 100)
177
- };
192
+ function normalizeExecutionSnapshot(raw = {}, fallbackInput = {}) {
193
+ return RunExecutionSnapshot.captureRawQueueTuple(raw, fallbackInput);
194
+ }
195
+
196
+ function normalizePayload(payload = {}, fallbackInput = {}) {
197
+ return RunExecutionSnapshot.toQueuePayload(
198
+ RunExecutionSnapshot.captureRawQueueTuple(payload, fallbackInput)
199
+ );
178
200
  }
179
201
 
180
202
  function normalizeTimestamp(value) {
@@ -183,7 +205,10 @@
183
205
 
184
206
  function trimToBudget(items) {
185
207
  const queue = items.slice(-MAX_ITEMS);
186
- while (queue.length && estimateBytes(queue) > MAX_QUEUE_BYTES) {
208
+ while (queue.length && (
209
+ estimateLegacyBytes(queue) > MAX_QUEUE_BYTES
210
+ || estimateBytes(queue) > MAX_PERSISTED_QUEUE_BYTES
211
+ )) {
187
212
  queue.shift();
188
213
  }
189
214
  return queue;
@@ -196,6 +221,29 @@
196
221
  : text.length * 2;
197
222
  }
198
223
 
224
+ function estimateLegacyBytes(items) {
225
+ return estimateBytes((Array.isArray(items) ? items : []).map(item => {
226
+ const legacyItem = { ...(item || {}) };
227
+ delete legacyItem.executionSnapshot;
228
+ return {
229
+ ...legacyItem,
230
+ payload: item?.payload && typeof item.payload === 'object'
231
+ ? {
232
+ mode: item.payload.mode,
233
+ providerId: item.payload.providerId,
234
+ providerRevision: item.payload.providerRevision,
235
+ model: item.payload.model,
236
+ reasoningEffort: item.payload.reasoningEffort,
237
+ speedTier: item.payload.speedTier,
238
+ autoRecompile: item.payload.autoRecompile,
239
+ requireReviewing: item.payload.requireReviewing,
240
+ focusFiles: item.payload.focusFiles
241
+ }
242
+ : {}
243
+ };
244
+ }));
245
+ }
246
+
199
247
  function compactForStorage(items, helpers = {}) {
200
248
  const normalizeField = helpers.normalizeField || (value => String(value || ''));
201
249
  const normalizeDisplay = helpers.normalizeDisplay || normalizeField;
@@ -204,6 +252,11 @@
204
252
  .slice(-MAX_ITEMS)
205
253
  .map(item => {
206
254
  const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {};
255
+ const snapshot = RunExecutionSnapshot.captureRawQueueTuple(item, payload);
256
+ const compactSnapshot = RunExecutionSnapshot.cloneSnapshot({
257
+ ...snapshot,
258
+ focusFiles: normalizePaths(snapshot.focusFiles)
259
+ });
207
260
  return {
208
261
  id: normalizeField(item?.id, 160),
209
262
  clientUserMessageId: normalizeField(item?.clientUserMessageId, 160),
@@ -215,17 +268,8 @@
215
268
  linkedRunId: normalizeField(item?.linkedRunId, 160),
216
269
  claimToken: normalizeField(item?.claimToken, 160),
217
270
  pauseReason: normalizeField(item?.pauseReason, 160),
218
- payload: {
219
- mode: typeof payload.mode === 'string' ? payload.mode : 'ask',
220
- providerId: normalizeField(payload.providerId, 160) || 'builtin',
221
- providerRevision: normalizeField(payload.providerRevision, 160),
222
- model: normalizeField(payload.model, 160),
223
- reasoningEffort: normalizeField(payload.reasoningEffort, 32),
224
- speedTier: payload.speedTier === 'fast' ? 'fast' : 'standard',
225
- autoRecompile: payload.autoRecompile !== false,
226
- requireReviewing: payload.requireReviewing !== false,
227
- focusFiles: normalizePaths(payload.focusFiles)
228
- }
271
+ executionSnapshot: compactSnapshot,
272
+ payload: RunExecutionSnapshot.toQueuePayload(compactSnapshot)
229
273
  };
230
274
  })
231
275
  .filter(item => item.id && item.text);
@@ -238,10 +282,13 @@
238
282
  return {
239
283
  MAX_ITEMS,
240
284
  MAX_QUEUE_BYTES,
285
+ MAX_PERSISTED_QUEUE_BYTES,
241
286
  MAX_TEXT_CHARS,
242
287
  claimNext,
243
288
  compactForStorage,
244
289
  enqueue,
290
+ estimateBytes,
291
+ estimateLegacyBytes,
245
292
  markExecuting,
246
293
  markSteering,
247
294
  normalizeQueue,
@@ -0,0 +1,79 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory(require('./scopedPersistenceQueuePolicy'));
4
+ } else {
5
+ root.CodexOverleafModuleRegistry.define(
6
+ 'ScopedPersistenceBrowserAdapter',
7
+ ['ScopedPersistenceQueuePolicy'],
8
+ factory
9
+ );
10
+ }
11
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function (QueuePolicy) {
12
+ 'use strict';
13
+
14
+ var META_PREFIX = 'codex-overleaf-scoped-persistence-v1:';
15
+ var fallbackMeta = new Map();
16
+ var cleanPart = QueuePolicy.cleanPart;
17
+ var normalizeMeta = QueuePolicy.normalizeMeta;
18
+
19
+ function normalizeScope(input) {
20
+ return Object.freeze({
21
+ accountScopeId: cleanPart(input && input.accountScopeId),
22
+ projectId: cleanPart(input && input.projectId)
23
+ });
24
+ }
25
+
26
+ function scopeKey(scope) {
27
+ var normalized = normalizeScope(scope);
28
+ if (!normalized.accountScopeId) {
29
+ var error = new Error('A stable account scope is required for durable persistence.');
30
+ error.code = 'account_scope_unavailable';
31
+ throw error;
32
+ }
33
+ return META_PREFIX + encodeURIComponent(normalized.accountScopeId)
34
+ + ':' + encodeURIComponent(normalized.projectId);
35
+ }
36
+
37
+ function createBrowserAdapter(options) {
38
+ options = options || {};
39
+ var chromeApi = options.chromeApi || (typeof chrome !== 'undefined' ? chrome : null);
40
+ var locksApi = Object.prototype.hasOwnProperty.call(options, 'locksApi')
41
+ ? options.locksApi
42
+ : (typeof navigator !== 'undefined' ? navigator.locks : null);
43
+ return {
44
+ async read(scope) {
45
+ var key = scopeKey(scope);
46
+ if (!chromeApi || !chromeApi.storage || !chromeApi.storage.local) {
47
+ return normalizeMeta(fallbackMeta.get(key));
48
+ }
49
+ var stored = await chromeApi.storage.local.get(key);
50
+ return normalizeMeta(stored && stored[key]);
51
+ },
52
+ async write(scope, meta) {
53
+ var key = scopeKey(scope);
54
+ var normalized = normalizeMeta(meta);
55
+ if (!chromeApi || !chromeApi.storage || !chromeApi.storage.local) {
56
+ fallbackMeta.set(key, normalized);
57
+ return normalized;
58
+ }
59
+ await chromeApi.storage.local.set({ [key]: normalized });
60
+ return normalized;
61
+ },
62
+ withLock(scope, work) {
63
+ var key = scopeKey(scope);
64
+ if (locksApi && typeof locksApi.request === 'function') {
65
+ return locksApi.request(key, { mode: 'exclusive' }, work);
66
+ }
67
+ var error = new Error('A cross-realm browser lock is required for durable persistence.');
68
+ error.code = 'storage_lock_unavailable';
69
+ return Promise.reject(error);
70
+ }
71
+ };
72
+ }
73
+
74
+ return Object.freeze({
75
+ createBrowserAdapter: createBrowserAdapter,
76
+ normalizeScope: normalizeScope,
77
+ scopeKey: scopeKey
78
+ });
79
+ });