codex-overleaf-link 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) 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 +78 -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 +11 -2
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +920 -276
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsPanel.js +18 -0
  14. package/extension/src/content/generated/content.bundle.js +4496 -924
  15. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  16. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  17. package/extension/src/content/localSkillsPanel.js +2 -2
  18. package/extension/src/content/markdownDomRenderer.js +5 -3
  19. package/extension/src/content/markdownText.js +42 -5
  20. package/extension/src/content/modelPicker.js +1 -0
  21. package/extension/src/content/nativeCompatibilityController.js +29 -10
  22. package/extension/src/content/otWarmMirror.js +1 -1
  23. package/extension/src/content/pageBridgeClient.js +2 -2
  24. package/extension/src/content/panelMaintenance.js +6 -2
  25. package/extension/src/content/panelRenderer.js +51 -27
  26. package/extension/src/content/pendingInputView.js +1 -1
  27. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  28. package/extension/src/content/providerSettingsDialog.js +2 -1
  29. package/extension/src/content/recentProjects.js +4 -3
  30. package/extension/src/content/runActivitySummary.js +122 -14
  31. package/extension/src/content/runCompletionMeta.js +194 -6
  32. package/extension/src/content/runController.js +2 -1
  33. package/extension/src/content/runFailureNotice.js +8 -1
  34. package/extension/src/content/runPresence.js +18 -8
  35. package/extension/src/content/runQueueScheduler.js +9 -5
  36. package/extension/src/content/runRecoveryActions.js +2 -1
  37. package/extension/src/content/runResultActions.js +22 -20
  38. package/extension/src/content/runReviewController.js +94 -35
  39. package/extension/src/content/runTimelineView.js +224 -25
  40. package/extension/src/content/scopedPersistencePanelState.js +92 -23
  41. package/extension/src/content/sensitivePreflight.js +14 -1
  42. package/extension/src/content/sessionManager.js +49 -3
  43. package/extension/src/content/sessionMenuView.js +54 -18
  44. package/extension/src/content/sessionPersistence.js +25 -5
  45. package/extension/src/content/settingsPanel.js +15 -2
  46. package/extension/src/content/settingsWorkbench.js +127 -89
  47. package/extension/src/content/sharedSessionViewSync.js +4 -3
  48. package/extension/src/content/subagentActivityView.js +23 -7
  49. package/extension/src/content/updateNotice.js +60 -6
  50. package/extension/src/content/writebackOrchestrator.js +35 -22
  51. package/extension/src/page/compileBridge.js +193 -180
  52. package/extension/src/page/overleafCapabilities.js +4 -1
  53. package/extension/src/page/textCoordinates.js +17 -1
  54. package/extension/src/page/textFileCreator.js +9 -1
  55. package/extension/src/page/writebackRouter.js +24 -77
  56. package/extension/src/pageBridge.js +107 -66
  57. package/extension/src/popup.js +1 -1
  58. package/extension/src/shared/agentTranscript.js +350 -17
  59. package/extension/src/shared/auditRecords.js +2 -0
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/src/shared/compileAdapter.js +58 -17
  62. package/extension/src/shared/composerAttachmentDraft.js +6 -1
  63. package/extension/src/shared/contextUsage.js +125 -0
  64. package/extension/src/shared/failureReasons.js +42 -3
  65. package/extension/src/shared/i18n.js +299 -29
  66. package/extension/src/shared/lineReferences.js +38 -31
  67. package/extension/src/shared/pageRpcContract.js +1 -1
  68. package/extension/src/shared/panelIcons.js +39 -0
  69. package/extension/src/shared/reviewing.js +130 -1
  70. package/extension/src/shared/runActivityModel.js +70 -4
  71. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  72. package/extension/src/shared/sessionState.js +63 -4
  73. package/extension/src/shared/sessionTitle.js +14 -1
  74. package/extension/src/shared/sharedSessionBridge.js +81 -13
  75. package/extension/src/shared/staleGuard.js +72 -63
  76. package/extension/src/shared/storageDb.js +35 -3
  77. package/extension/src/shared/storageMigration.js +7 -1
  78. package/extension/src/shared/storageRunActions.js +62 -8
  79. package/extension/src/shared/undoOperations.js +126 -28
  80. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  81. package/extension/src/shared/writebackSettlement.js +21 -3
  82. package/extension/styles/panel.css +1106 -490
  83. package/native-host/src/bootstrapUpdate.js +52 -16
  84. package/native-host/src/chatBridgeRequest.js +60 -6
  85. package/native-host/src/chatBridgeResponse.js +10 -1
  86. package/native-host/src/chatCompletionsBridge.js +29 -2
  87. package/native-host/src/codexPromptAssembly.js +86 -118
  88. package/native-host/src/codexSessionRunner.js +126 -17
  89. package/native-host/src/providerContextError.js +20 -0
  90. package/native-host/src/taskRunnerRuntime.js +18 -6
  91. package/native-host/src/textPatch.js +473 -377
  92. package/native-host/src/updateManager.js +120 -15
  93. package/package.json +5 -8
