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
@@ -11,6 +11,7 @@
11
11
  const MAX_TEXT_CHARS = 12000;
12
12
  const MAX_QUEUE_BYTES = 128 * 1024;
13
13
  const MAX_PERSISTED_QUEUE_BYTES = 512 * 1024;
14
+ const MAX_CUSTOM_INSTRUCTIONS_CHARS = 12000;
14
15
  const ACTIVE_STATUSES = new Set(['claimed', 'executing', 'steering']);
15
16
  const VALID_STATUSES = new Set(['queued', 'paused', ...ACTIVE_STATUSES]);
16
17
 
@@ -41,7 +42,7 @@
41
42
  claimToken: status === 'claimed' || status === 'executing' ? String(raw.claimToken || '') : '',
42
43
  pauseReason: status === 'paused' ? String(raw.pauseReason || '') : '',
43
44
  executionSnapshot: RunExecutionSnapshot.cloneSnapshot(executionSnapshot),
44
- payload: RunExecutionSnapshot.toQueuePayload(executionSnapshot)
45
+ payload: buildPayload(executionSnapshot, raw.payload?.submissionContext)
45
46
  });
46
47
  if (normalized.length >= MAX_ITEMS) {
47
48
  break;
@@ -79,7 +80,7 @@
79
80
  claimToken: '',
80
81
  pauseReason: input.status === 'paused' ? String(input.pauseReason || '') : '',
81
82
  executionSnapshot: RunExecutionSnapshot.cloneSnapshot(executionSnapshot),
82
- payload: RunExecutionSnapshot.toQueuePayload(executionSnapshot)
83
+ payload: buildPayload(executionSnapshot, input.payload?.submissionContext)
83
84
  };
84
85
  const next = [...queue, item];
85
86
  if (estimateLegacyBytes(next) > MAX_QUEUE_BYTES || estimateBytes(next) > MAX_PERSISTED_QUEUE_BYTES) {
@@ -137,7 +138,7 @@
137
138
  item = {
138
139
  ...item,
139
140
  executionSnapshot: RunExecutionSnapshot.cloneSnapshot(snapshot),
140
- payload: RunExecutionSnapshot.toQueuePayload(snapshot)
141
+ payload: buildPayload(snapshot, item.payload?.submissionContext)
141
142
  };
142
143
  } catch (error) {
143
144
  return {
@@ -199,6 +200,46 @@
199
200
  );
200
201
  }
201
202
 
203
+ // Queue inputs persist only these submitted extras. Attachments stay unsupported,
204
+ // and older items must not acquire a later draft's one-turn context on promotion.
205
+ function normalizeSubmissionContext(value = {}, helpers = {}) {
206
+ const input = value && typeof value === 'object' ? value : {};
207
+ const normalizeText = (value, maxChars) => {
208
+ const text = typeof value === 'string' ? value : '';
209
+ return String(helpers.normalizeDisplay ? helpers.normalizeDisplay(text, maxChars) : text).slice(0, maxChars);
210
+ };
211
+ const safeId = value => typeof value === 'string'
212
+ && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(value) && !value.includes('..')
213
+ && !['__proto__', 'prototype', 'constructor'].includes(value);
214
+ const rawSkill = input.skillInvocation;
215
+ const installer = rawSkill?.id === 'skill-installer' && (rawSkill.scope == null || rawSkill.scope === 'builtin');
216
+ const skillInvocation = safeId(rawSkill?.id)
217
+ && (installer || ['codex-overleaf', 'codex-local'].includes(rawSkill.scope))
218
+ ? { id: rawSkill.id, title: normalizeText(rawSkill.title || rawSkill.id, 80),
219
+ ...(installer ? {} : { scope: rawSkill.scope }) }
220
+ : null;
221
+ return {
222
+ customInstructions: normalizeText(input.customInstructions, MAX_CUSTOM_INSTRUCTIONS_CHARS),
223
+ skillLoadingSettings: {
224
+ loadCodexLocalSkills: input.skillLoadingSettings?.loadCodexLocalSkills === true,
225
+ loadCodexOverleafSkills: input.skillLoadingSettings?.loadCodexOverleafSkills === true
226
+ },
227
+ skillInvocation,
228
+ ...(Array.isArray(input.enabledCodexLocalSkillIds) ? {
229
+ enabledCodexLocalSkillIds: [...new Set(input.enabledCodexLocalSkillIds.filter(safeId))]
230
+ } : {}),
231
+ enabledCodexOverleafSkillIds: [...new Set((Array.isArray(input.enabledCodexOverleafSkillIds)
232
+ ? input.enabledCodexOverleafSkillIds : []).filter(safeId))]
233
+ };
234
+ }
235
+
236
+ function buildPayload(snapshot, submissionContext, helpers) {
237
+ const payload = RunExecutionSnapshot.toQueuePayload(snapshot);
238
+ // Keep legacy absence compact; safe defaults are applied only at execution.
239
+ if (submissionContext == null) return payload;
240
+ return { ...payload, submissionContext: normalizeSubmissionContext(submissionContext, helpers) };
241
+ }
242
+
202
243
  function normalizeTimestamp(value) {
203
244
  return typeof value === 'string' && value ? value : new Date().toISOString();
204
245
  }
