codex-overleaf-link 2.5.0 → 2.5.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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -8,6 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  var PREFS_KEY = 'codexOverleafPrefs';
11
+ var LEGACY_MIGRATION_PREFIX = 'codexOverleafLegacyPanelMigration:';
11
12
  var SESSION_TOMBSTONES_KEY = 'codexOverleafSessionTombstones';
12
13
  var SESSION_TOMBSTONE_ENTRY_PREFIX = 'codexOverleafSessionTombstone:v2:';
13
14
  var MAX_DELETED_SESSION_IDS_PER_PROJECT = 100;
@@ -139,118 +140,77 @@
139
140
 
140
141
  function runMigrationIfNeeded(projectId, legacyStorageKey, accountScopeId) {
141
142
  projectId = normalizeProjectPrefKey(projectId);
142
- var normalizedAccountScopeId = normalizeTextField(accountScopeId, PROJECT_PREF_KEY_MAX_CHARS);
143
- if (!normalizedAccountScopeId || !projectId) {
144
- return Promise.resolve({
145
- prefs: normalizePrefs({}),
146
- sessions: [],
147
- activeSessionId: '',
148
- migrated: false,
149
- reason: 'account_scope_unavailable'
150
- });
151
- }
152
- return withProjectPreferenceLock(projectId, function () {
153
- return chrome.storage.local.get(null).then(function (stored) {
154
- var claimed = claimLegacyProjectPreferences(
155
- stored[PREFS_KEY] || {},
156
- normalizedAccountScopeId,
157
- projectId
158
- );
159
- var storedPrefs = claimed.prefs;
160
- var persistClaim = claimed.changed
161
- ? chrome.storage.local.set({ [PREFS_KEY]: storedPrefs })
162
- : Promise.resolve();
163
- return persistClaim.then(function () {
164
- var prefs = buildProjectPreferenceView(
165
- storedPrefs,
166
- normalizedAccountScopeId,
167
- projectId
168
- );
169
- var tombstones = collectSessionTombstones(stored);
170
- var deletedSessionIds = getDeletedSessionIds(tombstones, projectId);
171
- var schemaVersion = prefs.storageSchemaVersion || 0;
172
-
173
- if (schemaVersion >= StorageDb.TARGET_SCHEMA_VERSION) {
174
- var activeSessionByProject = prefs.activeSessionByProject || {};
175
- var activeSessionId = activeSessionByProject[projectId] || '';
176
- if (typeof StorageDb.claimSessionsForAccount !== 'function') {
177
- return Promise.reject(Object.assign(
178
- new Error('Atomic legacy-session claiming is unavailable.'),
179
- { code: 'scoped_claim_unavailable' }
180
- ));
181
- }
182
- return StorageDb.claimSessionsForAccount(
183
- projectId,
184
- normalizedAccountScopeId,
185
- deletedSessionIds
186
- ).then(function (visibleSessions) {
187
- var visibleActiveSessionId = visibleSessions.some(function (session) {
188
- return session && session.id === activeSessionId;
189
- })
190
- ? activeSessionId
191
- : visibleSessions.length
192
- ? visibleSessions[visibleSessions.length - 1].id
193
- : '';
194
- return {
195
- prefs: prefs,
196
- sessions: visibleSessions,
197
- activeSessionId: visibleActiveSessionId,
198
- migrated: false
199
- };
200
- });
201
- }
202
-
203
- // Migration v0 → v1
204
- var legacyBlob = stored[legacyStorageKey] || {};
205
- var legacySessions = Array.isArray(legacyBlob.sessions) ? legacyBlob.sessions : [];
206
- var migratedSessions = [];
207
-
208
- for (var i = 0; i < legacySessions.length; i++) {
209
- var legacy = legacySessions[i];
210
- if (!legacy || !legacy.id) { continue; }
211
- if (deletedSessionIds.indexOf(legacy.id) !== -1) { continue; }
212
- var record = StorageDb.buildSessionRecord(
213
- buildLegacySessionRecordInput(projectId, legacyBlob, legacy, normalizedAccountScopeId)
214
- );
215
- migratedSessions.push(record);
216
- }
217
-
218
- var putPromise = migratedSessions.length
219
- ? StorageDb.putRecords('sessions', migratedSessions)
220
- : Promise.resolve([]);
221
-
222
- return putPromise.then(function () {
223
- var newPrefs = StorageDb.extractLightweightPrefs(legacyBlob, projectId);
224
- newPrefs.activeSessionByProject = StorageDb.buildActiveSessionByProject(
225
- {},
226
- projectId,
227
- legacyBlob.activeSessionId || (migratedSessions.length ? migratedSessions[migratedSessions.length - 1].id : '')
228
- );
229
- var mergedPrefs = mergeScopedProjectPreferences(
230
- storedPrefs,
231
- newPrefs,
232
- normalizedAccountScopeId,
233
- projectId
234
- );
235
- var prefsView = buildProjectPreferenceView(
236
- mergedPrefs,
237
- normalizedAccountScopeId,
238
- projectId
239
- );
240
-
241
- return chrome.storage.local.set({ [PREFS_KEY]: mergedPrefs }).then(function () {
242
- return chrome.storage.local.remove(legacyStorageKey).catch(function () {});
243
- }).then(function () {
244
- return {
245
- prefs: prefsView,
246
- sessions: migratedSessions,
247
- activeSessionId: prefsView.activeSessionByProject[projectId] || '',
248
- migrated: true
249
- };
250
- });
251
- });
143
+ var account = normalizeTextField(accountScopeId, PROJECT_PREF_KEY_MAX_CHARS);
144
+ if (!account || !projectId) return Promise.resolve({ prefs: normalizePrefs({}), sessions: [],
145
+ activeSessionId: '', migrated: false, reason: 'account_scope_unavailable' });
146
+ return withProjectPreferenceLock(projectId, async function () {
147
+ var stored = await chrome.storage.local.get(null);
148
+ var claimed = claimLegacyProjectPreferences(stored[PREFS_KEY] || {}, account, projectId);
149
+ var storedPrefs = claimed.prefs;
150
+ if (claimed.changed) await chrome.storage.local.set({ [PREFS_KEY]: storedPrefs });
151
+ var prefs = buildProjectPreferenceView(storedPrefs, account, projectId);
152
+ var deleted = getDeletedSessionIds(collectSessionTombstones(stored), projectId);
153
+ var markerKey = LEGACY_MIGRATION_PREFIX + encodeURIComponent(String(legacyStorageKey || ''));
154
+ var marker = stored[markerKey];
155
+ var legacyBlob = stored[legacyStorageKey];
156
+ var hasLegacy = Boolean(legacyBlob && typeof legacyBlob === 'object' && !Array.isArray(legacyBlob));
157
+ var schemaCurrent = (prefs.storageSchemaVersion || 0) >= StorageDb.TARGET_SCHEMA_VERSION;
158
+ if (marker && (marker.projectId !== projectId || marker.sourceKey !== legacyStorageKey || !marker.accountScopeId)) {
159
+ throw Object.assign(new Error('The legacy history source has a conflicting migration identity.'), { code: 'legacy_migration_scope_conflict' });
160
+ }
161
+ if (typeof StorageDb.claimSessionsForAccount !== 'function') {
162
+ throw Object.assign(new Error('Atomic legacy-session claiming is unavailable.'), { code: 'scoped_claim_unavailable' });
163
+ }
164
+ var visible = await StorageDb.claimSessionsForAccount(projectId, account, deleted);
165
+ function result(currentPrefs, sessions, migrated) {
166
+ var wanted = currentPrefs.activeSessionByProject?.[projectId] || '';
167
+ return { prefs: currentPrefs, sessions: sessions,
168
+ activeSessionId: sessions.some(session => session.id === wanted) ? wanted : sessions.at(-1)?.id || '',
169
+ migrated: migrated };
170
+ }
171
+ if ((schemaCurrent && !hasLegacy) || marker?.complete || (marker && marker.accountScopeId !== account)) {
172
+ return result(prefs, visible, false);
173
+ }
174
+ legacyBlob = hasLegacy ? legacyBlob : {};
175
+ var legacySessions = Array.isArray(legacyBlob.sessions) ? legacyBlob.sessions : [];
176
+ var owners = new Set([legacyBlob.accountScopeId, legacyBlob.__codexOverleafFallbackAccountScopeId,
177
+ ...legacySessions.map(session => session?.accountScopeId)].filter(value => typeof value === 'string' && value));
178
+ if ([...owners].some(owner => owner !== account)) return result(prefs, visible, false);
179
+ // The shared preferences lock serializes this source claim across accounts.
180
+ // Keep it after a write failure; only its owner can finish the import.
181
+ if (hasLegacy && !marker) {
182
+ marker = { sourceKey: legacyStorageKey, projectId: projectId, accountScopeId: account, complete: false };
183
+ await chrome.storage.local.set({ [markerKey]: marker });
184
+ }
185
+ var existingIds = new Set(visible.map(session => session.id));
186
+ var incoming = legacySessions.filter(session => session?.id && !deleted.includes(session.id) && !existingIds.has(session.id))
187
+ .map(session => StorageDb.buildSessionRecord(buildLegacySessionRecordInput(projectId, legacyBlob, session, account)));
188
+ var written = incoming.length ? await StorageDb.putRecords('sessions', incoming) : [];
189
+ if (incoming.length && (!Array.isArray(written) || incoming.some(session => !written.some(record =>
190
+ record.id === session.id && record.accountScopeId === account && record.projectId === projectId)))) {
191
+ throw Object.assign(new Error('Legacy history was not confirmed in the main session store.'), { code: 'legacy_migration_not_persisted' });
192
+ }
193
+ var sessions = [...visible, ...written];
194
+ var newPrefs = StorageDb.extractLightweightPrefs(legacyBlob, projectId);
195
+ if (schemaCurrent) {
196
+ // A later project's import must not reset current global preferences.
197
+ var legacyPrefs = newPrefs;
198
+ newPrefs = Object.assign({}, legacyPrefs, prefs);
199
+ PROJECT_SCOPED_PREF_MAPS.forEach(function (name) {
200
+ newPrefs[name] = Object.assign({}, legacyPrefs[name] || {}, prefs[name] || {});
252
201
  });
253
- });
202
+ }
203
+ var currentActive = prefs.activeSessionByProject?.[projectId];
204
+ var activeId = sessions.some(session => session.id === currentActive) ? currentActive
205
+ : legacyBlob.activeSessionId || sessions.at(-1)?.id || '';
206
+ newPrefs.activeSessionByProject = StorageDb.buildActiveSessionByProject({}, projectId, activeId);
207
+ var mergedPrefs = mergeScopedProjectPreferences(storedPrefs, newPrefs, account, projectId);
208
+ await chrome.storage.local.set({ [PREFS_KEY]: mergedPrefs });
209
+ // Completion is per source and owner, independent of the global schema.
210
+ // A failed cleanup cannot replay old content or transfer it to another account.
211
+ if (hasLegacy) await chrome.storage.local.set({ [markerKey]: { ...marker, complete: true } });
212
+ await chrome.storage.local.remove(legacyStorageKey).catch(function () {});
213
+ return result(buildProjectPreferenceView(mergedPrefs, account, projectId), sessions, true);
254
214
  });
255
215
  }
256
216
 
@@ -1,18 +1,16 @@
1
1
  (function initStorageRunActions(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'));
3
+ module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'), require('./directoryCleanupProof'));
4
4
  } else {
5
- root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle'], factory);
5
+ root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle', 'DirectoryCleanupProof'], factory);
6
6
  }
7
- })(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle) {
7
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle, DirectoryCleanupProof) {
8
8
  'use strict';
9
9
 
10
- var MAX_PERSISTED_ACTION_RUNS_PER_SESSION = 2;
11
- // Undo/Accept need whole pre- and post-images of every written file, so one
12
- // run that edits a 120 KB roadmap and creates a 100 KB chapter already needs
13
- // ~560 KB. The old 320 KB cap silently dropped the whole payload and the
14
- // buttons vanished after a reload. Two runs per session keep this bounded.
15
- var MAX_PERSISTED_ACTION_BYTES_PER_RUN = 2 * 1024 * 1024;
10
+ // Share a bounded budget across the retained history, rather than expiring
11
+ // otherwise valid recovery data after two edits. The session builder also
12
+ // reserves room for display metadata within the shared transport limit.
13
+ var MAX_PERSISTED_ACTION_BYTES_PER_SESSION = 16 * 1024 * 1024;
16
14
 
17
15
  function compactRunsForStorage(runs, options, maxRuns, compactRun) {
18
16
  if (typeof compactRun !== 'function') {
@@ -21,22 +19,100 @@
21
19
  var selectedRuns = (Array.isArray(runs) ? runs : [])
22
20
  .filter(function (run) { return run && typeof run.id === 'string'; })
23
21
  .slice(-Math.max(0, Number(maxRuns) || 0));
24
- var actionRunIds = new Set();
22
+ var payloads = new Map();
23
+ var remaining = Math.min(MAX_PERSISTED_ACTION_BYTES_PER_SESSION,
24
+ Number.isFinite(options?.maxActionPayloadBytes) ? Math.max(0, options.maxActionPayloadBytes) : Infinity);
25
+ var usedBytes = 0, usedChars = 0;
26
+ var fits = (bytes, chars) => bytes <= remaining && (typeof options?.canRetainActionPayload !== 'function'
27
+ || options.canRetainActionPayload(usedBytes + bytes, usedChars + chars));
25
28
  if (options && options.preserveRunActionPayload === true) {
26
- for (var index = selectedRuns.length - 1;
27
- index >= 0 && actionRunIds.size < MAX_PERSISTED_ACTION_RUNS_PER_SESSION;
28
- index -= 1) {
29
- if (hasReloadableRunActionPayload(selectedRuns[index])) {
30
- actionRunIds.add(selectedRuns[index].id);
29
+ for (var index = selectedRuns.length - 1; index >= 0; index -= 1) {
30
+ var run = selectedRuns[index];
31
+ if (!hasReloadableRunActionPayload(withoutDirectoryFacts(run))) continue;
32
+ var payload = compactRunActionPayload(run, true, { includeCaptures: false, includeDirectoryCleanup: false });
33
+ if (payload.recoveryRetention?.state === 'omitted') { payloads.set(run.id, payload); continue; }
34
+ var serialized = JSON.stringify(payload);
35
+ var bytes = getUtf8ByteLength(serialized);
36
+ if (!fits(bytes, serialized.length)) {
37
+ payloads.set(run.id, { ...emptyActionPayload(), recoveryRetention: omitted('session-budget', bytes) });
38
+ continue;
39
+ }
40
+ remaining -= bytes;
41
+ usedBytes += bytes; usedChars += serialized.length;
42
+ payloads.set(run.id, payload);
43
+ }
44
+ // Captures are optional. They must not crowd out any retained core data.
45
+ for (var run of selectedRuns.slice().reverse()) {
46
+ var payload = payloads.get(run.id);
47
+ if (!payload || payload.recoveryRetention?.state === 'omitted'
48
+ || run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) continue;
49
+ var captures = cloneSerializableArray(run.trackedChangeCaptures);
50
+ var extraText = captures.length ? JSON.stringify({ trackedChangeCaptures: captures }) : '';
51
+ var extraBytes = getUtf8ByteLength(extraText);
52
+ if (extraBytes && fits(extraBytes, extraText.length)) {
53
+ payload.trackedChangeCaptures = captures;
54
+ remaining -= extraBytes;
55
+ usedBytes += extraBytes; usedChars += extraText.length;
31
56
  }
32
57
  }
33
58
  }
59
+ // Passive directory history uses only the space left after all original
60
+ // recovery payloads and optional captures have been selected.
61
+ var directoryRemaining = Number.isFinite(options?.maxOptionalDirectoryBytes)
62
+ ? Math.max(0, options.maxOptionalDirectoryBytes) : Infinity;
34
63
  return selectedRuns.map(function (run) {
35
- return compactRun(run, actionRunIds.has(run.id));
64
+ var payload = payloads.get(run.id) || compactRunActionPayload(run, false, { includeDirectoryCleanup: false });
65
+ if (options?.deferDirectoryCleanup !== true) payload = appendDirectoryFacts(payload, run,
66
+ options?.preserveRunActionPayload === true, function (bytes, chars) {
67
+ if (bytes > directoryRemaining || !fits(bytes, chars)) return false;
68
+ directoryRemaining -= bytes; remaining -= bytes;
69
+ usedBytes += bytes; usedChars += chars;
70
+ return true;
71
+ }, 'session-budget');
72
+ return compactRun(run, payloads.has(run.id) && !coreWasOmitted(payload), payload);
36
73
  });
37
74
  }
38
75
 
76
+ // Keep the complete display history while allocating finite recovery space
77
+ // across the current project. A record plus its previous merge baseline is
78
+ // transported together; a readable batch must also fit the response bound.
79
+ function compactSessionBatchActions(records, options = {}, buildSessionRecord, SharedSessionTransfer) {
80
+ var budget = SharedSessionTransfer.MAX_RESPONSE_BYTES - 64 * 1024;
81
+ if (SharedSessionTransfer.byteLength(JSON.stringify(records)) <= budget) return records;
82
+ var summaries = records.map(record => buildSessionRecord(record, { preserveRunActionPayload: false, deferDirectoryCleanup: true }));
83
+ var remaining = Math.max(0, budget - SharedSessionTransfer.byteLength(JSON.stringify(summaries)));
84
+ var allocated = new Map();
85
+ var ordered = records.slice().sort((a, b) => Number(b.id === options.activeSessionId) - Number(a.id === options.activeSessionId)
86
+ || (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0));
87
+ for (var record of ordered) {
88
+ var summary = summaries.find(item => item.id === record.id);
89
+ var next = buildSessionRecord(record, { preserveRunActionPayload: true, maxActionPayloadBytes: remaining, deferDirectoryCleanup: true });
90
+ remaining -= Math.max(0, SharedSessionTransfer.byteLength(JSON.stringify(next)) - SharedSessionTransfer.byteLength(JSON.stringify(summary)));
91
+ allocated.set(record.id, next);
92
+ }
93
+ // Do not let an active session's optional directory facts evict another
94
+ // session's existing Undo core under the shared response bound.
95
+ for (var record of ordered) {
96
+ var core = allocated.get(record.id);
97
+ var sourceRuns = new Map((record.runs || []).map(run => [run.id, run]));
98
+ var withFacts = { ...core, runs: core.runs.map(run => {
99
+ var source = sourceRuns.get(run.id), next = { ...run };
100
+ if (source?.pendingDirectoryCleanup) next.pendingDirectoryCleanup = source.pendingDirectoryCleanup;
101
+ if (source?.recoveryRetention?.directoryCleanup === true) next.recoveryRetention = {
102
+ ...(next.recoveryRetention || { ...source.recoveryRetention, coreRetained: true }), directoryCleanup: true };
103
+ return next;
104
+ }) };
105
+ var enriched = buildSessionRecord(withFacts, { preserveRunActionPayload: true, maxOptionalDirectoryBytes: remaining });
106
+ remaining -= Math.max(0, SharedSessionTransfer.byteLength(JSON.stringify(enriched)) - SharedSessionTransfer.byteLength(JSON.stringify(core)));
107
+ allocated.set(record.id, enriched);
108
+ }
109
+ return records.map(record => allocated.get(record.id));
110
+ }
111
+
39
112
  function hasReloadableRunActionPayload(run) {
113
+ if (!run || run.forkSnapshot === true) return false;
114
+ if (runDirectoryCleanup(run)) return true;
115
+ if (run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) return false;
40
116
  var trackedStatus = run && run.trackedChangeStatus;
41
117
  var trackedLifecycle = (trackedStatus === 'pending' || trackedStatus === 'needs_review')
42
118
  && Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length > 0
@@ -56,29 +132,96 @@
56
132
  return trackedLifecycle || reviewingEditorUndo || legacyUndo;
57
133
  }
58
134
 
59
- function compactRunActionPayload(run, keepActionPayload) {
135
+ function runDirectoryCleanup(run) {
136
+ var proof = DirectoryCleanupProof.normalize(run?.pendingDirectoryCleanup);
137
+ return proof && proof.runId === run.id && proof.projectId === run.runProjectId ? proof : null;
138
+ }
139
+
140
+ function omitted(reason, bytes = 0) {
141
+ return { state: 'omitted', reason, bytes };
142
+ }
143
+
144
+ function withoutDirectoryFacts(run) {
145
+ var core = { ...run };
146
+ delete core.pendingDirectoryCleanup;
147
+ if (core.recoveryRetention?.directoryCleanup === true) {
148
+ if (core.recoveryRetention.coreRetained === true) delete core.recoveryRetention;
149
+ else {
150
+ core.recoveryRetention = { ...core.recoveryRetention };
151
+ delete core.recoveryRetention.directoryCleanup;
152
+ delete core.recoveryRetention.coreRetained;
153
+ }
154
+ }
155
+ return core;
156
+ }
157
+
158
+ function coreWasOmitted(payload) {
159
+ return payload.recoveryRetention?.state === 'omitted' && payload.recoveryRetention.coreRetained !== true;
160
+ }
161
+
162
+ function appendDirectoryFacts(payload, run, keep, fits, reason) {
163
+ if (run?.forkSnapshot === true) return payload;
164
+ var directory = runDirectoryCleanup(run), previous = run?.recoveryRetention;
165
+ if (!directory && previous?.directoryCleanup !== true) return payload;
166
+ if (directory && keep) {
167
+ var extended = { ...payload, pendingDirectoryCleanup: directory };
168
+ var before = JSON.stringify(payload), after = JSON.stringify(extended);
169
+ if (fits(getUtf8ByteLength(after) - getUtf8ByteLength(before), after.length - before.length)) return extended;
170
+ }
171
+ // The existing omission marker distinguishes optional history from lost
172
+ // recovery authority. Never imply that a retained Undo core was discarded.
173
+ var retention = coreWasOmitted(payload) ? payload.recoveryRetention
174
+ : omitted(!keep ? 'fallback' : directory ? reason : previous.reason,
175
+ directory ? getUtf8ByteLength(JSON.stringify(directory)) : previous.bytes);
176
+ return { ...payload, recoveryRetention: { ...retention, directoryCleanup: true,
177
+ ...(!coreWasOmitted(payload) ? { coreRetained: true } : {}) } };
178
+ }
179
+
180
+ function compactRunActionPayload(run, keepActionPayload, options = {}) {
181
+ var payload = compactCoreActionPayload(withoutDirectoryFacts(run), keepActionPayload, options);
182
+ if (options.includeDirectoryCleanup === false) return payload;
183
+ var serialized = JSON.stringify(payload);
184
+ return appendDirectoryFacts(payload, run, keepActionPayload,
185
+ bytes => getUtf8ByteLength(serialized) + bytes <= MAX_PERSISTED_ACTION_BYTES_PER_SESSION, 'run-budget');
186
+ }
187
+
188
+ function compactCoreActionPayload(run, keepActionPayload, options = {}) {
60
189
  var empty = emptyActionPayload();
61
190
  if (!keepActionPayload || !hasReloadableRunActionPayload(run)) {
191
+ if (hasReloadableRunActionPayload(run)) empty.recoveryRetention = omitted('fallback');
192
+ else if (run?.recoveryRetention?.state === 'omitted' && !run.forkSnapshot
193
+ && run.undoStatus !== 'applied' && !['accepted', 'rejected'].includes(run.trackedChangeStatus)) {
194
+ empty.recoveryRetention = omitted(
195
+ ['run-budget', 'session-budget', 'fallback', 'invalid'].includes(run.recoveryRetention.reason)
196
+ ? run.recoveryRetention.reason : 'invalid', Math.max(0, Number(run.recoveryRetention.bytes) || 0));
197
+ }
62
198
  return empty;
63
199
  }
64
- var payload = {
200
+ var terminal = run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus);
201
+ var payload = terminal ? emptyActionPayload() : {
65
202
  appliedOperations: cloneSerializableArray(run.appliedOperations),
66
203
  undoOperations: cloneSerializableArray(run.undoOperations),
67
204
  undoBaseFiles: cloneSerializableArray(run.undoBaseFiles),
68
205
  undoTrackedChanges: cloneSerializableArray(run.undoTrackedChanges),
69
206
  undoExpectedFiles: cloneSerializableArray(run.undoExpectedFiles)
70
207
  };
208
+ if (!terminal && Object.keys(payload).some(key => run[key]?.length && !payload[key].length)) {
209
+ return { ...empty, recoveryRetention: omitted('invalid') };
210
+ }
71
211
  var serialized;
72
212
  try {
73
213
  serialized = JSON.stringify(payload);
74
214
  } catch (_error) {
75
- return empty;
215
+ return { ...empty, recoveryRetention: omitted('invalid') };
76
216
  }
77
- if (getUtf8ByteLength(serialized) > MAX_PERSISTED_ACTION_BYTES_PER_RUN) return empty;
217
+ var bytes = getUtf8ByteLength(serialized);
218
+ if (bytes > MAX_PERSISTED_ACTION_BYTES_PER_SESSION) return { ...empty,
219
+ recoveryRetention: omitted('run-budget', bytes) };
220
+ if (options.includeCaptures === false || terminal) return payload;
78
221
  const captures = cloneSerializableArray(run.trackedChangeCaptures);
79
222
  if (captures.length) {
80
223
  const extended = { ...payload, trackedChangeCaptures: captures };
81
- if (getUtf8ByteLength(JSON.stringify(extended)) <= MAX_PERSISTED_ACTION_BYTES_PER_RUN) return extended;
224
+ if (getUtf8ByteLength(JSON.stringify(extended)) <= MAX_PERSISTED_ACTION_BYTES_PER_SESSION) return extended;
82
225
  }
83
226
  return payload; // Optional capture evidence must never evict a mature Undo payload.
84
227
  }
@@ -169,17 +312,20 @@
169
312
  function mergeSessionReviewState(incoming, existing, reviewRunIds) {
170
313
  if (!existing || incoming.id !== existing.id || incoming.projectId !== existing.projectId
171
314
  || !incoming.accountScopeId || incoming.accountScopeId !== existing.accountScopeId) return incoming;
172
- var terminal = run => ['accepted', 'rejected'].includes(run?.trackedChangeStatus);
315
+ var terminal = run => ['accepted', 'rejected'].includes(run?.trackedChangeStatus) || run?.undoStatus === 'applied';
173
316
  var incomingRuns = new Map((incoming.runs || []).map(run => [run.id, run]));
174
317
  var existingRuns = new Map((existing.runs || []).map(run => [run.id, run]));
175
318
  var requestedIds = new Set(reviewRunIds || []);
176
- var explicitReview = Array.from(requestedIds).some(id =>
177
- ['accepted', 'rejected', 'needs_review'].includes(incomingRuns.get(id)?.trackedChangeStatus));
319
+ var explicitReview = Array.from(requestedIds).some(id => {
320
+ var run = incomingRuns.get(id);
321
+ return ['accepted', 'rejected', 'needs_review'].includes(run?.trackedChangeStatus)
322
+ || ['applied', 'partial'].includes(run?.undoStatus);
323
+ });
178
324
  var newer = (Date.parse(incoming.updatedAt) || 0) >= (Date.parse(existing.updatedAt) || 0);
179
325
  if (!newer && !explicitReview) return incoming;
180
326
  var base = newer ? incoming : existing;
181
- var fields = ['trackedChangeStatus', 'undoStatus', 'undoTrackedChanges', 'undoExpectedFiles',
182
- 'undoOperations', 'undoBaseFiles', 'appliedOperations', 'trackedChangeCaptures', 'settlement', 'settlementFacts'];
327
+ var fields = ['trackedChangeStatus', 'undoStatus', 'undoOwnerProtocol', 'undoTrackedChanges', 'undoExpectedFiles',
328
+ 'undoOperations', 'undoBaseFiles', 'appliedOperations', 'trackedChangeCaptures', 'recoveryRetention', 'settlement', 'settlementFacts'];
183
329
  var changed = false;
184
330
  var runs = (base.runs || []).map(function (run) {
185
331
  var previous = existingRuns.get(run.id), candidate = incomingRuns.get(run.id);
@@ -218,7 +364,44 @@
218
364
  return (hash >>> 0).toString(16).padStart(8, '0');
219
365
  }
220
366
 
367
+ // A successful durable write can omit an oversized checkpoint while the
368
+ // current tab still has it. Warn only the view and exact action revision
369
+ // that was saved; never mutate or remove its in-memory recovery material.
370
+ function projectRetentionUpdates({ liveState, capturedState, written, capturedScope, currentScope } = {}) {
371
+ if (!capturedScope?.accountScopeId || !capturedScope.projectId
372
+ || !Number.isSafeInteger(capturedScope.generation)
373
+ || ['accountScopeId', 'projectId', 'generation'].some(key => capturedScope[key] !== currentScope?.[key])) return [];
374
+ var fields = ['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoTrackedChanges',
375
+ 'undoExpectedFiles', 'undoStatus', 'trackedChangeStatus', 'pendingDirectoryCleanup'];
376
+ var fingerprint = run => JSON.stringify(fields.map(field => run[field]));
377
+ var updates = [];
378
+ for (var record of written || []) {
379
+ if (record.accountScopeId !== capturedScope.accountScopeId || record.projectId !== capturedScope.projectId) continue;
380
+ var live = liveState?.sessions?.find(session => session.id === record.id);
381
+ var captured = capturedState?.sessions?.find(session => session.id === record.id);
382
+ for (var storedRun of record.runs || []) {
383
+ var run = live?.runs?.find(run => run.id === storedRun.id);
384
+ var original = captured?.runs?.find(run => run.id === storedRun.id);
385
+ if (!run || !original || run.runProjectId !== capturedScope.projectId
386
+ || original.runProjectId !== capturedScope.projectId || !hasReloadableRunActionPayload(run)) continue;
387
+ try {
388
+ if (fingerprint(run) !== fingerprint(original)) continue;
389
+ if (storedRun.recoveryRetention?.state === 'omitted') updates.push({ runId: run.id, retention: { ...storedRun.recoveryRetention } });
390
+ else if (fingerprint(storedRun) === fingerprint(original)) updates.push({ runId: run.id, retention: null });
391
+ } catch (_error) { /* Invalid data cannot establish a revision. */ }
392
+ }
393
+ }
394
+ return updates;
395
+ }
396
+
397
+ function projectRetentionWarnings(input) {
398
+ return projectRetentionUpdates(input).filter(update => update.retention).map(update => update.runId);
399
+ }
400
+
221
401
  return {
402
+ projectRetentionUpdates: projectRetentionUpdates,
403
+ projectRetentionWarnings: projectRetentionWarnings,
404
+ compactSessionBatchActions: compactSessionBatchActions,
222
405
  compactStructuredEventValue: compactStructuredEventValue,
223
406
  mergeSessionReviewState: mergeSessionReviewState,
224
407
  compactRunsForStorage: compactRunsForStorage,
@@ -132,7 +132,7 @@
132
132
  if (typeof value === 'string' && value) agent[field] = clean(value, sanitize, field === 'task' ? 4096 : 160);
133
133
  }
134
134
  agent.clipped ||= detail.historyClipped === true;
135
- agent.historyUnavailable ||= detail.historyUnavailable === true;
135
+ if (typeof detail.historyUnavailable === 'boolean') agent.historyUnavailable = detail.historyUnavailable;
136
136
  let next = type.endsWith('.queued') ? 'pending' : type.endsWith('.started') ? 'running'
137
137
  : type.endsWith('.completed') ? 'completed' : type.endsWith('.rejected') ? 'failed'
138
138
  : type.endsWith('.failed') ? (detail.status === 'cancelled' ? 'cancelled' : 'failed') : detail.status;