codex-overleaf-link 2.5.1 → 2.6.5

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 (111) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +148 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -1,7 +1,7 @@
1
1
  (function initStorageDb(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
3
  module.exports = factory(require('./storageRunActions'), require('./runInputQueue'),
4
- require('./settlementFacts'), require('./storageSessionClaims'), require('./sessionState'), require('./runActivityModel'), require('./sharedSessionBridge'), require('./subagentActivityModel'), require('./storageValuePruning'), require('./composerAttachmentDraft'), require('./sharedSessionTransfer'));
4
+ require('./settlementFacts'), require('./storageSessionClaims'), require('./sessionState'), require('./runActivityModel'), require('./sharedSessionBridge'), require('./subagentActivityModel'), require('./storageValuePruning'), require('./composerAttachmentDraft'), require('./sharedSessionTransfer'), require('./contextUsage'));
5
5
  } else {
6
6
  root.CodexOverleafModuleRegistry.define('StorageDb', [
7
7
  'StorageRunActions',
@@ -14,10 +14,11 @@
14
14
  'SubagentActivityModel',
15
15
  'StorageValuePruning',
16
16
  'ComposerAttachmentDraft',
17
- 'SharedSessionTransfer'
17
+ 'SharedSessionTransfer',
18
+ 'ContextUsage'
18
19
  ], factory);
19
20
  }
20
- })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims, SessionState, RunActivityModel, SharedSessionBridge, SubagentActivityModel, StorageValuePruning, ComposerAttachmentDraft, SharedSessionTransfer) {
21
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue, SettlementFacts, StorageSessionClaims, SessionState, RunActivityModel, SharedSessionBridge, SubagentActivityModel, StorageValuePruning, ComposerAttachmentDraft, SharedSessionTransfer, ContextUsage) {
21
22
  'use strict';
22
23
  const { removeEmptySummaryFields, removeEmptyFields } = StorageValuePruning;
23
24
  if (!StorageRunActions?.compactRunsForStorage || !StorageRunActions?.compactRunActionPayload || !StorageRunActions?.compactProviderSnapshot || !StorageRunActions?.compactRunExecutionSnapshot || !StorageRunActions?.compactStructuredEventValue || !StorageRunActions?.hashString || !RunInputQueue?.compactForStorage || !SettlementFacts?.compactSettlementFacts || !StorageSessionClaims?.createSessionClaimer) {
@@ -276,6 +277,7 @@
276
277
  title: normalizeSessionTitleForStorage(input.title, titleSource),
277
278
  titleSource: titleSource,
278
279
  codexThreadId: typeof input.codexThreadId === 'string' ? input.codexThreadId : '',
280
+ contextUsage: ContextUsage.normalize(input.contextUsage),
279
281
  forkedFromSessionId: normalizeTextField(input.forkedFromSessionId, 160),
280
282
  forkedFromRunId: normalizeTextField(input.forkedFromRunId, 160),
281
283
  status: typeof input.status === 'string' && input.status ? input.status : 'active',
@@ -303,6 +305,17 @@
303
305
  accountScopeUnavailable: accountScopeUnavailable,
304
306
  safeTaskSummary: safeTaskSummary
305
307
  };
308
+ // Optional provenance for the automatic clear of one captured submission.
309
+ // It is envelope metadata, not run/review authority; no schema migration is needed.
310
+ if (record.task === '' && typeof input.draftConsumption?.task === 'string'
311
+ && input.draftConsumption.task && typeof input.draftConsumption.runId === 'string'
312
+ && input.draftConsumption.runId && record.runs.some(run => run.id === input.draftConsumption.runId
313
+ && !run.forkSnapshot && run.runProjectId === record.projectId)) {
314
+ record.draftConsumption = {
315
+ runId: input.draftConsumption.runId,
316
+ task: normalizeDisplayTextForStorage(input.draftConsumption.task, SESSION_STORAGE_LIMITS.taskChars)
317
+ };
318
+ }
306
319
  if (options.preserveRunActionPayload === true) {
307
320
  // Count the real display record before allocating recovery space. Adding
308
321
  // older checkpoints must not make an otherwise valid history unwritable.
@@ -436,6 +449,8 @@
436
449
  turnId: typeof input.turnId === 'string' ? input.turnId : '',
437
450
  createdAt: typeof input.createdAt === 'string' ? input.createdAt : now,
438
451
  completedAt: typeof input.completedAt === 'string' ? input.completedAt : '',
452
+ requireReviewing: typeof input.requireReviewing === 'boolean' ? input.requireReviewing : undefined,
453
+ trackingDecision: ['continue', 'disable'].includes(input.trackingDecision) ? input.trackingDecision : '',
439
454
  mode: typeof input.mode === 'string' ? input.mode : '', ...StorageRunActions.compactProviderSnapshot(input),
440
455
  model: typeof input.model === 'string' ? input.model : '',
441
456
  reasoningEffort: typeof input.reasoningEffort === 'string' ? input.reasoningEffort : '',
@@ -708,7 +723,9 @@
708
723
  undoStatus: normalizeDisplayTextForStorage(run.undoStatus, SESSION_STORAGE_LIMITS.statusTextChars),
709
724
  ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
710
725
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
726
+ ...(run.operation === 'compact' ? { operation: 'compact' } : {}),
711
727
  codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
728
+ contextUsage: run.forkSnapshot === true ? null : ContextUsage.normalize(run.contextUsage),
712
729
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
713
730
  queueItemId: normalizeTextField(run.queueItemId, 160)
714
731
  };
@@ -775,6 +792,19 @@
775
792
  if (event.failure) {
776
793
  compact.failure = compactStructuredEventValueForStorage(event.failure, 0);
777
794
  }
795
+ if (Array.isArray(event.compileErrors) && event.compileErrors.length) {
796
+ compact.compileErrors = event.compileErrors.slice(0, 5)
797
+ .map(item => normalizeDisplayTextForStorage(String(item), SESSION_STORAGE_LIMITS.detailChars))
798
+ .filter(Boolean);
799
+ }
800
+ if (Array.isArray(event.rejectedHunks) && event.rejectedHunks.length) {
801
+ compact.rejectedHunks = event.rejectedHunks.slice(0, 10)
802
+ .map(item => ({
803
+ path: normalizeDisplayTextForStorage(String(item?.path || ''), SESSION_STORAGE_LIMITS.pathChars),
804
+ summary: normalizeDisplayTextForStorage(String(item?.summary || ''), SESSION_STORAGE_LIMITS.detailChars)
805
+ }))
806
+ .filter(item => item.path);
807
+ }
778
808
  return compact;
779
809
  });