@@ -237,7 +278,8 @@
237
278
  speedTier: item.payload.speedTier,
238
279
  autoRecompile: item.payload.autoRecompile,
239
280
  requireReviewing: item.payload.requireReviewing,
240
- focusFiles: item.payload.focusFiles
281
+ focusFiles: item.payload.focusFiles,
282
+ submissionContext: item.payload.submissionContext
241
283
  }
242
284
  : {}
243
285
  };
@@ -269,7 +311,7 @@
269
311
  claimToken: normalizeField(item?.claimToken, 160),
270
312
  pauseReason: normalizeField(item?.pauseReason, 160),
271
313
  executionSnapshot: compactSnapshot,
272
- payload: RunExecutionSnapshot.toQueuePayload(compactSnapshot)
314
+ payload: buildPayload(compactSnapshot, payload.submissionContext, { normalizeDisplay })
273
315
  };
274
316
  })
275
317
  .filter(item => item.id && item.text);
@@ -292,6 +334,7 @@
292
334
  markExecuting,
293
335
  markSteering,
294
336
  normalizeQueue,
337
+ normalizeSubmissionContext,
295
338
  pauseAll,
296
339
  remove,
297
340
  resumeAll,
@@ -6,20 +6,29 @@
6
6
  const MAX_TEXT = 20000;