@@ -22,7 +22,7 @@
22
22
 
23
23
  const RETRYABLE_FAILURE_CODES = new Set([
24
24
  'native_request_failed', 'codex_timeout', 'codex_no_usable_result',
25
- 'codex_output_limit', 'stale_source_changed', 'patch_anchor_not_found',
25
+ 'codex_output_limit', 'codex_context_limit', 'stale_source_changed', 'patch_anchor_not_found',
26
26
  'target_editor_not_ready', 'write_timeout', 'partial_write_needs_review',
27
27
  'write_operation_failed'
28
28
  ]);
@@ -56,6 +56,7 @@
56
56
  const retry = buildRecoveryButton(actionKey,
57
57
  tx('Edit & resend', '编辑后重发'),
58
58
  tx('Put this run\u2019s task back into the composer so you can adjust and resend it.', '把本轮任务填回输入框,修改后可直接重发。'));
59
+ retry.dataset.recoveryAction = 'edit-resend';
59
60
  retry.addEventListener('click', clickEvent => {
60
61
  clickEvent.stopPropagation();
61
62
  refillComposerForRetry(run || null);
@@ -4,13 +4,12 @@
4
4
  })(typeof window !== 'undefined' ? window : globalThis, function (RunCompletionMeta) {
5
5
  'use strict';
6
6
 
7
- const { splitFlatCompletionReport, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta } = RunCompletionMeta;
7
+ const { splitFlatCompletionReport, projectNotStartedRun, projectFinishedRun, projectCompletionConclusion, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta } = RunCompletionMeta;
8
8
 
9
9
 
10
10
  function create(deps = {}) {
11
11
  const {
12
12
  tr,
13
- getLocale,
14
13
  forkRunFromNode,
15
14
  document: documentRef = globalThis.document,
16
15
  navigator: navigatorRef = globalThis.navigator
@@ -21,11 +20,27 @@
21
20
  const reports = root.querySelectorAll('.run-completion-report');
22
21
  const report = reports[reports.length - 1];
23
22
  if (!actions || !report) return;
24
- report.append(actions);
23
+ // Share one row with the facts summary when it exists; the expandable
24
+ // details list stays below both.
25
+ const factsToggle = report.querySelector(':scope > .run-final-answer__facts > .run-final-answer__facts-sum');
26
+ if (factsToggle) factsToggle.after(actions);
27
+ else {
28
+ // Without a facts summary, the report's own Details toggle shares the row.
29
+ const details = report.querySelector(':scope > .run-outcome-details, :scope > .run-outcome-footer > .run-outcome-details');
30
+ if (details) {
31
+ let footer = details.parentElement.matches('.run-outcome-footer') ? details.parentElement : null;
32
+ if (!footer) {
33
+ footer = documentRef.createElement('div');
34
+ footer.className = 'run-outcome-footer';
35
+ details.before(footer);
36
+ footer.append(details);
37
+ }
38
+ footer.append(actions);
39
+ } else report.append(actions);
40
+ }
25
41
  actions.hidden = false;
26
42
  configureCopyButton(root);
27
43
  configureForkButton(root, run);
28
- configureCompletedTime(actions, run);
29
44
  }
30
45
 
31
46
  function configureForkButton(root, run) {
@@ -40,7 +55,7 @@
40
55
  button.disabled = !available;
41
56
  button.title = tr(available ? 'forkRunTitle' : 'forkRunUnavailable');
42
57
  button.setAttribute('aria-label', button.title);
43
- button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h3.5c3.5 0 4.2-4 7.5-4h2m-2.5-2.5L16 6l-2.5 2.5M6.5 10c3.5 0 4.2 4 7.5 4h2m-2.5-2.5L16 14l-2.5 2.5"/></svg>';
58
+ button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><path transform="translate(.5 0)" d="M3 10h3.5c3.5 0 4.2-4 7.5-4h2m-2.5-2.5L16 6l-2.5 2.5M6.5 10c3.5 0 4.2 4 7.5 4h2m-2.5-2.5L16 14l-2.5 2.5"/></svg>';
44
59
  if (!available) return;
45
60
  button.addEventListener('click', async event => {
46
61
  event.stopPropagation();
@@ -55,19 +70,6 @@
55
70
  });
56
71
  }
57
72
 
58
- function configureCompletedTime(actions, run) {
59
- const target = actions.querySelector('[data-run-completed-time]');
60
- const finishedAt = String(run.finishedAt || '').trim();
61
- const date = finishedAt ? new Date(finishedAt) : null;
62
- if (!target || !date || !Number.isFinite(date.getTime())) return;
63
- target.hidden = false;
64
- target.textContent = new Intl.DateTimeFormat(getLocale(), {
65
- hour: '2-digit',
66
- minute: '2-digit'
67
- }).format(date);
68
- target.title = date.toLocaleString(getLocale());
69
- }
70
-
71
73
  function configureCopyButton(root) {
72
74
  const existing = root.querySelector('[data-run-copy]');
73
75
  const button = existing.cloneNode(true);
@@ -78,7 +80,7 @@
78
80
  button.disabled = !copyText;
79
81
  button.title = tr('copyConclusionTitle');
80
82
  button.setAttribute('aria-label', button.title);
81
- button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><rect x="6.5" y="6.5" width="9" height="9" rx="2"/><path d="M13.5 6.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6.5a2 2 0 0 0 2 2h1.5"/></svg>';
83
+ button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><g transform="translate(.75 .75)"><rect x="6.5" y="6.5" width="9" height="9" rx="2"/><path d="M13.5 6.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6.5a2 2 0 0 0 2 2h1.5"/></g></svg>';
82
84
  if (!copyText) return;
83
85
  button.addEventListener('click', async event => {
84
86
  event.stopPropagation();
@@ -109,7 +111,7 @@
109
111
  if (!copied) throw new Error('clipboard_copy_failed');
110
112
  }
111
113
 
112
- return Object.freeze({ configureResultActions, projectUndoAvailability, splitFlatCompletionReport, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta });
114
+ return Object.freeze({ configureResultActions, projectUndoAvailability, splitFlatCompletionReport, projectNotStartedRun, projectFinishedRun, projectCompletionConclusion, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta });
113
115
  }
114
116
 
115
117
 
@@ -58,9 +58,12 @@
58
58
  if (Modules.StorageDb?.sharedSessionsEnabled?.() !== true) return;
59
59
  findRunRecord(runId).undoOwnerProtocol = 1;
60
60
  try { await flushQueuedSaveState({ requireDurable: true, reviewRunIds: [runId] }); }
61
- catch (cause) { throw Object.assign(new Error('The Undo owner marker could not be durably saved.'), { code: 'review_state_not_persisted', cause }); }
61
+ catch (cause) {
62
+ if (cause.code === 'shared_session_conflict' && cause.durableReviewRunIds?.includes(runId)) notify?.(cause.message);
63
+ else throw Object.assign(new Error('The Undo owner marker could not be durably saved.'), { code: 'review_state_not_persisted', cause });
64
+ }
62
65
  reviewOwners.get(runId)?.();
63
- const saved = await Modules.StorageDb.getRecord('sessions', getState().activeSessionId);
66
+ const saved = await Modules.StorageDb.getRecord('sessions', getState().activeSessionId, { adopt: false });
64
67
  if (saved?.runs?.find(run => run.id === runId)?.undoOwnerProtocol !== 1)
65
68
  throw Object.assign(new Error('The Undo owner marker was not durably retained.'), { code: 'review_state_not_persisted' });
66
69
  reviewOwners.get(runId)?.();
@@ -68,7 +71,7 @@
68
71
  async function withSharedReviewOwnership(runId, work) {
69
72
  const db = Modules.StorageDb;
70
73
  if (db?.sharedSessionsEnabled?.() !== true) return work();
71
- const run = findRunRecord(runId);
74
+ let run = findRunRecord(runId);
72
75
  if (!run || trackedChangeInFlight.has(runId)) return;
73
76
  const assertCurrent = undoOwner(run);
74
77
  const sessionId = getState().activeSessionId;
@@ -77,8 +80,9 @@
77
80
  reviewOwners.set(runId, assertOwner);
78
81
  try {
79
82
  assertCurrent();
80
- const session = await db.getRecord('sessions', sessionId);
81
- assertCurrent();
83
+ // Only the run is refreshed here; keep the visible session's merge baseline.
84
+ const session = await db.getRecord('sessions', sessionId, { adopt: false });
85
+ run = assertCurrent();
82
86
  const latest = session?.runs?.find(record => record.id === runId);
83
87
  if (!latest || session.projectId !== getRunProjectIdForWriteback(run) || session.accountScopeId !== getAccountScopeId())
84
88
  throw Object.assign(new Error('The shared Undo run is no longer available in its original scope.'), { code: 'review_context_changed' });
@@ -162,9 +166,10 @@
162
166
  trackedChangeInFlight.set(runId, 'reject'); refreshRunCardControls(runId);
163
167
  try { return await work(); }
164
168
  catch (error) {
165
- let current = true; try { assertOwner(); } catch (_stale) { current = false; }
166
- if (current && findRunRecord(runId) === run) {
167
- if (run.undoStatus === 'running') setRunUndoStatus(runId, 'partial');
169
+ let current; try { current = assertOwner(); } catch (_stale) {}
170
+ const durableReviewConflict = error.code === 'shared_session_conflict' && error.durableReviewRunIds?.includes(runId);
171
+ if (current && error.code !== 'review_context_changed' && !durableReviewConflict) {
172
+ if (current.undoStatus === 'running') setRunUndoStatus(runId, 'partial');
168
173
  appendRunRecordEvent(runId, { title: tr('undoNoTraceTitle'), status: 'failed',
169
174
  detail: { [tr('detailReason')]: error.message } });
170
175
  }
@@ -174,18 +179,59 @@
174
179
  } finally { trackedChangeInFlight.delete(runId); refreshRunCardControls(runId); }
175
180
  }
176
181
  const withUndoLock = (runId, work) => withSharedReviewOwnership(runId, () => withLocalUndoLock(runId, work));
177
- const undoRun = runId => withUndoLock(runId, () => executeUndo(runId));
182
+ // Undo restores the source; with Auto compile on, refresh the PDF the way a write does.
183
+ async function recompileAfterUndo(runId, work) {
184
+ const before = findRunRecord(runId);
185
+ const prior = `${before?.undoStatus || ''}|${before?.trackedChangeStatus || ''}`;
186
+ const result = await work();
187
+ const run = findRunRecord(runId);
188
+ const undone = ['applied', 'partial'].includes(run?.undoStatus) || run?.trackedChangeStatus === 'rejected';
189
+ if (undone && `${run.undoStatus || ''}|${run.trackedChangeStatus || ''}` !== prior
190
+ && getState()?.autoRecompile !== false && run.runProjectId && run.runProjectId === getCurrentProjectId?.()) {
191
+ Promise.resolve(callPageBridge('triggerCompile', { preferUiClick: true, waitForSaveMs: 5000,
192
+ runProjectId: run.runProjectId })).catch(() => {});
193
+ }
194
+ return result;
195
+ }
196
+ // Inside the lock: a stale page that adopts another page's finished Undo never reaches its work.
197
+ const undoRun = runId => withUndoLock(runId, () => recompileAfterUndo(runId, () => executeUndo(runId)));
178
198
  const acceptRun = runId => withSharedReviewOwnership(runId, () => executeAcceptRun(runId));
179
- const undoRunTrackedChanges = (runId, run) => withUndoLock(runId, () => executeTrackedUndo(runId, run));
199
+ const undoRunTrackedChanges = (runId, run) => withUndoLock(runId,
200
+ () => recompileAfterUndo(runId, () => executeTrackedUndo(runId, run)));
180
201
 
181
202
  function undoOwner(run) {
182
- const captured = getState(), sessionId = captured.activeSessionId;
203
+ const sessionId = getState().activeSessionId;
183
204
  const scope = [getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()];
205
+ const provenance = value => JSON.stringify([value?.id, value?.runProjectId || '', value?.startedAt || '',
206
+ value?.runAccountScopeId || scope[1] || '', value?.nativeEventSeq || 0, value?.codexThreadId || '', value?.codexTurnId || '', value?.executionSnapshot || null]);
207
+ const sessionScope = () => {
208
+ const session = getState().sessions?.find(item => item.id === sessionId);
209
+ return JSON.stringify([session?.projectId || '', session?.accountScopeId || '']);
210
+ };
211
+ const source = provenance(run), owningSession = sessionScope();
184
212
  return () => {
185
213
  reviewOwners.get(run.id)?.();
186
- if (getState() !== captured || captured.activeSessionId !== sessionId || findRunRecord(run.id) !== run
214
+ const live = findRunRecord(run.id);
215
+ if (getState().activeSessionId !== sessionId || sessionScope() !== owningSession || !live
216
+ || !(live === run || Modules.SessionState?.sameRunIdentity?.(run, live)) || provenance(live) !== source
187
217
  || JSON.stringify(scope) !== JSON.stringify([getCurrentProjectId?.(), getAccountScopeId?.(), getRouteGeneration?.()]))
188
218
  throw Object.assign(new Error('The Undo project, account or session changed.'), { code: 'review_context_changed' });
219
+ return live;
220
+ };
221
+ }
222
+
223
+ function checkpointGuard(run, assertOwner) {
224
+ const snapshot = value => {
225
+ const normalized = Modules.SessionState?.normalizeRuns?.([value])?.[0] || value;
226
+ return JSON.stringify(['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoExpectedFiles',
227
+ 'undoTrackedChanges', 'trackedChangeCaptures', 'undoStatus', 'trackedChangeStatus', 'forkSnapshot']
228
+ .map(key => normalized[key]));
229
+ };
230
+ const expected = snapshot(run);
231
+ return () => {
232
+ const live = assertOwner();
233
+ if (snapshot(live) !== expected) throw Object.assign(new Error('The Undo project, account or session changed.'), { code: 'review_context_changed' });
234
+ return live;
189
235
  };
190
236
  }
191
237
 
@@ -198,9 +244,10 @@ function undoOwner(run) {
198
244
  }
199
245
 
200
246
  async function executeUndo(runId) {
201
- const run = findRunRecord(runId);
247
+ let run = findRunRecord(runId);
202
248
  if (run) Object.assign(run, Modules.UndoOperations.prepareRunUndo(run));
203
249
  const assertCurrent = undoOwner(run);
250
+ let assertInputs = checkpointGuard(run, assertCurrent);
204
251
  if (!getRunUndoCount(run) || run.undoStatus === 'applied') {
205
252
  return;
206
253
  }
@@ -210,7 +257,7 @@ async function executeUndo(runId) {
210
257
  const pendingMirror = getPendingMirrorRefresh?.();
211
258
  if (pendingMirror) {
212
259
  await pendingMirror.catch(() => {});
213
- assertCurrent();
260
+ run = assertInputs();
214
261
  }
215
262
 
216
263
  if ((Array.isArray(run.undoTrackedChanges) && run.undoTrackedChanges.length) || hasTrackedEditorUndo(run)
@@ -272,8 +319,9 @@ async function executeUndo(runId) {
272
319
  }
273
320
  }
274
321
 
275
- assertCurrent();
322
+ run = assertInputs();
276
323
  const deadlineAt = await prepareUndoDispatch(runId);
324
+ run = assertInputs();
277
325
  setRunUndoStatus(runId, 'running');
278
326
  appendRunRecordEvent(runId, {
279
327
  title: tr('undoNoTraceStarted'),
@@ -281,7 +329,8 @@ async function executeUndo(runId) {
281
329
  detail: { [tr('detailWillUndo')]: formatOperationFiles(selectedOperations) }
282
330
  });
283
331
 
284
- await persistUndoOwnerMarker(runId); assertCurrent();
332
+ assertInputs = checkpointGuard(run, assertCurrent);
333
+ await persistUndoOwnerMarker(runId); run = assertInputs();
285
334
  const result = await callPageBridge('applyOperations', {
286
335
  operations: selectedOperations, deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
287
336
  baseFiles: run.undoBaseFiles || [],
@@ -289,7 +338,7 @@ async function executeUndo(runId) {
289
338
  // Undo remains bound to its original project, including after navigation.
290
339
  runProjectId: getRunProjectIdForWriteback(run)
291
340
  });
292
- assertCurrent();
341
+ run = assertInputs();
293
342
  const undoApplied = isUndoResultEffectivelyApplied(run, result);
294
343
  writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
295
344
  appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
@@ -321,6 +370,7 @@ async function executeUndo(runId) {
321
370
 
322
371
  async function executeTrackedUndo(runId, run) {
323
372
  const assertCurrent = undoOwner(run);
373
+ let assertInputs = checkpointGuard(run, assertCurrent);
324
374
  const fileUndoOperations = Modules.UndoOperations.getFileUndoOperations(run);
325
375
  const textRun = Modules.UndoOperations.textUndoRun(run);
326
376
  const trackedUndo = textRun.undoTrackedChanges.length > 0;
@@ -343,8 +393,9 @@ async function executeTrackedUndo(runId, run) {
343
393
  return;
344
394
  }
345
395
 
346
- assertCurrent();
396
+ run = assertInputs();
347
397
  const deadlineAt = await prepareUndoDispatch(runId);
398
+ run = assertInputs();
348
399
  // Keep the review lifecycle until every pending part has settled.
349
400
  // Its local lock stays active while verified recovery progress is persisted.
350
401
  const lifecycleReject = (trackedUndo || fileUndoOperations.length > 0) && isTrackedChangeLifecycleRun(run);
@@ -363,7 +414,8 @@ async function executeTrackedUndo(runId, run) {
363
414
 
364
415
  let result;
365
416
  try {
366
- await persistUndoOwnerMarker(runId); assertCurrent();
417
+ assertInputs = checkpointGuard(run, assertCurrent);
418
+ await persistUndoOwnerMarker(runId); run = assertInputs();
367
419
  const trackedUndoPostFilesAtDispatch = buildTrackedUndoPostFiles(textRun);
368
420
  result = hasTextUndo ? await callPageBridge('rejectTrackedChanges', {
369
421
  trackedChanges: textRun.undoTrackedChanges || [], deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
@@ -375,12 +427,13 @@ async function executeTrackedUndo(runId, run) {
375
427
  // The page-side project guard still refuses writes after navigation.
376
428
  runProjectId: getRunProjectIdForWriteback(run)
377
429
  }) : { ok: true, applied: [], skipped: [] };
378
- assertCurrent();
430
+ run = assertInputs();
379
431
  if (hasTextUndo) WritebackSettlement.attachUndoNotVerifiedFailure(textRun, result, {
380
432
  buildFailure: buildContentFailure, postFiles: trackedUndoPostFilesAtDispatch });
381
433
  if (fileUndoOperations.length && lifecycleReject && hasTextUndo) {
382
434
  WritebackSettlement.attachUndoNotVerifiedFailure(textRun, result, { buildFailure: buildContentFailure, postFiles: trackedUndoPostFilesAtDispatch });
383
435
  await applyTrackedChangeSettlement(runId, 'reject', { ...result, ok: false }, { assertCurrent });
436
+ run = assertCurrent(); assertInputs = checkpointGuard(run, assertCurrent);
384
437
  }
385
438
  if (fileUndoOperations.length) {
386
439
  let createdResult;
@@ -399,7 +452,7 @@ async function executeTrackedUndo(runId, run) {
399
452
  operation, result: { ok: false, code: 'undo_operation_failed', reason: error.message }
400
453
  })) };
401
454
  }
402
- assertCurrent();
455
+ run = assertInputs();
403
456
  result = Modules.UndoOperations.mergeFileUndoResult(result, createdResult, fileUndoOperations);
404
457
  }
405
458
  appendRunRecordEvent(runId, {
@@ -436,23 +489,24 @@ async function executeTrackedUndo(runId, run) {
436
489
  }
437
490
 
438
491
  function reviewOwner(run) {
439
- const owner = { run, state: getState(), sessionId: getState().activeSessionId,
440
- projectId: getCurrentProjectId(), accountScopeId: getAccountScopeId(), generation: getRouteGeneration() };
492
+ const projectId = getCurrentProjectId(), accountScopeId = getAccountScopeId();
493
+ const assertRun = undoOwner(run);
441
494
  const assert = () => {
442
- reviewOwners.get(run.id)?.();
443
- if (!owner.projectId || !owner.accountScopeId || run.runProjectId !== owner.projectId
444
- || getCurrentProjectId() !== owner.projectId || getAccountScopeId() !== owner.accountScopeId
445
- || getRouteGeneration() !== owner.generation || getState() !== owner.state
446
- || getState().activeSessionId !== owner.sessionId || findRunRecord(run.id) !== run
447
- || (run.runAccountScopeId && run.runAccountScopeId !== owner.accountScopeId))
495
+ let live;
496
+ try { live = assertRun(); } catch (error) {
497
+ if (error.code === 'review_context_changed') error.message = 'The original review project, account or session changed.';
498
+ throw error;
499
+ }
500
+ if (!projectId || !accountScopeId || live.runProjectId !== projectId
501
+ || (live.runAccountScopeId && live.runAccountScopeId !== accountScopeId))
448
502
  throw Object.assign(new Error('The original review project, account or session changed.'), { code: 'review_context_changed' });
449
- return run;
503
+ return live;
450
504
  };
451
- assert(); return { ...owner, assert };
505
+ assert(); return { projectId, accountScopeId, assert };
452
506
  }
453
507
 
454
508
  async function executeAcceptRun(runId) {
455
- const run = findRunRecord(runId);
509
+ let run = findRunRecord(runId);
456
510
  if (!run || !isTrackedChangeLifecycleRun(run)) {
457
511
  return;
458
512
  }
@@ -471,6 +525,7 @@ async function executeAcceptRun(runId) {
471
525
  }
472
526
 
473
527
  const owner = reviewOwner(run);
528
+ const assertInputs = checkpointGuard(run, owner.assert);
474
529
  trackedChangeInFlight.set(runId, 'accept');
475
530
  refreshRunCardControls(runId);
476
531
  appendRunRecordEvent(runId, {
@@ -481,6 +536,7 @@ async function executeAcceptRun(runId) {
481
536
 
482
537
  try {
483
538
  const deadlineAt = await prepareUndoDispatch(runId);
539
+ run = assertInputs();
484
540
  const result = await callPageBridge('acceptTrackedChanges', { deadlineAt, reviewOwnerToken: reviewOwners.get(runId)?.token,
485
541
  trackedChanges: run.undoTrackedChanges || [],
486
542
  expectedFiles: run.undoExpectedFiles || [],
@@ -488,7 +544,7 @@ async function executeAcceptRun(runId) {
488
544
  appliedOperations: Array.isArray(run.appliedOperations) ? run.appliedOperations : [],
489
545
  runProjectId: owner.projectId, accountScopeId: owner.accountScopeId
490
546
  });
491
- owner.assert();
547
+ run = assertInputs();
492
548
  appendAcceptDiagnosticEvents(runId, Array.isArray(result.diagnostics) ? result.diagnostics : []);
493
549
  appendRunRecordEvent(runId, {
494
550
  title: tr('runAcceptTrackedResult', { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
@@ -565,10 +621,13 @@ async function applyLegacyUndoSettlement(runId, undoStatus, result) {
565
621
  })
566
622
  );
567
623
  await Modules.ScopedPersistenceCoordinator.applyReviewTransition({ getState, runId, transition,
568
- legacyUndo: true, persist: flushQueuedSaveState, assertCurrent });
624
+ legacyUndo: true, persist: flushQueuedSaveState, assertCurrent, SessionState: Modules.SessionState });
569
625
  refreshRunCardControls(runId);
570
626
  }
571
- return { recoverInterruptedUndoStatuses, undoRun, undoRunTrackedChanges, acceptRun, applyLegacyUndoSettlement };
627
+ // From the shared review lock until settlement, a replaced view would
628
+ // invalidate the action's ownership checks after Page effects.
629
+ const hasActiveReview = () => reviewOwners.size > 0;
630
+ return { recoverInterruptedUndoStatuses, undoRun, undoRunTrackedChanges, acceptRun, applyLegacyUndoSettlement, hasActiveReview };
572
631
  }
573
632
  return { create };
574
633
  });