780
810
  }
@@ -1343,6 +1373,8 @@
1343
1373
  projectRetentionWarnings: StorageRunActions.projectRetentionWarnings,
1344
1374
  projectRetentionUpdates: StorageRunActions.projectRetentionUpdates,
1345
1375
  mergeSessionReviewState: StorageRunActions.mergeSessionReviewState,
1376
+ copyRunReviewState: StorageRunActions.copyRunReviewState,
1377
+ reviewStateSnapshot: StorageRunActions.reviewStateSnapshot,
1346
1378
  buildTurnRecord: buildTurnRecord,
1347
1379
  buildEventRecord: buildEventRecord,
1348
1380
  buildArtifactRecord: buildArtifactRecord,
@@ -19,9 +19,105 @@
19
19
  'experimentalOtByProject',
20
20
  'customInstructionsByProject',
21
21
  'governanceRulesByProject',
22
+ 'reviewingAvailabilityByProject',
22
23
  'selectedLocalSkillIdsByProject'
23
24
  ];
24
25
  var projectPreferenceLockTails = {};
26
+ var projectPreferenceBaselines = new Map();
27
+ var projectPreferenceIntents = new Map();
28
+ var projectPreferenceBaselineVersions = new WeakMap();
29
+
30
+ function getProjectPreferenceBaseline(accountScopeId, projectId) {
31
+ var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
32
+ var baseline = copyPreferenceValue(projectPreferenceBaselines.get(key));
33
+ var intents = projectPreferenceIntents.get(key);
34
+ if (baseline && intents) projectPreferenceBaselineVersions.set(baseline, {
35
+ owner: intents, versions: Object.assign({}, intents.acknowledged)
36
+ });
37
+ return baseline;
38
+ }
39
+
40
+ function adoptProjectPreferenceBaseline(prefs, accountScopeId, projectId) {
41
+ var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
42
+ if (key) {
43
+ projectPreferenceBaselines.set(key, copyPreferenceValue(prefs));
44
+ projectPreferenceIntents.set(key, { revision: 0, fields: {}, acknowledged: {} });
45
+ }
46
+ }
47
+
48
+ function captureProjectPreferenceWrite(input) {
49
+ var projectId = input.projectId;
50
+ var key = buildScopedProjectPreferenceKey(input.accountScopeId, projectId);
51
+ if (!key) return null;
52
+ var intents = projectPreferenceIntents.get(key);
53
+ if (!intents) {
54
+ intents = { revision: 0, fields: {}, acknowledged: {} };
55
+ projectPreferenceIntents.set(key, intents);
56
+ }
57
+ function values(prefs) {
58
+ return {
59
+ experimentalOtByProject: input.normalizeExperimentalOtByProject(prefs.experimentalOtByProject)[projectId] === true,
60
+ reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject?.[projectId],
61
+ governanceRulesByProject: input.normalizeGovernanceRulesByProject({
62
+ [projectId]: prefs.governanceRulesByProject?.[projectId] || {}
63
+ })[projectId],
64
+ customInstructionsByProject: input.normalizeCustomInstructionsByProject({
65
+ [projectId]: prefs.customInstructionsByProject?.[projectId] || ''
66
+ })[projectId]
67
+ };
68
+ }
69
+ var baseline = input.baseline || getProjectPreferenceBaseline(input.accountScopeId, projectId);
70
+ var captured = projectPreferenceBaselineVersions.get(baseline);
71
+ var versions = captured?.owner === intents ? captured.versions : {};
72
+ var previous = values(baseline || {});
73
+ var current = values(input.state);
74
+ var token = { key: key, owner: intents, projectId: projectId, fields: {}, versions: {}, written: [] };
75
+ for (var name of Object.keys(current)) {
76
+ var latest = intents.fields[name];
77
+ // A pending or later local intent supersedes the older captured read.
78
+ // Peer adoption alone does not turn an unchanged queued snapshot into an edit.
79
+ var reference = latest && latest.revision > (versions[name] || 0) ? latest.value : previous[name];
80
+ if (JSON.stringify(current[name]) !== JSON.stringify(reference)) {
81
+ latest = { revision: ++intents.revision, value: copyPreferenceValue(current[name]), dirty: true };
82
+ intents.fields[name] = latest;
83
+ }
84
+ token.fields[name] = latest && JSON.stringify(current[name]) === JSON.stringify(latest.value) ? latest : null;
85
+ token.versions[name] = latest?.revision || 0;
86
+ }
87
+ return token;
88
+ }
89
+
90
+ function projectPreferencePatch(token, accountScopeId, projectId) {
91
+ if (token.key !== buildScopedProjectPreferenceKey(accountScopeId, projectId)) {
92
+ throw Object.assign(new Error('Scoped project preferences require account and project identity.'), {
93
+ code: 'account_scope_unavailable'
94
+ });
95
+ }
96
+ var current = projectPreferenceIntents.get(token.key) === token.owner;
97
+ var patch = {};
98
+ token.written = [];
99
+ for (var name of Object.keys(token.fields)) {
100
+ var intent = token.fields[name];
101
+ var changed = current && intent?.dirty && token.owner.fields[name] === intent;
102
+ patch[name] = changed ? { [token.projectId]: copyPreferenceValue(intent.value) } : {};
103
+ if (changed) token.written.push(name);
104
+ }
105
+ return patch;
106
+ }
107
+
108
+ function completeProjectPreferenceWrite(token, prefs) {
109
+ if (projectPreferenceIntents.get(token.key) !== token.owner) return [];
110
+ for (var name of token.written) {
111
+ var intent = token.fields[name];
112
+ intent.dirty = false;
113
+ token.owner.acknowledged[name] = Math.max(token.owner.acknowledged[name] || 0, intent.revision);
114
+ }
115
+ projectPreferenceBaselines.set(token.key, copyPreferenceValue(prefs));
116
+ return Object.keys(token.fields).filter(function (name) {
117
+ var latest = token.owner.fields[name];
118
+ return (latest?.revision || 0) === token.versions[name] && !latest?.dirty;
119
+ });
120
+ }
25
121
 