7
7
  function normalize(value) {
8
8
  if (!value || typeof value !== 'object') return null;
9
- const path = typeof value.path === 'string' ? value.path.replace(/\\/g, '/') : '';
10
- if (!path || path.startsWith('/') || /^[A-Za-z]:/.test(path)
9
+ const path = typeof value.path === 'string' ? value.path : '';
10
+ if (!path || path.length > 2048 || path.trim() !== path || path.includes('\\')
11
+ || /[\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || /^[A-Za-z]:/.test(path)
11
12
  || path.split('/').some(part => !part || part === '.' || part === '..')) return null;
12
13
  if (typeof value.projectId !== 'string' || !value.projectId
13
14
  || typeof value.text !== 'string' || !value.text.length || value.text.length > MAX_TEXT
15
+ || /[\uD800-\uDFFF]/u.test(value.text)
14
16
  || !Number.isSafeInteger(value.from) || !Number.isSafeInteger(value.to)
15
17
  || value.from < 0 || value.to <= value.from || value.to - value.from !== value.text.length
16
18
  || !/^[a-f0-9]{64}$/i.test(value.documentHash || '')) return null;
19
+ const proof = value.rebaseEvidence;
20
+ const rebaseEvidence = proof?.v === 1 && Number.isSafeInteger(proof.documentLength)
21
+ && proof.documentLength >= value.to && /^[a-f0-9]{64}$/i.test(proof.prefixHash || '')
22
+ && /^[a-f0-9]{64}$/i.test(proof.suffixHash || '')
23
+ ? Object.freeze({ v: 1, documentLength: proof.documentLength,
24
+ prefixHash: proof.prefixHash.toLowerCase(), suffixHash: proof.suffixHash.toLowerCase() }) : null;
17
25
  return Object.freeze({
18
- projectId: value.projectId.slice(0, 160), path: path.slice(0, 320),
26
+ projectId: value.projectId.slice(0, 160), path,
19
27
  from: value.from, to: value.to, text: value.text,
20
28
  lineStart: Number.isSafeInteger(value.lineStart) && value.lineStart > 0 ? value.lineStart : 1,
21
29
  lineEnd: Number.isSafeInteger(value.lineEnd) && value.lineEnd > 0 ? value.lineEnd : 1,
22
30
  documentHash: value.documentHash.toLowerCase(),
31
+ ...(rebaseEvidence ? { rebaseEvidence } : {}),
23
32
  mode: value.mode === 'edit' ? 'edit' : 'reference',
24
33
  capturedAt: typeof value.capturedAt === 'string' ? value.capturedAt.slice(0, 64) : ''
25
34
  });
@@ -10,12 +10,14 @@
10
10
  require('./sessionTitle'),
11
11
  require('./runActivityModel'),
12
12
  require('./subagentActivityModel'),
13
- require('./writebackIntent')
13
+ require('./writebackIntent'),
14
+ require('./directoryCleanupProof'),
15
+ require('./undoOperations')
14
16
  );
15
17
  } else {
16
18
  root.CodexOverleafModuleRegistry.define(
17
19
  'SessionState',
18
- ['I18n', 'SettlementFacts', 'RunInputQueue', 'SelectionContext', 'LineReferences', 'PathRedaction', 'SessionTitle', 'RunActivityModel', 'SubagentActivityModel', 'WritebackIntent'],
20
+ ['I18n', 'SettlementFacts', 'RunInputQueue', 'SelectionContext', 'LineReferences', 'PathRedaction', 'SessionTitle', 'RunActivityModel', 'SubagentActivityModel', 'WritebackIntent', 'DirectoryCleanupProof', 'UndoOperations'],
19
21
  factory
20
22
  );
21
23
  }
@@ -29,7 +31,9 @@
29
31
  SessionTitle,
30
32
  RunActivityModel,
31
33
  SubagentActivityModel,
32
- WritebackIntent
34
+ WritebackIntent,
35
+ DirectoryCleanupProof,
36
+ UndoOperations
33
37
  ) {
34
38
  'use strict';
35
39
 
@@ -46,6 +50,7 @@
46
50
  loadCodexLocalSkills: true,
47
51
  loadCodexOverleafSkills: true,
48
52
  codexOverleafSkillEnabled: {},
53
+ codexLocalSkillEnabled: {},
49
54
  theme: 'dark',
50
55
  panelWidth: 380,
51
56
  task: '',
@@ -120,7 +125,6 @@
120
125
  statusTextChars: 400,
121
126
  attachmentPreviewChars: 160 * 1024
122
127
  };
123
- const REDACTED_SECRET = '[REDACTED_SECRET]';
124
128
  const SECRET_REDACTION_PATTERNS = [
125
129
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
126
130
  /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
@@ -153,6 +157,7 @@
153
157
  state.loadCodexLocalSkills = state.loadCodexLocalSkills !== false;
154
158
  state.loadCodexOverleafSkills = state.loadCodexOverleafSkills !== false;
155
159
  state.codexOverleafSkillEnabled = normalizeCodexOverleafSkillEnabled(state.codexOverleafSkillEnabled);
160
+ state.codexLocalSkillEnabled = normalizeCodexLocalSkillEnabled(state.codexLocalSkillEnabled);
156
161
  state.theme = ['dark', 'light', 'auto'].includes(state.theme) ? state.theme : 'dark';
157
162
  state.panelWidth = normalizePanelWidth(state.panelWidth);
158
163
  state.task = typeof state.task === 'string' ? state.task : '';
@@ -213,6 +218,8 @@
213
218
  const history = normalizeHistoryEntries(session.history);
214
219
  const normalizedRuns = normalizeRuns(session.runs, options);
215
220
  const runs = normalizedRuns.length ? normalizedRuns : recoverRunsFromHistory(session, history);
221
+ for (const run of runs) pruneRunRecoveryScope(run, session);
222
+
216
223
  const rawTitle = typeof session.title === 'string' ? session.title.trim() : '';
217
224
  const derivedTitle = deriveSessionTitle(runs, session.task);
218
225
  const titleSource = normalizeTitleSource(session.titleSource, rawTitle, derivedTitle);
@@ -642,6 +649,22 @@
642
649
  finishedAt: Number.isFinite(Date.parse(run.finishedAt)) ? run.finishedAt : event.timestamp };
643
650
  }
644
651
 
