codex-overleaf-link 2.5.0 → 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 (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  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 +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  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 +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -7,12 +7,23 @@
7
7
  })(typeof window !== 'undefined' ? window : globalThis, function activeTurnControlFactory() {
8
8
  'use strict';
9
9
 
10
+ // Startup has an owner before a Native request exists. This identity is only
11
+ // for ownership probes; nativeRequestId remains reserved for the actual RPC.
12
+ function getStartupRequestId(sessionId, runId) {
13
+ return `startup:${encodeURIComponent(sessionId)}:${encodeURIComponent(runId)}`;
14
+ }
15
+
10
16
  function create(options = {}) {
11
17
  const runtime = options.chrome?.runtime;
12
18
  let port = null;
13
19
  let binding = null;
14
20
  let reconnectTimer = null;
15
21
  let destroyed = false;
22
+ let bindingWaiter = null;
23
+ let bindingSequence = 0;
24
+ let recoveryTimer = null;
25
+ let recoveryGeneration = 0;
26
+ const inventoryRetries = new Map();
16
27
 
17
28
  function connect() {
18
29
  if (destroyed || port || typeof runtime?.connect !== 'function') {
@@ -21,6 +32,11 @@
21
32
  try {
22
33
  const nextPort = runtime.connect({ name: 'codex-overleaf/run-owner' });
23
34
  port = nextPort;
35
+ nextPort.onMessage?.addListener?.(message => {
36
+ if (port !== nextPort || message?.type !== 'bound' || !bindingWaiter || !binding) return;
37
+ if (['requestId', 'projectKey', 'clientRunId', 'sessionId'].every(key => message[key] === binding[key])
38
+ && message.browserOnly === true && message.ownerToken === binding.ownerToken) bindingWaiter(message.ok !== false);
39
+ });
24
40
  nextPort.onDisconnect?.addListener?.(() => {
25
41
  if (port === nextPort) {
26
42
  port = null;
@@ -46,20 +62,39 @@
46
62
  }
47
63
 
48
64
  function bind(value = {}) {
65
+ bindingWaiter?.(false);
49
66
  binding = {
50
67
  requestId: String(value.requestId || ''),
51
68
  projectKey: String(value.projectKey || ''),
52
69
  clientRunId: String(value.clientRunId || ''),
53
- sessionId: String(value.sessionId || '')
70
+ sessionId: String(value.sessionId || ''),
71
+ ...(value.browserOnly === true ? { browserOnly: true, ownerToken: String(value.ownerToken || `browser-${++bindingSequence}`) } : {})
54
72
  };
55
73
  connect();
56
74
  postBinding('bind', binding);
57
75
  }
58
76
 
59
- function release(requestId) {
60
- if (!binding || (requestId && binding.requestId !== requestId)) {
77
+ async function bindBrowserRun(value) {
78
+ bind({ ...value, browserOnly: true });
79
+ if (destroyed || !port) return false;
80
+ return new Promise(resolve => {
81
+ const timeout = setTimeout(() => complete(false), 5000);
82
+ const complete = ok => {
83
+ clearTimeout(timeout);
84
+ if (bindingWaiter === complete) bindingWaiter = null;
85
+ resolve(ok);
86
+ };
87
+ bindingWaiter = complete;
88
+ postBinding('bind', binding);
89
+ });
90
+ }
91
+
92
+ function release(requestId, ownerToken) {
93
+ if (!binding || (requestId && binding.requestId !== requestId)
94
+ || (ownerToken && ownerToken !== binding.ownerToken)) {
61
95
  return;
62
96
  }
97
+ bindingWaiter?.(false);
63
98
  postBinding('release', binding);
64
99
  binding = null;
65
100
  }
@@ -86,14 +121,97 @@
86
121
  return response?.ok === true;
87
122
  }
88
123
 
124
+ async function knownJournals(requestIds, owners = [], onRetry) {
125
+ if (typeof runtime?.sendMessage !== 'function') return null;
126
+ const known = new Set();
127
+ for (let offset = 0; offset < requestIds.length; offset += 64) {
128
+ const ids = requestIds.slice(offset, offset + 64);
129
+ const response = await runtime.sendMessage({ type: 'codex-overleaf/run-journal/known',
130
+ requestIds: ids, owners: owners.slice(offset, offset + 64) });
131
+ // An older background cannot rule out browser-only owners.
132
+ const needsStartupInventory = owners.slice(offset, offset + 64).some(owner => owner.preDispatch === true);
133
+ if (!response?.ok || !Array.isArray(response.known) || response.ownersChecked !== true
134
+ || (needsStartupInventory && response.preDispatchChecked !== true)) {
135
+ if (Number.isFinite(response?.retryAfterMs) && response.retryAfterMs > 0) onRetry?.(response.retryAfterMs);
136
+ return null;
137
+ }
138
+ for (const id of response.known) if (ids.includes(id)) known.add(id);
139
+ }
140
+ return known;
141
+ }
142
+
143
+ // Native journals and live browser-only owners are separate evidence. A
144
+ // missing journal alone cannot establish that another page stopped retrying.
145
+ async function recoverOrphanedRuns(projectKey, options, result, isCurrent) {
146
+ const now = Date.now(), candidates = [];
147
+ let recheckAfterMs = Infinity;
148
+ const sessions = () => options.listSessions?.() || [];
149
+ const requestIdFor = (session, record) => record.nativeRequestId || getStartupRequestId(session.id, record.id);
150
+ const eligible = (session, record) => record.status === 'running' && session.id && record.id
151
+ && !options.isLiveRun?.(record, session)
152
+ && (!session.projectId || session.projectId === projectKey)
153
+ && (!options.accountScopeId || !session.accountScopeId || session.accountScopeId === options.accountScopeId)
154
+ && (!record.runProjectId || record.runProjectId === projectKey);
155
+ for (const session of sessions()) {
156
+ for (const record of session.runs || []) {
157
+ if (!eligible(session, record)) continue;
158
+ const started = Date.parse(record.startedAt);
159
+ if (!Number.isFinite(started)) continue;
160
+ const remaining = started + (options.orphanGraceMs ?? 60000) - now;
161
+ if (remaining > 0) { recheckAfterMs = Math.min(recheckAfterMs, remaining); continue; }
162
+ candidates.push({ session, record, requestId: requestIdFor(session, record) });
163
+ }
164
+ }
165
+ result.recheckAfterMs = Number.isFinite(recheckAfterMs) ? recheckAfterMs : null;
166
+ if (!candidates.length) return;
167
+ const known = await knownJournals(candidates.map(item => item.requestId), candidates.map(({ session, record, requestId }) => ({
168
+ requestId, projectKey, clientRunId: record.id, sessionId: session.id,
169
+ ...(!record.nativeRequestId ? { preDispatch: true } : {})
170
+ })), delay => {
171
+ const key = `${options.accountScopeId || ''}:${projectKey}`;
172
+ const attempts = inventoryRetries.get(key) || 0;
173
+ if (attempts >= 3) return;
174
+ inventoryRetries.set(key, attempts + 1);
175
+ result.recheckAfterMs = Math.min(result.recheckAfterMs ?? Infinity, delay);
176
+ }).catch(() => null);
177
+ if (!known || !isCurrent()) return;
178
+ inventoryRetries.delete(`${options.accountScopeId || ''}:${projectKey}`);
179
+ for (const { session, record, requestId } of candidates) {
180
+ const currentSession = sessions().find(value => value.id === session.id);
181
+ // State normalization can replace run objects while inventory is pending.
182
+ if (!currentSession?.runs?.includes(record) || !eligible(currentSession, record)
183
+ || requestIdFor(currentSession, record) !== requestId || known.has(requestId)) continue;
184
+ if (!record.runProjectId) record.runProjectId = projectKey;
185
+ options.normalizeInterruptedRun?.(record);
186
+ record.status = 'interrupted';
187
+ record.interruptedDraft = { requestId, threadId: currentSession.codexThreadId || '',
188
+ turnId: record.codexTurnId || '', lastEventSeq: record.nativeEventSeq || 0, reason: 'extension_reloaded' };
189
+ options.pauseSessionQueue?.(currentSession, 'page_reload');
190
+ result.changed = true;
191
+ result.recoveredRuns.push({ sessionId: currentSession.id, runId: record.id,
192
+ projectId: projectKey, accountScopeId: options.accountScopeId });
193
+ }
194
+ }
195
+
196
+ function cancelRecovery() {
197
+ recoveryGeneration++;
198
+ clearTimeout(recoveryTimer);
199
+ recoveryTimer = null;
200
+ }
201
+
89
202
  async function recoverJournals(options = {}) {
203
+ cancelRecovery();
204
+ const generation = recoveryGeneration;
205
+ const isCurrent = () => !destroyed && generation === recoveryGeneration && options.isCurrent?.() !== false;
90
206
  const projectKey = String(options.projectKey || '');
91
207
  if (!projectKey) {
92
208
  return { changed: false, acknowledgeIds: [] };
93
209
  }
94
210
  const journals = await listJournals(projectKey).catch(() => []);
211
+ if (!isCurrent()) return { changed: false, acknowledgeIds: [], recoveredRuns: [] };
95
212
  let changed = false;
96
213
  const acknowledgeIds = [];
214
+ const recoveredRuns = [];
97
215
  for (const journal of journals) {
98
216
  if (!journal?.terminal && !journal?.ownerLost) {
99
217
  continue;
@@ -104,7 +222,8 @@
104
222
  continue;
105
223
  }
106
224
  const session = options.findSession?.(journal.sessionId);
107
- if (!session) {
225
+ if (!session || (session.projectId && session.projectId !== projectKey)
226
+ || (options.accountScopeId && session.accountScopeId && session.accountScopeId !== options.accountScopeId)) {
108
227
  continue;
109
228
  }
110
229
  let record = (session.runs || []).find(run => run.id === journal.clientRunId);
@@ -114,9 +233,19 @@
114
233
  session.runs = [...(session.runs || []), record].slice(-20);
115
234
  }
116
235
  }
117
- if (!record) {
236
+ if (!record || options.isLiveRun?.(record, session)) {
118
237
  continue;
119
238
  }
239
+ if (record.runProjectId && record.runProjectId !== projectKey) continue;
240
+ if (!record.runProjectId) record.runProjectId = projectKey;
241
+ if (journal.operation === 'compact') {
242
+ record.operation = 'compact';
243
+ if (!record.codexThreadId) record.codexThreadId = String(journal.threadId || '');
244
+ if (session.contextUsage?.threadId === record.codexThreadId
245
+ && Date.parse(session.contextUsage.observedAt || '') < Date.parse(record.startedAt || '')) {
246
+ session.contextUsage = null;
247
+ }
248
+ }
120
249
  if (record.status === 'running') {
121
250
  options.normalizeInterruptedRun?.(record);
122
251
  }
@@ -136,6 +265,9 @@
136
265
  record.nativeEventSeq = sequence;
137
266
  continue;
138
267
  }
268
+ // Non-display state (for example context usage) must also recover
269
+ // from the journal, after the owning thread/turn has been bound.
270
+ options.onRawEvent?.(raw, { record, session, sequence, journal });
139
271
  const recovered = options.mapEvent?.(raw, journal);
140
272
  if (recovered) {
141
273
  if (recovered.kind === 'stream') {
@@ -163,12 +295,24 @@
163
295
  options.pauseSessionQueue?.(session, 'page_reload');
164
296
  changed = true;
165
297
  acknowledgeIds.push(journal.requestId);
298
+ recoveredRuns.push({ sessionId: session.id, runId: record.id,
299
+ projectId: projectKey, accountScopeId: options.accountScopeId });
300
+ }
301
+ const result = { changed, acknowledgeIds, recoveredRuns };
302
+ await recoverOrphanedRuns(projectKey, options, result, isCurrent);
303
+ if (isCurrent() && result.recheckAfterMs !== null && typeof options.onRecheck === 'function') {
304
+ recoveryTimer = setTimeout(() => {
305
+ recoveryTimer = null;
306
+ if (isCurrent()) Promise.resolve(options.onRecheck()).catch(() => {});
307
+ }, Math.min(result.recheckAfterMs, 2147483647));
166
308
  }
167
- return { changed, acknowledgeIds };
309
+ return result;
168
310
  }
169
311
 
170
312
  function destroy() {
171
313
  destroyed = true;
314
+ cancelRecovery();
315
+ bindingWaiter?.(false);
172
316
  clearTimeout(reconnectTimer);
173
317
  reconnectTimer = null;
174
318
  try {
@@ -181,8 +325,27 @@
181
325
  }
182
326
 
183
327
  connect();
184
- return { acknowledge, bind, destroy, listJournals, recoverJournals, release };
328
+ return { acknowledge, bind, bindBrowserRun, cancelRecovery, destroy, getOwner: () => binding ? { ...binding } : null,
329
+ knownJournals, listJournals, recoverJournals, release };
330
+ }
331
+
332
+ function createInterruptedRun(journal, projectKey, statusText) {
333
+ return { id: journal.clientRunId, task: journal.task || 'interrupted task', mode: journal.mode || 'ask',
334
+ ...(journal.operation === 'compact' ? { operation: 'compact', codexThreadId: String(journal.threadId || '') } : {}),
335
+ model: journal.model || '', status: 'interrupted', statusText, runProjectId: projectKey,
336
+ startedAt: journal.createdAt || new Date().toISOString(), finishedAt: journal.updatedAt || new Date().toISOString(),
337
+ events: [], attachments: [], undoOperations: [], undoTrackedChanges: [], undoExpectedFiles: [] };
338
+ }
339
+
340
+ function mapRecoveredEvent(raw, journal, options) {
341
+ const activity = options.mapActivity(raw);
342
+ if (!activity?.visible || activity.kind === 'technical') return null;
343
+ return { kind: activity.kind || 'activity', title: options.sanitizeText(activity.title) || 'Event',
344
+ status: activity.status || 'running', detail: options.sanitizeValue(activity.detail),
345
+ timestamp: raw.timestamp || journal.updatedAt || new Date().toISOString(),
346
+ streamKey: options.sanitizeText(activity.streamKey), streamRole: options.sanitizeText(activity.streamRole),
347
+ appendText: options.sanitizeText(activity.appendText), replaceText: activity.replaceText };
185
348
  }
186
349
 
187
- return { create };
350
+ return { create, createInterruptedRun, mapRecoveredEvent, getStartupRequestId };
188
351
  });
@@ -85,7 +85,8 @@
85
85
  */
86
86
  function normalizeSkippedFailure(item) {
87
87
  if (!FailureReasons || !FailureReasons.normalizeFailureReason) return null;
88
- const failure = FailureReasons.normalizeFailureReason(item?.result, item?.operation || {}, { locale: getLocale() });
88
+ const normalize = FailureReasons.normalizeWritebackFailure || FailureReasons.normalizeFailureReason;
89
+ const failure = normalize(item?.result, item?.operation || {}, { locale: getLocale() });
89
90
  if (!failure || !failure.code) return null;
90
91
  const localized = FailureReasons.localizeFailureReason
91
92
  ? FailureReasons.localizeFailureReason(failure, getLocale(), failureReasonI18nLookup)
@@ -98,7 +99,7 @@
98
99
 
99
100
  function pickPrimarySkippedFailure(failures) {
100
101
  if (!FailureReasons || !FailureReasons.selectPrimaryFailure) return null;
101
- return FailureReasons.selectPrimaryFailure(failures);
102
+ return (FailureReasons.selectPrimaryWritebackFailure || FailureReasons.selectPrimaryFailure)(failures);
102
103
  }
103
104
 
104
105
  function failureReasonI18nLookup(key) {
@@ -134,6 +135,8 @@
134
135
  const code = result.code || '';
135
136
  const filePath = result.reasonParams?.filePath || operation.path || operation.from || operation.to || '';
136
137
  const withDebug = reason => appendApplyResultDebug(reason, result);
138
+ const catalogReason = formatCatalogFailureReason(result, filePath);
139
+ if (catalogReason) return withDebug(catalogReason);
137
140
  if (key === 'missingBaseFile' || code === 'missing_base_file') {
138
141
  const target = filePath || tx('this file', '这个文件');
139
142
  return withDebug(tx(
@@ -236,82 +239,52 @@
236
239
  return parts.join(', ');
237
240
  }
238
241
 
242
+ function formatCatalogFailureReason(result, path) {
243
+ if (!FailureReasons?.formatFailureReasonForDisplay) return '';
244
+ // Compact historical settlements can omit canonical validation fields.
245
+ // Their display is still recoverable without re-normalizing stored facts.
246
+ const failure = result.failure && typeof result.failure === 'object'
247
+ ? { ...result.failure, file: result.failure.file || result.file || path }
248
+ : FailureReasons.normalizeFailureReason(result, { path: result.file || path });
249
+ const display = FailureReasons.formatFailureReasonForDisplay(failure, getLocale(), failureReasonI18nLookup);
250
+ return display.localized ? display.message : '';
251
+ }
252
+
239
253
  function formatBridgeResultReason(result = {}, fallbackPath = '') {
240
- const code = result.code || '';
241
- const path = result.path || fallbackPath || '';
242
- if (getLocale() !== 'en') {
243
- return localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
244
- }
245
- if (code === 'missing_tracked_changes') {
246
- return 'No matching Overleaf tracked-change records were found for this run, so Codex did not undo with text patches.';
247
- }
248
- if (code === 'tracked_change_file_open_failed') {
249
- return path
250
- ? `Could not open ${path} to find this run's tracked changes. Handle them manually in the Overleaf review tools.`
251
- : 'Could not open the file to find this run\'s tracked changes. Handle them manually in the Overleaf review tools.';
252
- }
253
- if (code === 'tracked_change_not_found') {
254
- return 'No matching tracked changes were found on the Overleaf page for this run.';
255
- }
256
- if (code === 'tracked_change_reject_control_not_found') {
257
- return path
258
- ? `Some tracked changes in ${path} remain, but the matching Reject button was not found. Reject them manually in Overleaf.`
259
- : 'Some tracked changes remain, but the matching Reject button was not found. Reject them manually in Overleaf.';
260
- }
261
- if (code === 'tracked_change_reject_not_confirmed') {
262
- return 'Codex clicked Reject, but Overleaf still shows this tracked change. Reject it manually in the Overleaf review panel.';
263
- }
264
- if (code === 'tracked_change_accept_control_not_found') {
265
- return path
266
- ? `Some tracked changes in ${path} remain, but the matching Accept button was not found. Accept them manually in Overleaf.`
267
- : 'Some tracked changes remain, but the matching Accept button was not found. Accept them manually in Overleaf.';
268
- }
269
- if (code === 'tracked_change_accept_not_confirmed') {
270
- return 'Codex clicked Accept, but Overleaf still shows this tracked change. Accept it manually in the Overleaf review panel.';
271
- }
272
- if (code === 'tracked_change_editor_undo_open_failed') {
273
- return path ? `Could not open ${path} for Overleaf native undo.` : 'Could not open the file for Overleaf native undo.';
274
- }
275
- if (code === 'tracked_change_editor_undo_current_mismatch') {
276
- return path
277
- ? `${path} no longer matches this run's written content, so Codex did not use Overleaf native undo.`
278
- : 'The current content no longer matches this run\'s written content, so Codex did not use Overleaf native undo.';
279
- }
280
- if (code === 'editor_undo_control_not_found') {
281
- return path
282
- ? `Could not find Overleaf's editor Undo button to undo this run in ${path}.`
283
- : 'Could not find Overleaf\'s editor Undo button to undo this run.';
284
- }
285
- if (code === 'editor_undo_no_progress') {
286
- return path
287
- ? `Codex clicked Overleaf Undo, but ${path} did not change.`
288
- : 'Codex clicked Overleaf Undo, but the file did not change.';
289
- }
290
- if (code === 'editor_undo_max_iterations') {
291
- return path
292
- ? `${path} did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits.`
293
- : 'The file did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits.';
294
- }
295
- if (code === 'tracked_change_undo_max_iterations') {
296
- return path
297
- ? `${path} still has tracked changes left. Codex stopped to avoid rejecting unrelated edits.`
298
- : 'Tracked changes are still left. Codex stopped to avoid rejecting unrelated edits.';
299
- }
300
- if (code === 'tracked_change_undo_verify_open_failed') {
301
- return path
302
- ? `Could not open ${path} after undo to verify content. Reload Overleaf and check manually.`
303
- : 'Could not open the file after undo to verify content. Reload Overleaf and check manually.';
304
- }
305
- if (code === 'tracked_change_undo_verify_failed') {
306
- return path
307
- ? `${path} did not return to its pre-run content after rejecting tracked changes. Check this run's changes in the Overleaf review panel.`
308
- : 'The file did not return to its pre-run content after rejecting tracked changes. Check this run\'s changes in the Overleaf review panel.';
309
- }
310
- return localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
254
+ const path = result.file || result.path || fallbackPath || '';
255
+ return formatCatalogFailureReason(result, path)
256
+ || localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
311
257
  }
312
258
 
259
+ const runtimeOwnedReasonPairs = [
260
+ ['The selected provider no longer exists.', '所选模型服务已不存在。'],
261
+ ['The selected provider has no usable revision.', '所选模型服务没有可用的配置版本。'],
262
+ ['The selected provider revision was not captured for this run.', '本轮任务未记录所选模型服务的配置版本。'],
263
+ ['The selected provider changed after this run was submitted.', '本轮任务提交后,所选模型服务的配置已发生变化。'],
264
+ ['The captured selection is invalid. Attach it again.', '已记录的选区无效,请重新添加。'],
265
+ ['Provider revision must be a finite non-negative integer.', '模型服务配置版本必须是有效的非负整数。'],
266
+ ['A stable matching Overleaf account is required for shared history.', '共享历史需要稳定且匹配的 Overleaf 账号。'],
267
+ ['The background storage connection was lost.', '后台存储连接已断开。'],
268
+ ['Shared history is unavailable. Reload the extension and this page.', '共享历史不可用,请重新加载扩展和当前页面。'],
269
+ ['The shared history lock disconnected.', '共享历史的存储锁连接已断开。'],
270
+ ['Another page is saving history. Retry after it finishes.', '其他页面正在保存历史,请待保存完成后重试。'],
271
+ ['The shared history lock was lost before completion.', '操作完成前,共享历史的存储锁已丢失。'],
272
+ ['A stable account scope is required for durable persistence.', '持久保存需要稳定的账号范围。'],
273
+ ['A cross-realm browser lock is required for durable persistence.', '持久保存需要能够跨页面协调的浏览器锁。'],
274
+ ['Shared session storage is unavailable.', '共享会话存储不可用。'],
275
+ ['Native host request failed.', '本地主机请求失败。'],
276
+ ['Native host disconnected', '本地主机连接已断开。']
277
+ ];
278
+
313
279
  function localizeVisibleReason(reason) {
314
280
  const text = String(reason || '').trim();
281
+ for (const [en, zh] of runtimeOwnedReasonPairs) {
282
+ if (text === en || text === zh) return tx(en, zh);
283
+ }
284
+ const scoped = /^(?:Scoped persistence failed: |限定范围的持久保存失败:)(invalid_commit|stale_view|revision_conflict|queue_revision_conflict|invalid_queue_mutation|queue_claim_conflict)$/.exec(text);
285
+ if (scoped) return tx(`Scoped persistence failed: ${scoped[1]}`, `限定范围的持久保存失败:${scoped[1]}`);
286
+ const localized = FailureReasons?.localizeFailureMessage?.(text, getLocale(), failureReasonI18nLookup);
287
+ if (localized && localized !== text) return localized;
315
288
  if (!text || getLocale() !== 'en') {
316
289
  return text;
317
290
  }
@@ -17,11 +17,13 @@
17
17
  const deadlineAt = Number.isFinite(input.deadlineAt) && input.deadlineAt > 0 ? input.deadlineAt : null;
18
18
  const invalidDeadline = input.deadlineAt !== undefined && deadlineAt === null;
19
19
  const expired = () => invalidDeadline || (deadlineAt !== null && now() >= deadlineAt);
20
- let textBatch = [], blocked = false;
20
+ let textBatch = [], blocked = false, cancelled = false, halted = false;
21
21
  function skipUndispatched(operations) {
22
22
  const result = normalizeTextResult({ ok: false,
23
- code: blocked ? 'writeback_tail_not_started' : 'writeback_deadline_exceeded',
24
- error: blocked ? 'An earlier writeback is unconfirmed. This operation was not dispatched.'
23
+ code: cancelled ? 'codex_cancelled' : blocked ? 'writeback_tail_not_started' : 'writeback_deadline_exceeded',
24
+ error: cancelled ? 'The run was cancelled. This operation was not dispatched.'
25
+ : halted ? 'An earlier writeback stopped this file plan. This operation was not dispatched.'
26
+ : blocked ? 'An earlier writeback is unconfirmed. This operation was not dispatched.'
25
27
  : invalidDeadline ? 'The writeback deadline is invalid. This operation was not dispatched.'
26
28
  : 'The writeback deadline has elapsed. This operation was not dispatched.',
27
29
  changedDocument: false }, operations);
@@ -32,26 +34,38 @@
32
34
  if (!textBatch.length) return;
33
35
  const operations = textBatch;
34
36
  textBatch = [];
35
- if (blocked || expired()) { skipUndispatched(operations); return; }
36
- const response = await callPageBridge('applyOperations', {
37
+ if (blocked || cancelled || expired()) { skipUndispatched(operations); return; }
38
+ let response;
39
+ try { response = await callPageBridge('applyOperations', {
37
40
  operations,
38
41
  baseFiles: input.baseFiles || [],
39
42
  reviewingPolicy: input.reviewingPolicy || '',
43
+ ...(input.reviewOwnerToken ? { reviewOwnerToken: input.reviewOwnerToken } : {}),
40
44
  ...(deadlineAt !== null ? { deadlineAt } : {}),
41
45
  requireReviewing: input.requireReviewing === true,
42
46
  requireEditing: input.requireEditing === true,
43
47
  retryCreates: input.retryCreates === true,
44
48
  runProjectId: input.runProjectId || ''
45
- });
46
- const recovered = response?.code === 'page_bridge_timeout' && response.requestId
49
+ }); } catch (error) {
50
+ if (error?.code !== 'codex_cancelled' && error?.cancelled !== true) throw error;
51
+ cancelled = true;
52
+ // A later cancelled batch must not discard earlier write/Undo evidence.
53
+ if (!error.requestId && !results.length) throw error;
54
+ response = { ok: false, code: 'codex_cancelled', error: error.message,
55
+ requestId: error.requestId, changedDocument: error.changedDocument !== false };
56
+ }
57
+ const recovered = ['page_bridge_timeout', 'codex_cancelled'].includes(response?.code) && response.requestId
47
58
  ? await recoverTextReceipt(response, operations, input.runProjectId || '', deadlineAt)
48
59
  : response;
49
60
  const result = normalizeTextResult(recovered, operations);
61
+ if (response?.code === 'codex_cancelled' && response.changedDocument === false) result.receiptUnconfirmed = false;
50
62
  results.push(result);
51
- blocked = result.receiptUnconfirmed === true;
63
+ cancelled ||= hasCancellation(result);
64
+ halted ||= result.haltRemainingOperations === true;
65
+ blocked = halted || result.receiptUnconfirmed === true;
52
66
  }
53
67
  for (const operation of Array.isArray(input.operations) ? input.operations : []) {
54
- if (blocked || expired()) {
68
+ if (blocked || cancelled || expired()) {
55
69
  skipUndispatched([operation]);
56
70
  continue;
57
71
  }
@@ -60,12 +74,13 @@
60
74
  continue;
61
75
  }
62
76
  await flushTextBatch();
63
- if (blocked || expired()) {
77
+ if (blocked || cancelled || expired()) {
64
78
  skipUndispatched([operation]);
65
79
  continue;
66
80
  }
67
81
  const binaryResult = await applyBinaryOperation(operation, input);
68
82
  results.push(binaryResult);
83
+ cancelled ||= hasCancellation(binaryResult);
69
84
  blocked = binaryResult.receiptUnconfirmed === true;
70
85
  }
71
86
  await flushTextBatch();
@@ -84,14 +99,16 @@
84
99
  receipt = await callPageBridge('getWritebackReceipt', { requestId: timeout.requestId, runProjectId,
85
100
  ...(deadlineAt !== null ? { deadlineAt } : {}) });
86
101
  } catch (error) {
87
- if (error?.code === 'codex_cancelled' || error?.cancelled === true) throw error;
102
+ if (error?.code === 'codex_cancelled' || error?.cancelled === true) {
103
+ recoveryError = error.message || 'Receipt settlement was cancelled.'; break;
104
+ }
88
105
  recoveryError = error?.message || String(error);
89
106
  if (!isTransientReceiptFailure(error)) break;
90
107
  transportFailed = true;
91
108
  }
92
109
  if (!transportFailed) {
93
110
  if (receipt?.code === 'codex_cancelled' || receipt?.cancelled === true) {
94
- throw responseError(receipt, 'codex_cancelled');
111
+ recoveryError = receipt.error || 'Receipt settlement was cancelled.'; break;
95
112
  }
96
113
  if (receipt?.ok !== true) {
97
114
  recoveryError = receipt?.error?.message || receipt?.error || receipt?.reason
@@ -130,8 +147,9 @@
130
147
  const transferId = `asset_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
131
148
  const assetRef = operation.assetRef || null;
132
149
  const safeOperation = sanitizeOperation(operation);
133
- let pageBegun = false, commitAttempted = false;
150
+ let beginAttempted = false, pageBegun = false, commitAttempted = false;
134
151
  try {
152
+ beginAttempted = true;
135
153
  const begin = await callPageBridge('binaryUploadBegin', {
136
154
  transferId,
137
155
  path: operation.path,
@@ -139,6 +157,7 @@
139
157
  sha256: assetRef?.sha256 || operation.sha256 || '',
140
158
  mimeType: assetRef?.mimeType || operation.mimeType || 'application/octet-stream',
141
159
  overwrite: operation.type === 'overwrite-binary',
160
+ previousSha256: operation.type === 'overwrite-binary' ? await binaryBaselineHash(operation, input.baseFiles) : undefined,
142
161
  runProjectId: input.runProjectId || ''
143
162
  });
144
163
  if (!begin?.ok) throw responseError(begin, 'binary_upload_begin_failed');
@@ -164,6 +183,8 @@
164
183
  if (!committed?.ok) throw responseError(committed, committed?.code || 'binary_upload_failed');
165
184
  return { ok: true, applied: [{ operation: safeOperation, result: committed }], skipped: [] };
166
185
  } catch (error) {
186
+ // A commit rejected before dispatch still leaves staged bytes to clean up.
187
+ if (error.requestDispatched === false) commitAttempted = false;
167
188
  let changedDocument = error?.changedDocument === true
168
189
  || (commitAttempted && error?.changedDocument !== false);
169
190
  if (commitAttempted && changedDocument) {
@@ -176,6 +197,7 @@
176
197
  || receipt.runProjectId !== input.runProjectId) break;
177
198
  if (receipt.result?.ok === true) {
178
199
  return { ok: true, applied: [{ operation: safeOperation, result: receipt.result }], skipped: [],
200
+ cancelled: error.code === 'codex_cancelled' || error.cancelled === true,
179
201
  receiptRecovery: { transferId, recovered: true, initialError: error.message } };
180
202
  }
181
203
  if (receipt.state === 'completed' && receipt.changedDocument === false) {
@@ -186,7 +208,8 @@
186
208
  await delay(Math.min(500, Math.max(1, deadline - now())));
187
209
  } while (now() <= deadline);
188
210
  }
189
- if (pageBegun && !commitAttempted) {
211
+ const uncertainBegin = beginAttempted && error.code === 'codex_cancelled' && error.requestDispatched !== false;
212
+ if (!commitAttempted && (pageBegun || uncertainBegin)) {
190
213
  await callPageBridge('binaryUploadAbort', { transferId, runProjectId: input.runProjectId || '' }).catch(() => null);
191
214
  }
192
215
  const code = error.code || 'binary_upload_failed';
@@ -203,6 +226,9 @@
203
226
  ok: false,
204
227
  code,
205
228
  reason: error.message || 'Binary asset upload failed.',
229
+ ...(Array.isArray(error.createdDirectories) ? { createdDirectories: error.createdDirectories,
230
+ folderChanged: error.folderChanged === true, fileUploadAttempted: error.fileUploadAttempted === true,
231
+ directoryCleanupRequired: error.createdDirectories.length > 0 } : {}),
206
232
  changedDocument,
207
233
  failure: { code, stage: 'write', file: operation.path, changedDocument }
208
234
  }
@@ -249,6 +275,7 @@
249
275
  const result = response && typeof response === 'object' ? { ...response } : { ok: false };
250
276
  result.applied = Array.isArray(result.applied) ? result.applied : [];
251
277
  result.skipped = Array.isArray(result.skipped) ? result.skipped.slice() : [];
278
+ if (result.skipped.some(entry => entry.result?.receiptUnconfirmed === true)) result.receiptUnconfirmed = true;
252
279
  const key = operation => [operation?.type, operation?.path, operation?.to || ''].join('\0');
253
280
  const known = new Map();
254
281
  for (const entry of [...result.applied, ...result.skipped]) {
@@ -286,6 +313,7 @@
286
313
  for (const result of results) {
287
314
  merged.applied.push(...(Array.isArray(result?.applied) ? result.applied : []));
288
315
  merged.skipped.push(...(Array.isArray(result?.skipped) ? result.skipped : []));
316
+ if (result?.haltRemainingOperations) merged.haltRemainingOperations = true;
289
317
  // Text writeback returns the Reviewing references captured by the page
290
318
  // bridge. They are lifecycle data, not diagnostics: dropping them makes
291
319
  // recordUndoFromApply classify a successful Track write as legacy Undo,
@@ -322,6 +350,31 @@
322
350
  return safe;
323
351
  }
324
352
 
353
+ function hasCancellation(result) {
354
+ return result?.cancelled === true || result?.code === 'codex_cancelled'
355
+ || result?.skipped?.some(entry => entry.result?.code === 'codex_cancelled');
356
+ }
357
+
358
+ async function binaryBaselineHash(operation, baseFiles = []) {
359
+ const valid = value => typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
360
+ const base = (baseFiles || []).find(file => file?.path === operation.path);
361
+ let captured = valid(base?.sha256) ? base.sha256.toLowerCase() : '';
362
+ if (typeof base?.contentBase64 === 'string' && globalThis.crypto?.subtle && typeof atob === 'function') {
363
+ const bytes = Uint8Array.from(atob(base.contentBase64), value => value.charCodeAt(0));
364
+ const hash = await globalThis.crypto.subtle.digest('SHA-256', bytes);
365
+ const digest = Array.from(new Uint8Array(hash), value => value.toString(16).padStart(2, '0')).join('');
366
+ if (captured && captured !== digest) throw codedError('binary_asset_baseline_conflict', 'The captured binary baseline has conflicting content and hash.');
367
+ captured = digest;
368
+ }
369
+ if (operation.previousSha256 != null) {
370
+ if (!valid(operation.previousSha256) || (captured && captured !== operation.previousSha256.toLowerCase())) {
371
+ throw codedError('binary_asset_baseline_conflict', 'The Native and browser binary baselines differ. Read the project again before replacement.');
372
+ }
373
+ return operation.previousSha256.toLowerCase();
374
+ }
375
+ return captured;
376
+ }
377
+
325
378
  function isTransientReceiptFailure(value) {
326
379
  const code = value?.code || value?.error?.code;
327
380
  if (code) return ['page_bridge_timeout', 'page_bridge_unavailable', 'page_bridge_transport_error',
@@ -337,6 +390,11 @@
337
390
  response?.reason || response?.error?.message || response?.error || 'Asset transfer failed.'
338
391
  );
339
392
  if (typeof response?.changedDocument === 'boolean') error.changedDocument = response.changedDocument;
393
+ if (Array.isArray(response?.createdDirectories)) {
394
+ error.createdDirectories = response.createdDirectories;
395
+ error.folderChanged = response.folderChanged === true;
396
+ error.fileUploadAttempted = response.fileUploadAttempted === true;
397
+ }
340
398
  error.stage = response?.failure?.stage || 'write';
341
399
  return error;
342
400
  }