26
122
  function buildScopedProjectPreferenceKey(accountScopeId, projectId) {
27
123
  var account = compactScopePart(accountScopeId);
@@ -130,7 +226,12 @@
130
226
  ? incoming[mapName]
131
227
  : {};
132
228
  if (Object.prototype.hasOwnProperty.call(incomingMap, projectId)) {
133
- storedMap[scopedKey] = copyPreferenceValue(incomingMap[projectId]);
229
+ const previous = storedMap[scopedKey], observation = incomingMap[projectId];
230
+ if (mapName !== 'reviewingAvailabilityByProject'
231
+ || !(Number(previous?.observedAt) > Number(observation?.observedAt)
232
+ || (previous?.observedAt === observation?.observedAt && previous?.status === 'available'))) {
233
+ storedMap[scopedKey] = copyPreferenceValue(observation);
234
+ }
134
235
  }
135
236
  delete storedMap[projectId];
136
237
  next[mapName] = storedMap;
@@ -164,6 +265,7 @@
164
265
  var visible = await StorageDb.claimSessionsForAccount(projectId, account, deleted);
165
266
  function result(currentPrefs, sessions, migrated) {
166
267
  var wanted = currentPrefs.activeSessionByProject?.[projectId] || '';
268
+ adoptProjectPreferenceBaseline(currentPrefs, account, projectId);
167
269
  return { prefs: currentPrefs, sessions: sessions,
168
270
  activeSessionId: sessions.some(session => session.id === wanted) ? wanted : sessions.at(-1)?.id || '',
169
271
  migrated: migrated };
@@ -254,7 +356,9 @@
254
356
  return chrome.storage.local.get([PREFS_KEY]).then(function (stored) {
255
357
  var claimed = claimLegacyProjectPreferences(stored[PREFS_KEY] || {}, account, project);
256
358
  var next = mergeScopedProjectPreferences(claimed.prefs, prefs, account, project);
257
- return chrome.storage.local.set({ [PREFS_KEY]: next });
359
+ return chrome.storage.local.set({ [PREFS_KEY]: next }).then(function () {
360
+ return buildProjectPreferenceView(next, account, project);
361
+ });
258
362
  });
259
363
  });
260
364
  }
@@ -458,6 +562,11 @@
458
562
  runMigrationIfNeeded: runMigrationIfNeeded,