652
+ function pruneRunRecoveryScope(run, session = {}) {
653
+ const wrongAccount = proof => Object.prototype.hasOwnProperty.call(session, 'accountScopeId')
654
+ && proof.accountScopeId !== session.accountScopeId;
655
+ const wrongProject = value => Boolean(session.projectId && value !== session.projectId);
656
+ if (run.pendingDirectoryCleanup && (run.pendingDirectoryCleanup.sessionId !== session.id
657
+ || wrongAccount(run.pendingDirectoryCleanup) || wrongProject(run.pendingDirectoryCleanup.projectId))) delete run.pendingDirectoryCleanup;
658
+ applyTrackedChangeStatus(run, run.trackedChangeStatus);
659
+ }
660
+
661
+ function normalizeRecoveryRetention(value) {
662
+ if (value?.state !== 'omitted') return undefined;
663
+ return { state: 'omitted', reason: ['run-budget', 'session-budget', 'fallback', 'invalid'].includes(value.reason)
664
+ ? value.reason : 'invalid', bytes: Number.isSafeInteger(value.bytes) && value.bytes >= 0 ? value.bytes : 0,
665
+ ...(value.directoryCleanup === true ? { directoryCleanup: true, ...(value.coreRetained === true ? { coreRetained: true } : {}) } : {}) };
666
+ }
667
+
645
668
  function normalizeRun(run, options = {}) {
646
669
  if (options.restoreRunningRuns === true || options.recoverSettledRuns === true) run = recoverSettledRunningRun(run);
647
670
  run = recoverRecordedRunFailure(run, options.locale);
@@ -687,7 +710,10 @@
687
710
  undoBaseFiles: normalizeRunFiles(run.undoBaseFiles),
688
711
  undoTrackedChanges: normalizeRunTrackedChanges(run.undoTrackedChanges),
689
712
  undoExpectedFiles: normalizeRunFiles(run.undoExpectedFiles),
713
+ ...(normalizeRecoveryRetention(run.recoveryRetention) && !run.forkSnapshot
714
+ ? { recoveryRetention: normalizeRecoveryRetention(run.recoveryRetention) } : {}),
690
715
  undoStatus: sanitizeAssistantVisibleText(run.undoStatus),
716
+ ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
691
717
  nativeRequestId: sanitizeAssistantVisibleText(run.nativeRequestId),
692
718
  codexThreadId: sanitizeAssistantVisibleText(run.codexThreadId), codexTurnId: sanitizeAssistantVisibleText(run.codexTurnId),
693
719
  forkSnapshot: run.forkSnapshot === true,
@@ -712,6 +738,9 @@
712
738
  if (Array.isArray(run.trackedChangeCaptures) && run.trackedChangeCaptures.length && !run.forkSnapshot) {
713
739
  normalized.trackedChangeCaptures = sanitizeAssistantVisibleValue(run.trackedChangeCaptures);
714
740
  }
741
+ const directories = DirectoryCleanupProof.normalize(run.pendingDirectoryCleanup);
742
+ if (directories && directories.runId === normalized.id && directories.projectId === normalized.runProjectId
743
+ && !run.forkSnapshot) normalized.pendingDirectoryCleanup = directories;
715
744
  applyTrackedChangeStatus(normalized, run.trackedChangeStatus);
716
745
  Object.assign(normalized, pickWritebackRecovery(run));
717
746
 
@@ -724,6 +753,7 @@
724
753
  // output yields the same result.
725
754
  function applyTrackedChangeStatus(run, rawStatus) {
726
755
  const hasRefs = run.undoTrackedChanges.length > 0;
756
+ const hasFileUndo = (run.undoOperations || []).some(UndoOperations.isFileUndoOperation);
727
757
  const hasRawStatus = rawStatus !== undefined && rawStatus !== null && rawStatus !== '';
728
758
 
729
759
  // Step 1 — value recovery. Keep a stable value; recover any other present
@@ -740,12 +770,11 @@
740
770
  }
741
771
 
742
772
  // Step 1 — reload reconciliation. A non-terminal status (`pending`) with no
743
- // refs is the post-reload state of an un-acted tracked-change run: the heavy refs are
744
- // never persisted, so without them there is nothing to act on. Drop it so
745
- // the run returns to the legacy-undo world. A terminal status with no refs
773
+ // refs can result from an old record or a storage-budget omission. Drop
774
+ // pending and let actual legacy Undo material decide the action. A terminal status with no refs
746
775
  // is kept — step 3 already empties terminal payloads and the label stays
747
776
  // meaningful.
748
- if (!hasRefs && status === 'pending') {
777
+ if (!hasRefs && !hasFileUndo && status === 'pending') {
749
778
  status = undefined;
750
779
  }
751
780
 
@@ -762,7 +791,7 @@
762
791
 
763
792
  if (status !== undefined) {
764
793
  run.trackedChangeStatus = status;
765
- }
794
+ } else delete run.trackedChangeStatus;
766
795
  }
767
796
 
768
797
  // Welcome-panel + write-guard: the run lifecycle now
@@ -1034,6 +1063,17 @@
1034
1063
  return result;
1035
1064
  }
1036
1065
 
1066
+ function normalizeCodexLocalSkillEnabled(value) {
1067
+ const result = {};
1068
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return result;
1069
+ for (const [id, enabled] of Object.entries(value)) {
1070
+ if (/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(id) && !id.includes('..')
1071
+ && !['__proto__', 'prototype', 'constructor'].includes(id) && typeof enabled === 'boolean') result[id] = enabled;
1072
+ }
1073
+ if (Object.keys(result).length > 200) throw new Error('Too many skill preferences.');
1074
+ return result;
1075
+ }
1076
+
1037
1077
  function normalizeProjectPrefKey(value) {
1038
1078
  const key = typeof value === 'string' ? value.trim() : '';
1039
1079
  if (!key) {
@@ -1105,6 +1145,7 @@
1105
1145
  loadCodexLocalSkills: source.loadCodexLocalSkills !== false,
1106
1146
  loadCodexOverleafSkills: source.loadCodexOverleafSkills !== false,
1107
1147
  codexOverleafSkillEnabled: normalizeCodexOverleafSkillEnabled(source.codexOverleafSkillEnabled),
1148
+ codexLocalSkillEnabled: normalizeCodexLocalSkillEnabled(source.codexLocalSkillEnabled),
1108
1149
  theme: ['dark', 'light', 'auto'].includes(source.theme) ? source.theme : 'dark',
1109
1150
  panelWidth: normalizePanelWidth(source.panelWidth),
1110
1151
  task: summarizeTextForStorage(active?.task || source.task || '', 'task'),
@@ -1283,7 +1324,13 @@
1283
1324
  undoBaseFiles: undoPayload.undoBaseFiles,
1284
1325
  undoTrackedChanges: undoPayload.undoTrackedChanges,
1285
1326
  undoExpectedFiles: undoPayload.undoExpectedFiles,
1327
+ ...(!run.forkSnapshot && (run.pendingDirectoryCleanup || run.recoveryRetention?.directoryCleanup === true || (run.undoStatus !== 'applied' && !['accepted', 'rejected'].includes(run.trackedChangeStatus)))
1328
+ && (run.pendingDirectoryCleanup || run.recoveryRetention?.state === 'omitted' || ['appliedOperations', 'undoOperations', 'undoTrackedChanges', 'undoExpectedFiles']
1329
+ .some(key => Array.isArray(run[key]) && run[key].length))
1330
+ ? { recoveryRetention: normalizeRecoveryRetention(run.recoveryRetention)
1331
+ || { state: 'omitted', reason: 'fallback', bytes: 0, ...(run.pendingDirectoryCleanup ? { directoryCleanup: true } : {}) } } : {}),
1286
1332
  undoStatus: summarizeTextForStorage(run.undoStatus, 'undo status'),
1333
+ ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
1287
1334
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
1288
1335
  codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
1289
1336
  forkSnapshot: run.forkSnapshot === true,
@@ -1291,6 +1338,11 @@
1291
1338
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
1292
1339
  queueItemId: normalizeTextField(run.queueItemId, 160)
1293
1340
  };
1341
+ if (compact.recoveryRetention?.coreRetained === true && run.undoStatus !== 'applied'
1342
+ && !TERMINAL_TRACKED_CHANGE_STATUS.has(run.trackedChangeStatus)
1343
+ && ['appliedOperations', 'undoOperations', 'undoTrackedChanges', 'undoExpectedFiles'].some(key => run[key]?.length)) {
1344
+ delete compact.recoveryRetention.coreRetained;
1345
+ }
1294
1346
  if (run.executionSnapshot && typeof run.executionSnapshot === 'object') {
1295
1347
  compact.executionSnapshot = sanitizeAssistantVisibleValue(run.executionSnapshot);
1296
1348
  }
@@ -1577,21 +1629,22 @@
1577
1629
  if (typeof value !== 'string' || !value) {
1578
1630
  return '';
1579
1631
  }
1580
- const secretRedacted = redactSecretLikeText(value);
1581
- if (!mightContainLocalReferenceText(secretRedacted)) {
1582
- return secretRedacted;
1632
+ // Conversation text is user/model content, not a credential field. Keep
1633
+ // examples and quoted values intact; local-reference display rules remain.
1634
+ if (!mightContainLocalReferenceText(value)) {
1635
+ return value;
1583
1636
  }
1584
1637
  if (LineReferences?.sanitizeLocalReferences) {
1585
1638
  try {
1586
- return LineReferences.sanitizeLocalReferences(secretRedacted, {
1639
+ return LineReferences.sanitizeLocalReferences(value, {
1587
1640
  projectFiles: [],
1588
1641
  context: 'persist'
1589
1642
  });
1590
1643
  } catch (_error) {
1591
- return fallbackSanitizeLocalReferences(secretRedacted);
1644
+ return fallbackSanitizeLocalReferences(value);
1592
1645
  }
1593
1646
  }
1594
- return fallbackSanitizeLocalReferences(secretRedacted);
1647
+ return fallbackSanitizeLocalReferences(value);
1595
1648
  }
1596
1649
 
1597
1650
  function mightContainLocalReferenceText(value) {
@@ -1619,18 +1672,6 @@
1619
1672
  });
1620
1673
  }
1621
1674
 
1622
- function redactSecretLikeText(value) {
1623
- if (typeof value !== 'string') {
1624
- return '';
1625
- }
1626
- let redacted = value;
1627
- for (const pattern of SECRET_REDACTION_PATTERNS) {
1628
- pattern.lastIndex = 0;
1629
- redacted = redacted.replace(pattern, REDACTED_SECRET);
1630
- }
1631
- return redacted;
1632
- }
1633
-
1634
1675
  function containsSecretLikeText(value) {
1635
1676
  if (typeof value === 'string') {
1636
1677
  return SECRET_REDACTION_PATTERNS.some(pattern => {
@@ -1703,6 +1744,8 @@
1703
1744
  }
1704
1745
 
1705
1746
  return {
1747
+ normalizeRecoveryRetention,
1748
+ pruneRunRecoveryScope,
1706
1749
  pickWritebackRecovery,
1707
1750
  DEFAULT_PANEL_STATE,
1708
1751
  createSession,
@@ -1720,7 +1763,9 @@
1720
1763
  normalizeRunStatus,
1721
1764
  recoverRecordedRunFailure,
1722
1765
  recoverSettledRunningRun,
1766
+ normalizeCodexLocalSkillEnabled,
1723
1767
  normalizeProjectReferenceFiles,
1768
+ normalizeComposerAttachmentsForPersistence,
1724
1769
  normalizeSelectionContext: SelectionContext.normalize,
1725
1770
  prepareStateForStorage,
1726
1771
  estimateJsonBytes,
@@ -157,6 +157,9 @@
157
157
  canAccept,
158
158
  canUndo,
159
159
  canRetry: Boolean(primaryFailure?.retryable),
160
+ requiresWriteReview: Boolean(facts && (facts.documentEffect === 'possibly-changed'
161
+ || facts.evidence?.settled === 'needs-review'
162
+ || facts.failures.some(failure => DefaultFailureReasons.isUnconfirmedWriteFailure?.(failure)))),
160
163
  terminalState: failureTerminalState || '',
161
164
  actions: { accept: canAccept, undo: canUndo }
162
165
  };
@@ -209,6 +212,10 @@
209
212
  file: safePath(item.file),
210
213
  operationType: text(item.operationType).slice(0, 80),
211
214
  changedDocument: item.changedDocument === true,
215
+ ...(item.mayHaveMutated === true ? { mayHaveMutated: true } : {}),
216
+ ...(item.evidence?.originalCode === 'native_untracked_undo_pending_changes'
217
+ && item.changedDocument !== true && item.mayHaveMutated !== true && item.evidence.writeStarted !== true
218
+ ? { evidence: { originalCode: 'native_untracked_undo_pending_changes' } } : {}),
212
219
  terminalState: normalizeTerminalState(item.terminalState)
213
220
  }));
214
221
  }