459
563
  savePrefs: savePrefs,
460
564
  loadPrefs: loadPrefs,
565
+ getProjectPreferenceBaseline: getProjectPreferenceBaseline,
566
+ adoptProjectPreferenceBaseline: adoptProjectPreferenceBaseline,
567
+ captureProjectPreferenceWrite: captureProjectPreferenceWrite,
568
+ projectPreferencePatch: projectPreferencePatch,
569
+ completeProjectPreferenceWrite: completeProjectPreferenceWrite,
461
570
  loadSessionTombstones: loadSessionTombstones,
462
571
  addSessionTombstones: addSessionTombstones,
463
572
  getDeletedSessionIds: getDeletedSessionIds,
@@ -1,10 +1,10 @@
1
1
  (function initStorageRunActions(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'), require('./directoryCleanupProof'));
3
+ module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'), require('./directoryCleanupProof'), require('./undoOperations'));
4
4
  } else {
5
- root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle', 'DirectoryCleanupProof'], factory);
5
+ root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle', 'DirectoryCleanupProof', 'UndoOperations'], factory);
6
6
  }
7
- })(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle, DirectoryCleanupProof) {
7
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle, DirectoryCleanupProof, UndoOperations) {
8
8
  'use strict';
9
9
 
10
10
  // Share a bounded budget across the retained history, rather than expiring
@@ -129,7 +129,7 @@
129
129
  && Array.isArray(run.undoExpectedFiles) && run.undoExpectedFiles.length > 0
130
130
  && Array.isArray(run.appliedOperations) && run.appliedOperations.length > 0;
131
131
  var legacyUndo = Array.isArray(run && run.undoOperations) && run.undoOperations.length > 0;
132
- return trackedLifecycle || reviewingEditorUndo || legacyUndo;
132
+ return trackedLifecycle || reviewingEditorUndo || legacyUndo || UndoOperations.getUndoUnavailableFiles(run).length > 0;
133
133
  }
134
134
 
135
135
  function runDirectoryCleanup(run) {
@@ -186,7 +186,7 @@
186
186
  }
187
187
 