@@ -287,8 +294,9 @@
287
294
  }
288
295
 
289
296
  function selectPrimaryFailure(failures) {
290
- return DefaultFailureReasons?.selectPrimaryFailure instanceof Function
291
- ? DefaultFailureReasons.selectPrimaryFailure(failures)
297
+ const select = DefaultFailureReasons?.selectPrimaryWritebackFailure || DefaultFailureReasons?.selectPrimaryFailure;
298
+ return select instanceof Function
299
+ ? select(failures)
292
300
  : failures[0] || null;
293
301
  }
294
302
 
@@ -1,7 +1,7 @@
1
1
  (function (root, factory) {
2
- if (typeof module === 'object' && module.exports) module.exports = factory();
3
- else root.CodexOverleafModuleRegistry.define('SharedSessionBridge', [], factory);
4
- })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory(require('./sharedSessionTransfer'));
3
+ else root.CodexOverleafModuleRegistry.define('SharedSessionBridge', ['SharedSessionTransfer'], factory);
4
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function (Transfer) {
5
5
  'use strict';
6
6
  const MESSAGE = 'codex-overleaf/shared-sessions-v1';
7
7
  const LOCK_PORT = 'codex-overleaf/shared-session-lock-v1';
@@ -34,7 +34,7 @@
34
34
  }
35
35
  function assertScope(value) { capture(value.projectId, value.accountScopeId); }
36
36
  const cacheKey = (value, id) => value.accountScopeId + ':' + id;
37
- const lockKey = (kind, value) => kind === 'preferences' ? 'preferences' : 'state:' + value.accountScopeId;
37
+ const lockKey = (kind, value) => kind === 'review' ? 'review:' + JSON.stringify([value.accountScopeId, value.projectId, value.runId]) : kind === 'preferences' ? 'preferences' : 'state:' + value.accountScopeId;
38
38
  async function request(method, value, args = {}) {
39
39
  assertScope(value);
40
40
  const lock = heldLocks.get(lockKey('state', value));
@@ -51,23 +51,24 @@
51
51
 
52
52
  // Live ports own the locks. Worker/page termination releases them without
53
53
  // allowing a stale client to silently fall back to an origin-local lock.
54
- async function acquire(kind, value, work) {
54
+ async function acquire(kind, value, work, onLost) {
55
55
  assertScope(value);
56
56
  const key = lockKey(kind, value), port = chromeApi.runtime.connect({ name: LOCK_PORT });
57
57
  let timer, heartbeat, released = false, lock = null, rejectGrant;
58
58
  const granted = new Promise((resolve, reject) => {
59
59
  rejectGrant = reject;
60
60
  port.onMessage.addListener(message => {
61
+ if (message?.type === 'busy') { reject(failure('review_busy', 'Another page is reviewing this run. Wait for it to finish.')); return; }
61
62
  if (message?.type !== 'granted' || lock || released) return;
62
63
  clearTimeout(timer);
63
- lock = { token: message.token, lost: false };
64
+ lock = { token: message.token, deadlineAt: message.deadlineAt, lost: false };
64
65
  heldLocks.set(key, lock);
65
66
  resolve();
66
67
  });
67
68
  port.onDisconnect.addListener(() => {
68
69
  // Read lastError to consume the runtime's disconnected-port warning.
69
70
  void chromeApi.runtime.lastError;
70
- if (lock) lock.lost = true;
71
+ if (lock) { lock.lost = true; if (!released) onLost?.(lock.token); }
71
72
  if (!released) reject(failure('storage_lock_lost', 'The shared history lock disconnected.'));
72
73
  });
73
74
  timer = environment.setTimeout(() => reject(failure('storage_lock_timeout',
@@ -84,12 +85,27 @@
84
85
  port.postMessage({ type: 'acquire', kind, scope: value });
85
86
  await granted;
86
87
  assertScope(value);
87
- const result = await work();
88
+ const assertOwner = () => { assertScope(value); if (lock.lost) throw failure('storage_lock_lost', 'The shared review owner disconnected.'); };
89
+ assertOwner.token = lock.token;
90
+ assertOwner.markDispatched = async () => {
91
+ assertOwner();
92
+ const lease = await request('review-dispatched', value, { runId: value.runId, reviewToken: lock.token });
93
+ assertOwner(); assertOwner.deadlineAt = lease.deadlineAt; return lease.deadlineAt;
94
+ };
95
+ const result = await work(assertOwner);
88
96
  assertScope(value);
89
97
  if (lock.lost) throw failure('storage_lock_lost', 'The shared history lock was lost before completion.');
90
98
  return result;
91
99
  } finally {
92
100
  released = true;
101
+ if (kind === 'review' && lock && !lock.lost) {
102
+ await new Promise(resolve => {
103
+ const done = message => { if (message?.type === 'released') { port.onMessage.removeListener(done); environment.clearTimeout(releaseTimer); resolve(); } };
104
+ const releaseTimer = environment.setTimeout(resolve, 2000);
105
+ port.onMessage.addListener(done);
106
+ try { port.postMessage({ type: 'release-review', token: lock.token }); } catch (_) { resolve(); }
107
+ });
108
+ }
93
109
  environment.clearTimeout(timer);
94
110
  environment.clearInterval(heartbeat);
95
111
  if (heldLocks.get(key) === lock) heldLocks.delete(key);
@@ -179,7 +195,8 @@
179
195
  record: clean(record),
180
196
  base: record[BASE] || baselines.get(cacheKey(value, record.id)) || null
181
197
  }));
182
- const written = await request('put', value, { entries, reviewRunIds: options.reviewRunIds || [] });
198
+ const transfer = await Transfer.encode(entries);
199
+ const written = await request(transfer.method, value, { ...transfer.payload, reviewRunIds: options.reviewRunIds || [] });
183
200
  // Keep the caller's submitted view as its merge baseline. A merged
184
201
  // remote change has not necessarily been applied to the visible UI.
185
202
  for (const entry of entries) baselines.set(cacheKey(value, entry.record.id), clean(entry.record));
@@ -192,8 +209,8 @@
192
209
  const record = await request('get', value, { id });
193
210
  return tag(record, value, !heldLocks.has(lockKey('state', value)));
194
211
  }
195
- async function erase(method, projectId = '', id) {
196
- const value = capture(projectId);
212
+ async function erase(method, projectId = '', id, accountScopeId) {
213
+ const value = capture(projectId, accountScopeId);
197
214
  return withWrite(value, () => request(method, value, { id }));
198
215
  }
199
216
  function decorate(api) {
@@ -225,8 +242,9 @@
225
242
  store === 'sessions' ? (await put([record]))[0] : api.putRecord(store, record));
226
243
  route('putRecords', (store, records, options) =>
227
244
  store === 'sessions' ? put(records, options) : api.putRecords(store, records));
228
- route('deleteRecord', (store, id) =>
229
- store === 'sessions' ? erase('delete', '', id) : api.deleteRecord(store, id));
245
+ route('deleteRecord', (store, id, expectedScope) =>
246
+ store === 'sessions' ? erase('delete', expectedScope?.projectId || '', id, expectedScope?.accountScopeId)
247
+ : api.deleteRecord(store, id));
230
248
  route('deleteByIndex', async (store, index, value) => {
231
249
  if (store !== 'sessions') return api.deleteByIndex(store, index, value);
232
250
  if (index === 'projectId') return erase('clear-project', value);
@@ -243,7 +261,7 @@
243
261
  for (const record of await api.getAllSessions()) {
244
262
  if (record.accountScopeId === value.accountScopeId) await api.deleteRecord('sessions', record.id);
245
263
  }
246
- for (const store of Object.keys(api.STORES)) if (store !== 'sessions') await api.clearStore(store);
264
+ await api.clearAuxiliaryStores();
247
265
  baselines.clear();
248
266
  });
249
267
  route('listRecentProjectsAcrossAccount', async options => {
@@ -253,6 +271,11 @@
253
271
  });
254
272
  Object.assign(wrapped, {
255
273
  sharedSessionsEnabled: enabled,
274
+ withSharedReviewLock: async (input, work) => {
275
+ const value = { ...capture(input.projectId, input.accountScopeId), runId: input.runId };
276
+ return acquire('review', value, work, input.onLost);
277
+ },
278
+ hasSharedReviewOwner: input => request('review-owner', capture(input.projectId, input.accountScopeId), { runId: input.runId }),
256
279
  withSharedLock: withLock,
257
280
  readSharedProjectSnapshot: projectId => list(projectId, false),
258
281
  adoptSharedSessionBaseline: adopt,