188
188
  function compactCoreActionPayload(run, keepActionPayload, options = {}) {
189
- var empty = emptyActionPayload();
189
+ var empty = { ...emptyActionPayload(), appliedOperations: UndoOperations.compactUnavailableUndoOperations(run) };
190
190
  if (!keepActionPayload || !hasReloadableRunActionPayload(run)) {
191
191
  if (hasReloadableRunActionPayload(run)) empty.recoveryRetention = omitted('fallback');
192
192
  else if (run?.recoveryRetention?.state === 'omitted' && !run.forkSnapshot
@@ -308,11 +308,48 @@
308
308
  return out;
309
309
  }
310
310
 
311
- // Review terminal states are monotonic even when a later session snapshot is stale.
312
- function mergeSessionReviewState(incoming, existing, reviewRunIds) {
311
+ const REVIEW_FIELDS = ['trackedChangeStatus', 'undoStatus', 'undoOwnerProtocol', 'undoTrackedChanges', 'undoExpectedFiles',
312
+ 'undoOperations', 'undoBaseFiles', 'appliedOperations', 'trackedChangeCaptures', 'recoveryRetention',
313
+ 'interruptedDraft', 'retryWriteback', 'saveCheck'];
314
+ const REVIEW_FACT_FIELDS = ['documentEffect', 'fileSettlements', 'failures', 'recoveryAvailability', 'recoveryDisposition'];
315
+ const terminalReview = run => ['accepted', 'rejected'].includes(run?.trackedChangeStatus) || run?.undoStatus === 'applied';
316
+ const factsOf = run => run?.settlement || run?.settlementFacts || {};
317
+
318
+ function copyRunReviewState(target, source) {
319
+ const merged = { ...target };
320
+ const copyFields = (into, from, fields) => {
321
+ for (const field of fields) {
322
+ if (Object.hasOwn(from, field)) into[field] = JSON.parse(JSON.stringify(from[field]));
323
+ else delete into[field];
324
+ }
325
+ };
326
+ copyFields(merged, source, REVIEW_FIELDS);
327
+ const sourceFacts = factsOf(source), targetFacts = factsOf(target);
328
+ const facts = { ...targetFacts };
329
+ copyFields(facts, sourceFacts, REVIEW_FACT_FIELDS);
330
+ const sourceEvidence = sourceFacts.evidence || {};
331
+ const evidence = { ...(targetFacts.evidence || {}) };
332
+ copyFields(evidence, sourceEvidence, [...new Set([...Object.keys(evidence), ...Object.keys(sourceEvidence)])]
333
+ .filter(key => !['mirrored', 'compiled'].includes(key)));
334
+ if (Object.keys(evidence).length) facts.evidence = evidence; else delete facts.evidence;
335
+ delete merged.settlementFacts;
336
+ if (Object.keys(facts).length) merged.settlement = facts; else delete merged.settlement;
337
+ return merged;
338
+ }
339
+
340
+ function reviewStateSnapshot(run) {
341
+ const facts = factsOf(run);
342
+ return JSON.stringify([REVIEW_FIELDS.map(key => run?.[key]), REVIEW_FACT_FIELDS.map(key => facts[key]),
343
+ Object.entries(facts.evidence || {}).filter(([key]) => !['mirrored', 'compiled'].includes(key))
344
+ .sort(([a], [b]) => a.localeCompare(b))]);
345
+ }
346
+
347
+ // Shared submissions retain the caller's envelope and private baseline. The
348
+ // legacy site-local path explicitly keeps its historical timestamp merge.
349
+ function mergeSessionReviewState(incoming, existing, reviewRunIds, options = {}) {
313
350
  if (!existing || incoming.id !== existing.id || incoming.projectId !== existing.projectId
314
351
  || !incoming.accountScopeId || incoming.accountScopeId !== existing.accountScopeId) return incoming;
315
- var terminal = run => ['accepted', 'rejected'].includes(run?.trackedChangeStatus) || run?.undoStatus === 'applied';
352
+ var terminal = terminalReview;
316
353
  var incomingRuns = new Map((incoming.runs || []).map(run => [run.id, run]));
317
354
  var existingRuns = new Map((existing.runs || []).map(run => [run.id, run]));
318
355
  var requestedIds = new Set(reviewRunIds || []);
@@ -321,6 +358,21 @@
321
358
  return ['accepted', 'rejected', 'needs_review'].includes(run?.trackedChangeStatus)
322
359
  || ['applied', 'partial'].includes(run?.undoStatus);
323
360
  });
361
+ if (options.shared !== false) {
362
+ const runs = (incoming.runs || []).map(run => {
363
+ const previous = existingRuns.get(run.id);
364
+ const source = terminal(previous) ? previous : null;
365
+ if (!source || (source.runProjectId && source.runProjectId !== incoming.projectId)) return run;
366
+ // The same decision can still gain save/recovery evidence. Let the
367
+ // three-way merge retain those edits and reject genuine collisions.
368
+ if (run.trackedChangeStatus === source.trackedChangeStatus && run.undoStatus === source.undoStatus) return run;
369
+ // An absent local settlement has no edits to its metadata or auxiliary
370
+ // evidence. Retain the stored container as well as the terminal fields.
371
+ const target = !run.settlement && !run.settlementFacts ? { ...run, settlement: factsOf(source) } : run;
372
+ return copyRunReviewState(target, source);
373
+ });
374
+ return { ...incoming, runs };
375
+ }
324
376
  var newer = (Date.parse(incoming.updatedAt) || 0) >= (Date.parse(existing.updatedAt) || 0);
325
377
  if (!newer && !explicitReview) return incoming;
326
378
  var base = newer ? incoming : existing;
@@ -404,6 +456,8 @@
404
456
  compactSessionBatchActions: compactSessionBatchActions,
405
457
  compactStructuredEventValue: compactStructuredEventValue,
406
458
  mergeSessionReviewState: mergeSessionReviewState,
459
+ copyRunReviewState: copyRunReviewState,
460
+ reviewStateSnapshot: reviewStateSnapshot,
407
461
  compactRunsForStorage: compactRunsForStorage,
408
462
  compactRunActionPayload: compactRunActionPayload,
409
463
  compactProviderSnapshot: compactProviderSnapshot,
@@ -8,6 +8,41 @@
8
8
  'use strict';
9
9
 
10
10
  function buildUndoOperations(project, appliedOperations) {
11
+ return buildUndoPlan(project, appliedOperations).undoOperations;
12
+ }
13
+
14
+ function getUndoUnavailableFiles(run = {}) {
15
+ const payload = run.recoveryPayload || run;
16
+ const files = new Map();
17
+ for (const operation of payload.appliedOperations || []) {
18
+ const reason = operation?.undoUnavailable;
19
+ if (typeof reason?.path === 'string' && reason.path === operation?.path
20
+ && ['stale_source_changed', 'snapshot_undo_unavailable'].includes(reason.code)) {
21
+ files.set(reason.path, { path: reason.path, code: reason.code });
22
+ }
23
+ }
24
+ return [...files.values()];
25
+ }
26
+
27
+ function withAvailableUndo(run = {}) {
28
+ const unavailable = new Set(getUndoUnavailableFiles(run).map(item => item.path));
29
+ if (!unavailable.size) return run;
30
+ const result = { ...run };
31
+ for (const field of ['undoOperations', 'undoBaseFiles', 'undoExpectedFiles', 'undoTrackedChanges', 'trackedChangeCaptures']) {
32
+ result[field] = (run[field] || []).filter(item => !unavailable.has(item?.path) && !unavailable.has(item?.to));
33
+ }
34
+ return result;
35
+ }
36
+
37
+ // Fallback history keeps only bounded reasons, never document bytes or
38
+ // authority to reconstruct a withheld checkpoint.
39
+ function compactUnavailableUndoOperations(run = {}) {
40
+ if (run.forkSnapshot || run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) return [];
41
+ return getUndoUnavailableFiles(run).filter(item => item.path.length <= 1024).slice(0, 100)
42
+ .map(item => ({ type: 'edit', path: item.path, undoUnavailable: item }));
43
+ }
44
+
45
+ function buildUndoPlan(project, appliedOperations) {
11
46
  const workingFilesByPath = new Map();
12
47
  for (const file of project?.files || []) {
13
48
  if (typeof file?.path === 'string' && typeof file.content === 'string') {
@@ -17,6 +52,10 @@
17
52
 
18
53
  const sourceFilesByPath = new Map(workingFilesByPath);
19
54
  const undo = [];
55
+ const inverseBases = new Map();
56
+ const mappedPaths = new Set((appliedOperations || []).filter(operation => Array.isArray(operation?.appliedPatches))
57
+ .map(operation => operation.path));
58
+ const unavailable = new Map(getUndoUnavailableFiles({ appliedOperations }).map(item => [item.path, item.code]));
20
59
  const fullRestoreEditPaths = new Set();
21
60
 
22
61
  for (const operation of appliedOperations || []) {
@@ -30,23 +69,33 @@
30
69
  }
31
70
  const currentBeforeEdit = workingFilesByPath.get(operation.path);
32
71
  if (!fullRestoreEditPaths.has(operation.path)) {
33
- const inversePatches = buildInverseTextPatches(sourceFilesByPath.get(operation.path), operation.patches,
34
- operation.verifiedContent, operation.path);
72
+ const hasAppliedPatches = Array.isArray(operation.appliedPatches) && typeof operation.verifiedContent === 'string';
73
+ const inversePatches = hasAppliedPatches
74
+ ? buildVerifiedInversePatches(operation.appliedPatches, operation.verifiedContent)
75
+ : buildInverseTextPatches(sourceFilesByPath.get(operation.path), operation.patches,
76
+ operation.verifiedContent, operation.path);
77
+ let inverse;
35
78
  if (inversePatches.length) {
36
- undo.push(normalizeUndoOperation({
79
+ inverse = normalizeUndoOperation({
37
80
  type: 'edit',
38
81
  path: operation.path,
39
82
  patches: inversePatches,
40
83
  reason: 'Undo edit'
41
- }));
42
- } else {
84
+ });
85
+ } else if (!hasAppliedPatches) {
43
86
  fullRestoreEditPaths.add(operation.path);
44
- undo.push(normalizeUndoOperation({
87
+ inverse = normalizeUndoOperation({
45
88
  type: 'edit',
46
89
  path: operation.path,
47
90
  replaceAll: currentBeforeEdit,
48
91
  reason: 'Undo edit'
49
- }));
92
+ });
93
+ } else {
94
+ unavailable.set(operation.path, 'snapshot_undo_unavailable');
95
+ }
96
+ if (inverse) {
97
+ undo.push(inverse);
98
+ if (typeof operation.verifiedContent === 'string') inverseBases.set(inverse, operation.verifiedContent);
50
99
  }
51
100
  }
52
101
  } else if (operation.type === 'create') {
@@ -97,7 +146,51 @@
97
146
  applyOperationToFiles(sourceFilesByPath, { ...operation, verifiedContent: undefined });
98
147
  }
99
148
 
100
- return undo.reverse();
149
+ // Each inverse was measured in its own verified post-image. Compose in
150
+ // reverse order against the latest image, preserving changes between
151
+ // entries. Empty inverse insertions must never reuse an older offset.
152
+ const composed = [];
153
+ for (const inverse of undo.reverse()) {
154
+ let operation = inverse;
155
+ if (mappedPaths.has(inverse.path) && inverseBases.has(inverse)) {
156
+ const current = workingFilesByPath.get(inverse.path);
157
+ const base = StaleGuard.buildBaseFileLookup([{ path: inverse.path, content: inverseBases.get(inverse) }]);
158
+ const fresh = StaleGuard.checkOperationFreshness(inverse, current, base);
159
+ const prepared = fresh.ok && inverse.patches
160
+ ? fresh.preparedPatches || StaleGuard.prepareOperationPatches(inverse, current, base) : fresh;
161
+ if (typeof current !== 'string' || !prepared.ok) {
162
+ unavailable.set(inverse.path, 'stale_source_changed');
163
+ continue;
164
+ }
165
+ if (inverse.patches) operation = { ...inverse, patches: prepared.patches };
166
+ }
167
+ composed.push(operation);
168
+ applyOperationToFiles(workingFilesByPath, operation);
169
+ }
170
+ return { undoOperations: composed.filter(operation => !unavailable.has(operation.path)),
171
+ undoUnavailable: [...unavailable].map(([path, code]) => ({ path, code })) };
172
+ }
173
+
174
+ // Invert the ranges actually dispatched to C, not the original Native
175
+ // coordinates in B. Verify against the observed post-image before storing
176
+ // recovery; invalid evidence must not fall back to overwriting C with B.
177
+ function buildVerifiedInversePatches(patches, postImage) {
178
+ const beforeLength = patches.reduce((length, patch) =>
179
+ length + Number(patch?.to) - Number(patch?.from) - String(patch?.insert ?? '').length, postImage.length);
180
+ if (!Number.isSafeInteger(beforeLength) || beforeLength < 0) return [];
181
+ const normalized = normalizeTextPatches(patches, beforeLength);
182
+ if (!normalized.ok || normalized.patches.some(patch => patch.expected.length !== patch.to - patch.from)) return [];
183
+ const inverse = invertPatches(normalized.patches);
184
+ return applyTextPatches(postImage, inverse).ok ? inverse : [];
185
+ }
186
+
187
+ function invertPatches(patches) {
188
+ let offset = 0;
189
+ return patches.map(patch => {
190
+ const from = patch.from + offset;
191
+ offset += patch.insert.length - (patch.to - patch.from);
192
+ return { from, to: from + patch.insert.length, expected: patch.insert, insert: patch.expected };
193
+ });
101
194
  }
102
195
 
103
196
  function buildInverseTextPatches(textBeforeEdit, patches, verifiedContent, path) {
@@ -113,28 +206,20 @@
113
206
  return [];
114
207
  }
115
208
 
116
- let offset = 0;
117
- const inverse = normalized.patches.map(patch => {
118
- const from = patch.from + offset;
119
- const to = from + patch.insert.length;
120
- offset += patch.insert.length - (patch.to - patch.from);
121
- return {
122
- from,
123
- to,
124
- expected: patch.insert,
125
- insert: patch.expected
126
- };
127
- });
209
+ const inverse = invertPatches(normalized.patches);
128
210
  if (typeof verifiedContent !== 'string' || verifiedContent === applied.text) return inverse;
129
211
  // The stored checkpoint uses the verified editor post-image. Its inverse
130
212
  // must use those same offsets, while Native patches retain their source
131
213
  // representation for later operations in the batch.
132
- const prepared = StaleGuard.prepareOperationPatches({ type: 'edit', path, patches: inverse }, verifiedContent,
214
+ const prepared = StaleGuard.prepareStoredInversePatches({ type: 'edit', path, patches: inverse }, verifiedContent,
133
215
  StaleGuard.buildBaseFileLookup([{ path, content: applied.text }]));
134
216
  return prepared.ok && applyTextPatches(verifiedContent, prepared.patches).ok ? prepared.patches : [];
135
217
  }
136
218
 
137
219
  function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = [], previousExpectedFiles = []) {
220
+ const checkpoint = buildUndoCheckpoint(project, operations);
221
+ const unavailable = new Set(checkpoint.undoUnavailable.map(item => item.path));
222
+ const mappedPaths = new Set(operations.filter(operation => Array.isArray(operation?.appliedPatches)).map(operation => operation.path));
138
223
  const paths = new Set();
139
224
  for (const change of trackedChanges || []) if (change?.path) paths.add(change.path);
140
225
  for (const operation of operations || []) {
@@ -143,7 +228,8 @@
143
228
  }
144
229
  const expectedByPath = new Map();
145
230
  for (const file of previousExpectedFiles || []) {
146
- if (paths.has(file?.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
231
+ if (paths.has(file?.path) && !unavailable.has(file.path) && !mappedPaths.has(file.path)
232
+ && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
147
233
  expectedByPath.set(file.path, { path: file.path, content: file.content });
148
234
  }
149
235
  }
@@ -152,10 +238,9 @@
152
238
  // content, including when the ZIP baseline used a different representation.
153
239
  const hasVerifiedPatches = operations.some(operation => typeof operation?.verifiedContent === 'string'
154
240
  && Array.isArray(operation.patches) && operation.patches.length);
155
- const checkpoint = hasVerifiedPatches ? buildUndoCheckpoint(project, operations) : null;
156
- const restored = checkpoint ? buildExpectedFilesAfterOperations({ files: checkpoint.undoBaseFiles }, checkpoint.undoOperations) : new Map();
241
+ const restored = hasVerifiedPatches ? buildExpectedFilesAfterOperations({ files: checkpoint.undoBaseFiles }, checkpoint.undoOperations) : new Map();
157
242
  for (const file of project?.files || []) {
158
- if (paths.has(file?.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
243
+ if (paths.has(file?.path) && !unavailable.has(file.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
159
244
  expectedByPath.set(file.path, { path: file.path, content: restored.get(file.path) ?? file.content });
160
245
  }
161
246
  }
@@ -163,11 +248,15 @@
163
248
  }
164
249
 
165
250
  function buildUndoCheckpoint(project, appliedOperations) {
166
- const undoOperations = buildUndoOperations(project, appliedOperations);
251
+ const { undoOperations, undoUnavailable } = buildUndoPlan(project, appliedOperations);
252
+ const unavailable = new Map(undoUnavailable.map(item => [item.path, item]));
167
253
  const expectedFiles = buildExpectedFilesAfterOperations(project, appliedOperations);
168
254
  return {
169
255
  undoOperations,
170
- undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations))
256
+ undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations)),
257
+ undoUnavailable,
258
+ appliedOperations: (appliedOperations || []).map(operation => unavailable.has(operation?.path)
259
+ ? { ...operation, undoUnavailable: unavailable.get(operation.path) } : operation)
171
260
  };
172
261
  }
173
262
 
@@ -236,6 +325,7 @@
236
325
  // file creation, backed by both the forward delete and the captured original.
237
326
  // This also recovers older Track-on records that omitted that inverse.
238
327
  function getFileUndoOperations(run = {}) {
328
+ run = withAvailableUndo(run);
239
329
  const operations = (run.undoOperations || []).filter(isFileUndoOperation);
240
330
  const before = filesListToMap(run.undoExpectedFiles);
241
331
  const after = buildExpectedFilesAfterOperations({ files: run.undoExpectedFiles }, run.appliedOperations);
@@ -246,6 +336,8 @@
246
336
  }
247
337
 
248
338
  function prepareRunUndo(run = {}) {
339
+ const original = run;
340
+ run = withAvailableUndo(run);
249
341
  const files = getFileUndoOperations(run);
250
342
  const restored = new Set(files.filter(operation => operation.type === 'create').map(operation => operation.path));
251
343
  const pending = [...files], operations = [];
@@ -255,10 +347,12 @@
255
347
  if (index >= 0) operations.push(...pending.splice(index, 1));
256
348
  } else if (!restored.has(operation.path)) operations.push(operation);
257
349
  }
258
- return { ...run, undoOperations: [...operations, ...pending] };
350
+ return { ...original, undoOperations: [...operations, ...pending],
351
+ ...(run !== original ? { undoBaseFiles: run.undoBaseFiles, undoExpectedFiles: run.undoExpectedFiles } : {}) };
259
352
  }
260
353
 
261
354
  function textUndoRun(run = {}) {
355
+ run = withAvailableUndo(run);
262
356
  const restored = new Set(getFileUndoOperations(run).flatMap(operation => operation.type === 'create' ? [operation.path]
263
357
  : operation.type === 'move' ? [operation.path, operation.to] : []));
264
358
  return { ...run,
@@ -332,6 +426,7 @@
332
426
  }
333
427
 
334
428
  function buildSnapshotRestoreUndo(run = {}) {
429
+ run = withAvailableUndo(run);
335
430
  const undoOperations = Array.isArray(run.undoOperations) ? run.undoOperations : [];
336
431
  const originalByPath = buildOriginalFilesForSnapshotUndo(run, undoOperations);
337
432
  if (!originalByPath.size) {
@@ -571,6 +666,9 @@
571
666
  }
572
667
 
573
668
  return {
669
+ getUndoUnavailableFiles,
670
+ withAvailableUndo,
671
+ compactUnavailableUndoOperations,
574
672
  isFileUndoOperation,
575
673
  getFileUndoOperations,
576
674
  prepareRunUndo,
@@ -12,6 +12,18 @@
12
12
  return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
13
13
  }
14
14
 
15
+ function verifiedRollback(status = {}) {
16
+ const transaction = status.transaction;
17
+ const pair = status.restoredPair;
18
+ const digest = value => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
19
+ return Boolean(transaction?.id && transaction.state === 'rolled_back'
20
+ && version(transaction.sourceVersion) && version(transaction.targetVersion)
21
+ && status.installedAligned === true && status.activeVersion === transaction.sourceVersion
22
+ && pair?.extensionVersion === transaction.sourceVersion && pair.nativeVersion === transaction.sourceVersion
23
+ && digest(pair.extensionSha256) && digest(pair.nativeSha256)
24
+ && (!transaction.bootstrapRevision || digest(pair.bootstrapSha256)));
25
+ }
26
+
15
27
  function inspectInstalledRuntime(status = {}, options = {}) {
16
28
  const result = { installedVersion: version(status.activeVersion), nativeVersion: version(status.runtimeVersion),
17
29
  extensionVersion: version(options.extensionVersion), runtimeVersion: version(options.runtimeVersion), state: 'unknown' };
@@ -24,9 +36,11 @@
24
36
  if (status.installedAligned !== true) return { ...result, state: 'repair_required' };
25
37
  const loaded = [result.nativeVersion, result.extensionVersion, result.runtimeVersion];
26
38
  if (loaded.every(value => value === result.installedVersion)) return { ...result, state: 'aligned' };
27
- return { ...result, state: loaded.every(value => compare(result.installedVersion, value) >= 0)
39
+ const rollbackReload = verifiedRollback(status) && loaded.every(value =>
40
+ value === status.transaction.sourceVersion || value === status.transaction.targetVersion);
41
+ return { ...result, state: rollbackReload || loaded.every(value => compare(result.installedVersion, value) >= 0)
28
42
  ? 'reload_required' : 'repair_required' };
29
43
  }
30
44
 
31
- return Object.freeze({ inspectInstalledRuntime });
45
+ return Object.freeze({ inspectInstalledRuntime, verifiedRollback });
32
46
  });