codex-overleaf-link 2.1.5 → 2.2.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 (66) hide show
  1. package/README.md +27 -23
  2. package/extension/bootstrap/background.js +14 -1
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/runtime-manifest.json +10 -0
  5. package/extension/src/background.js +507 -8
  6. package/extension/src/backgroundUpdateCoordinator.js +284 -19
  7. package/extension/src/content/activeTurnControl.js +179 -0
  8. package/extension/src/content/composerPanel.js +14 -5
  9. package/extension/src/content/contentRuntime.js +330 -309
  10. package/extension/src/content/diagnosticsPanel.js +132 -1
  11. package/extension/src/content/markdownDomRenderer.js +451 -0
  12. package/extension/src/content/markdownText.js +86 -7
  13. package/extension/src/content/mathText.js +49 -4
  14. package/extension/src/content/modelPicker.js +14 -5
  15. package/extension/src/content/nativeChannel.js +7 -1
  16. package/extension/src/content/panelRenderer.js +112 -12
  17. package/extension/src/content/pendingInputView.js +106 -0
  18. package/extension/src/content/projectProviderSelection.js +68 -0
  19. package/extension/src/content/projectSessionCleanup.js +4 -0
  20. package/extension/src/content/providerSettingsCoordinator.js +170 -22
  21. package/extension/src/content/providerSettingsDialog.js +2 -2
  22. package/extension/src/content/recentProjects.js +7 -2
  23. package/extension/src/content/runGuidanceController.js +70 -0
  24. package/extension/src/content/runGuidanceView.js +48 -0
  25. package/extension/src/content/runQueueScheduler.js +260 -0
  26. package/extension/src/content/runTimelineView.js +33 -80
  27. package/extension/src/content/sessionManager.js +2 -2
  28. package/extension/src/content/sessionPersistence.js +62 -0
  29. package/extension/src/content/settingsPanel.js +1 -1
  30. package/extension/src/content/updateNotice.js +80 -1
  31. package/extension/src/page/writebackRouter.js +3 -9
  32. package/extension/src/shared/compatibility.js +2 -1
  33. package/extension/src/shared/i18n.js +38 -0
  34. package/extension/src/shared/providerProfiles.js +3 -2
  35. package/extension/src/shared/runInputQueue.js +253 -0
  36. package/extension/src/shared/sessionState.js +45 -22
  37. package/extension/src/shared/storageDb.js +39 -44
  38. package/extension/src/shared/storageMigration.js +123 -3
  39. package/extension/src/shared/storageRunActions.js +34 -1
  40. package/extension/src/shared/updateConsent.js +15 -4
  41. package/extension/styles/panel.css +319 -20
  42. package/extension/vendor/katex/katex.min.css +1 -1
  43. package/extension/vendor/markdown-it/LICENSE +22 -0
  44. package/extension/vendor/markdown-it/SOURCE.json +7 -0
  45. package/extension/vendor/markdown-it/VERSION +1 -0
  46. package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
  47. package/native-host/src/anthropicBridgeRequest.js +3 -3
  48. package/native-host/src/anthropicBridgeResponse.js +58 -17
  49. package/native-host/src/anthropicMessagesBridge.js +156 -23
  50. package/native-host/src/chatBridgeRequest.js +1 -1
  51. package/native-host/src/chatBridgeResponse.js +12 -8
  52. package/native-host/src/chatCompletionsBridge.js +22 -40
  53. package/native-host/src/codexPromptAssembly.js +3 -1
  54. package/native-host/src/codexProviderLaunch.js +6 -3
  55. package/native-host/src/codexSessionRunner.js +65 -65
  56. package/native-host/src/codexSessionTiming.js +45 -0
  57. package/native-host/src/index.js +58 -3
  58. package/native-host/src/managedInstall.js +45 -7
  59. package/native-host/src/providerProfile.js +2 -2
  60. package/native-host/src/providerStreamLifecycle.js +100 -0
  61. package/native-host/src/responsesPassthroughBridge.js +636 -0
  62. package/native-host/src/taskRunnerRuntime.js +81 -2
  63. package/native-host/src/updateArchive.js +4 -3
  64. package/native-host/src/updateManager.js +306 -19
  65. package/native-host/src/updateTrust.js +1 -1
  66. package/package.json +3 -1
@@ -6,8 +6,22 @@ if (!globalThis.CodexOverleafCompatibility) {
6
6
  'use strict';
7
7
 
8
8
  const HOST_NAME = 'com.codex.overleaf';
9
+ const MANAGED_UPDATE_STATE_KEY = 'codex-overleaf-managed-update-state-v1';
10
+ const MANAGED_UPDATE_TABS_KEY = 'codex-overleaf-managed-update-tabs-v1';
11
+ const MANAGED_OVERLEAF_MATCHES = [
12
+ 'https://www.overleaf.com/project',
13
+ 'https://overleaf.com/project',
14
+ 'https://www.overleaf.com/project/*',
15
+ 'https://overleaf.com/project/*'
16
+ ];
17
+ const MANAGED_UPDATE_PHASE_TIMEOUT_MS = Object.freeze({
18
+ applying: 90 * 1000,
19
+ awaiting_health: 2 * 60 * 1000,
20
+ rolling_back: 60 * 1000
21
+ });
9
22
  const COMPATIBILITY_REQUIRED_METHODS = new Set([
10
23
  'codex.run',
24
+ 'codex.steer',
11
25
  'task.run',
12
26
  'task.confirm',
13
27
  'mirror.sync',
@@ -34,16 +48,57 @@ if (!globalThis.CodexOverleafCompatibility) {
34
48
  ]);
35
49
  const CodexOverleafCompatibility = globalThis.CodexOverleafCompatibility;
36
50
  let port = null;
51
+ let managedUpdateExecutionLocked = false;
37
52
  const pending = new Map();
53
+ const runJournals = new Map();
54
+ const journalWrites = new Map();
55
+ const ownerBindings = new Map();
56
+ const ownerCancelTimers = new Map();
57
+ const RUN_JOURNAL_PREFIX = 'codex-overleaf:run-journal:';
58
+ const RUN_JOURNAL_MAX_EVENTS = 300;
59
+ const RUN_JOURNAL_MAX_BYTES = 512 * 1024;
60
+ const managedBootstrapRuntime = isManagedBootstrapRuntime();
38
61
  globalThis.CodexOverleafNativeBridge = Object.freeze({
39
- requestInternal: payload => sendNativeRequest(payload, null, { skipCompatibility: true }),
62
+ requestInternal: payload => requestManagedInternal(payload),
40
63
  getPendingState: () => ({
41
64
  executionRequests: Array.from(pending.values()).filter(item => item.retryClass === 'no_silent_retry').length,
42
65
  totalRequests: pending.size
43
66
  })
44
67
  });
45
68
 
69
+ if (managedBootstrapRuntime) {
70
+ chrome.storage?.onChanged?.addListener((changes, area) => {
71
+ if (area !== 'local' || !changes[MANAGED_UPDATE_STATE_KEY]?.newValue) {
72
+ return;
73
+ }
74
+ void repairManagedUpdatePhaseMetadata(changes[MANAGED_UPDATE_STATE_KEY].newValue);
75
+ });
76
+ setTimeout(() => {
77
+ void reconcileManagedUpdateTransaction();
78
+ }, 250);
79
+ }
80
+
46
81
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
82
+ if (message?.type === 'codex-overleaf/run-journal/list') {
83
+ if (!isAllowedOverleafSender(sender)) {
84
+ sendResponse({ ok: false, journals: [] });
85
+ return undefined;
86
+ }
87
+ listRunJournals(String(message.projectKey || ''))
88
+ .then(journals => sendResponse({ ok: true, journals }))
89
+ .catch(error => sendResponse({ ok: false, journals: [], error: getErrorMessage(error, 'Journal read failed.') }));
90
+ return true;
91
+ }
92
+ if (message?.type === 'codex-overleaf/run-journal/ack') {
93
+ if (!isAllowedOverleafSender(sender)) {
94
+ sendResponse({ ok: false });
95
+ return undefined;
96
+ }
97
+ acknowledgeRunJournal(String(message.requestId || ''))
98
+ .then(() => sendResponse({ ok: true }))
99
+ .catch(error => sendResponse({ ok: false, error: getErrorMessage(error, 'Journal cleanup failed.') }));
100
+ return true;
101
+ }
47
102
  if (message?.type !== 'codex-overleaf/native-request') {
48
103
  return undefined;
49
104
  }
@@ -103,6 +158,42 @@ if (!globalThis.CodexOverleafCompatibility) {
103
158
  return true;
104
159
  });
105
160
 
161
+ chrome.runtime.onConnect?.addListener(portConnection => {
162
+ if (portConnection.name !== 'codex-overleaf/run-owner' || !isAllowedOverleafSender(portConnection.sender)) {
163
+ return;
164
+ }
165
+ portConnection.onMessage.addListener(message => {
166
+ const requestId = String(message?.requestId || '');
167
+ if (!requestId) {
168
+ return;
169
+ }
170
+ if (message.type === 'release') {
171
+ ownerBindings.delete(portConnection);
172
+ clearTimeout(ownerCancelTimers.get(requestId));
173
+ ownerCancelTimers.delete(requestId);
174
+ return;
175
+ }
176
+ if (message.type === 'bind') {
177
+ ownerBindings.set(portConnection, {
178
+ requestId,
179
+ projectKey: String(message.projectKey || ''),
180
+ documentId: String(portConnection.sender?.documentId || '')
181
+ });
182
+ clearTimeout(ownerCancelTimers.get(requestId));
183
+ ownerCancelTimers.delete(requestId);
184
+ }
185
+ });
186
+ portConnection.onDisconnect.addListener(() => {
187
+ const binding = ownerBindings.get(portConnection);
188
+ ownerBindings.delete(portConnection);
189
+ if (!binding?.requestId) {
190
+ return;
191
+ }
192
+ const timer = setTimeout(() => interruptOwnerlessRun(binding), 150);
193
+ ownerCancelTimers.set(binding.requestId, timer);
194
+ });
195
+ });
196
+
106
197
  function getNativeRetryClass(method) {
107
198
  switch (method) {
108
199
  case 'bridge.ping':
@@ -141,6 +232,18 @@ if (!globalThis.CodexOverleafCompatibility) {
141
232
  });
142
233
  }
143
234
  const request = sanitizeNativeRequest(requestWithEvidence);
235
+ if (managedUpdateExecutionLocked &&
236
+ !request.method.startsWith('update.') &&
237
+ request.method !== 'codex.cancel' &&
238
+ getNativeRetryClass(request.method) === 'no_silent_retry') {
239
+ return Promise.resolve({
240
+ ok: false,
241
+ error: {
242
+ code: 'update_applying',
243
+ message: 'A managed update is activating. Retry after the extension reloads.'
244
+ }
245
+ });
246
+ }
144
247
 
145
248
  return new Promise((resolve, reject) => {
146
249
  const pendingRequest = {
@@ -156,6 +259,9 @@ if (!globalThis.CodexOverleafCompatibility) {
156
259
  eventForwarded: false
157
260
  };
158
261
  pending.set(id, pendingRequest);
262
+ if (request.method === 'codex.run') {
263
+ createRunJournal(id, pendingRequest);
264
+ }
159
265
 
160
266
  try {
161
267
  postNativeRequest(pendingRequest);
@@ -165,6 +271,213 @@ if (!globalThis.CodexOverleafCompatibility) {
165
271
  });
166
272
  }
167
273
 
274
+ async function requestManagedInternal(payload) {
275
+ if (managedBootstrapRuntime && payload?.method === 'update.apply') {
276
+ const gate = await verifyManagedUpdateSafety();
277
+ if (!gate.ok) {
278
+ return gate;
279
+ }
280
+ managedUpdateExecutionLocked = true;
281
+ try {
282
+ const result = await sendNativeRequest(payload, null, { skipCompatibility: true });
283
+ if (!result?.ok) {
284
+ managedUpdateExecutionLocked = false;
285
+ }
286
+ return result;
287
+ } catch (error) {
288
+ managedUpdateExecutionLocked = false;
289
+ throw error;
290
+ }
291
+ }
292
+ return sendNativeRequest(payload, null, { skipCompatibility: true });
293
+ }
294
+
295
+ async function verifyManagedUpdateSafety() {
296
+ const tabs = await chrome.tabs.query({ url: MANAGED_OVERLEAF_MATCHES }).catch(() => []);
297
+ const editorTabs = tabs.filter(tab => (
298
+ isManagedEditorTab(tab) && !tab.discarded && tab.status !== 'unloaded'
299
+ ));
300
+ const probes = await Promise.all(editorTabs.map(tab => probeManagedUpdateTab(tab.id)));
301
+ const nativeGate = await sendNativeRequest({
302
+ id: crypto.randomUUID(),
303
+ method: 'update.canApply',
304
+ params: {}
305
+ }, null, { skipCompatibility: true }).catch(error => ({
306
+ ok: false,
307
+ error: {
308
+ code: error?.code || 'native_connection_failed',
309
+ message: getErrorMessage(error, 'Native Host update safety check failed.')
310
+ }
311
+ }));
312
+ const pendingState = globalThis.CodexOverleafNativeBridge?.getPendingState?.() || {};
313
+ const backgroundBlockers = Number(pendingState.executionRequests || 0) > 0
314
+ ? ['background_execution_pending']
315
+ : [];
316
+ const blockers = [
317
+ ...backgroundBlockers,
318
+ ...(globalThis.CodexOverleafUpdateStatus?.collectBlockers(probes, nativeGate) || ['busy'])
319
+ ];
320
+ if (!blockers.length) {
321
+ return { ok: true };
322
+ }
323
+ return {
324
+ ok: false,
325
+ error: {
326
+ code: 'update_not_idle',
327
+ message: 'The update paused because Overleaf or the Native Host became busy. It will retry at the next safe point.',
328
+ blockers
329
+ }
330
+ };
331
+ }
332
+
333
+ async function probeManagedUpdateTab(tabId) {
334
+ if (!Number.isInteger(tabId)) {
335
+ return { idle: false, blockers: ['tab_unavailable'] };
336
+ }
337
+ try {
338
+ return await Promise.race([
339
+ chrome.tabs.sendMessage(tabId, { type: 'codex-overleaf/update-idle-probe' }),
340
+ new Promise(resolve => setTimeout(() => resolve({
341
+ idle: false,
342
+ blockers: ['tab_probe_timeout']
343
+ }), 3500))
344
+ ]);
345
+ } catch (_error) {
346
+ return { idle: false, blockers: ['tab_probe_unavailable'] };
347
+ }
348
+ }
349
+
350
+ function isManagedEditorTab(tab) {
351
+ try {
352
+ const url = new URL(tab?.url || '');
353
+ return url.protocol === 'https:' &&
354
+ (url.hostname === 'www.overleaf.com' || url.hostname === 'overleaf.com') &&
355
+ /^\/project\/[^/]+(?:\/|$)/.test(url.pathname);
356
+ } catch (_error) {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ function isManagedBootstrapRuntime() {
362
+ if (typeof chrome.runtime?.getManifest !== 'function') {
363
+ return false;
364
+ }
365
+ return chrome.runtime.getManifest().background?.service_worker === 'bootstrap/background.js';
366
+ }
367
+
368
+ async function repairManagedUpdatePhaseMetadata(state) {
369
+ const timeoutMs = MANAGED_UPDATE_PHASE_TIMEOUT_MS[state?.state] || 0;
370
+ if (!timeoutMs || Number(state.deadlineAt || 0) > 0) {
371
+ return;
372
+ }
373
+ const now = Date.now();
374
+ await chrome.storage.local.set({
375
+ [MANAGED_UPDATE_STATE_KEY]: {
376
+ ...state,
377
+ phaseStartedAt: now,
378
+ deadlineAt: now + timeoutMs,
379
+ heartbeatAt: now
380
+ }
381
+ });
382
+ }
383
+
384
+ async function reconcileManagedUpdateTransaction() {
385
+ const response = await sendNativeRequest({
386
+ id: crypto.randomUUID(),
387
+ method: 'update.status',
388
+ params: {}
389
+ }, null, { skipCompatibility: true }).catch(() => null);
390
+ if (!response?.ok) {
391
+ return;
392
+ }
393
+
394
+ const transaction = response.result?.transaction || null;
395
+ const state = await getManagedUpdateState();
396
+ if (transaction?.state === 'rolled_back') {
397
+ await setManagedUpdateState({
398
+ ...state,
399
+ state: 'rolled_back',
400
+ currentVersion: transaction.sourceVersion || state.currentVersion,
401
+ transactionId: transaction.id || state.transactionId,
402
+ code: transaction.reasonCode || state.code || 'update_rolled_back'
403
+ });
404
+ await reloadManagedUpdateTabs();
405
+ return;
406
+ }
407
+ if (transaction?.state === 'committed') {
408
+ const targetVersion = transaction.targetVersion || state.latestVersion || state.currentVersion;
409
+ await setManagedUpdateState({
410
+ ...state,
411
+ state: 'committed',
412
+ currentVersion: targetVersion,
413
+ latestVersion: targetVersion,
414
+ transactionId: '',
415
+ blocker: '',
416
+ blockers: [],
417
+ code: '',
418
+ message: ''
419
+ });
420
+ await reloadManagedUpdateTabs();
421
+ return;
422
+ }
423
+ if (transaction?.state === 'staged' && ['applying', 'awaiting_health'].includes(state.state)) {
424
+ await setManagedUpdateState({
425
+ ...state,
426
+ state: 'staged',
427
+ transactionId: transaction.id || state.transactionId,
428
+ blocker: '',
429
+ blockers: [],
430
+ code: '',
431
+ message: '',
432
+ phaseStartedAt: 0,
433
+ deadlineAt: 0,
434
+ heartbeatAt: 0
435
+ });
436
+ setTimeout(() => {
437
+ void globalThis.CodexOverleafManagedUpdateExecutor?.installAuthorizedUpdate?.().catch?.(() => {});
438
+ }, 250);
439
+ return;
440
+ }
441
+ if (!transaction && ['applying', 'awaiting_health', 'rolling_back'].includes(state.state)) {
442
+ await setManagedUpdateState({
443
+ ...state,
444
+ state: 'failed',
445
+ code: 'update_transaction_lost',
446
+ message: 'The managed update transaction could not be recovered. Use the manual update command.',
447
+ phaseStartedAt: 0,
448
+ deadlineAt: 0,
449
+ heartbeatAt: 0
450
+ });
451
+ }
452
+ }
453
+
454
+ async function getManagedUpdateState() {
455
+ const stored = await chrome.storage.local.get(MANAGED_UPDATE_STATE_KEY);
456
+ return stored?.[MANAGED_UPDATE_STATE_KEY] || {
457
+ state: 'idle',
458
+ managed: true,
459
+ currentVersion: chrome.runtime.getManifest().version,
460
+ latestVersion: chrome.runtime.getManifest().version
461
+ };
462
+ }
463
+
464
+ async function setManagedUpdateState(state) {
465
+ await chrome.storage.local.set({ [MANAGED_UPDATE_STATE_KEY]: state });
466
+ return state;
467
+ }
468
+
469
+ async function reloadManagedUpdateTabs() {
470
+ const stored = await chrome.storage.local.get(MANAGED_UPDATE_TABS_KEY);
471
+ const pendingIds = Array.isArray(stored?.[MANAGED_UPDATE_TABS_KEY])
472
+ ? stored[MANAGED_UPDATE_TABS_KEY]
473
+ : [];
474
+ const openTabs = await chrome.tabs.query({ url: MANAGED_OVERLEAF_MATCHES }).catch(() => []);
475
+ const openIds = new Set(openTabs.map(tab => tab.id).filter(Number.isInteger));
476
+ const reloadIds = [...new Set(pendingIds.filter(tabId => Number.isInteger(tabId) && openIds.has(tabId)))];
477
+ await chrome.storage.local.remove(MANAGED_UPDATE_TABS_KEY);
478
+ await Promise.allSettled(reloadIds.map(tabId => chrome.tabs.reload(tabId)));
479
+ }
480
+
168
481
  function getNativeCompatibilityBlock(request = {}) {
169
482
  const evidence = getNativeCompatibilityEvidence(request);
170
483
  if (!evidence) {
@@ -366,15 +679,20 @@ if (!globalThis.CodexOverleafCompatibility) {
366
679
  }
367
680
  const pendingRequest = pending.get(id);
368
681
  if (message?.event) {
369
- pendingRequest.eventForwarded = (
370
- forwardNativeEvent(pendingRequest.tabId, id, message.event) ||
371
- pendingRequest.eventForwarded
372
- );
682
+ const { sequence, write } = appendRunJournalEvent(id, message.event);
683
+ Promise.resolve(write).catch(() => {}).then(() => {
684
+ pendingRequest.eventForwarded = (
685
+ forwardNativeEvent(pendingRequest.tabId, id, message.event, sequence) ||
686
+ pendingRequest.eventForwarded
687
+ );
688
+ });
373
689
  return;
374
690
  }
375
691
  pendingRequest.finalResponseReceived = true;
376
692
  pending.delete(id);
377
- pendingRequest.resolve(message);
693
+ finalizeRunJournal(id, message).catch(() => {}).finally(() => {
694
+ pendingRequest.resolve(message);
695
+ });
378
696
  });
379
697
  nativePort.onDisconnect.addListener(() => {
380
698
  if (nativePort !== port) {
@@ -432,6 +750,13 @@ if (!globalThis.CodexOverleafCompatibility) {
432
750
 
433
751
  function rejectInterruptedNativeRequest(pendingId, pendingRequest, errorMessage) {
434
752
  pending.delete(pendingId);
753
+ finalizeRunJournal(pendingId, {
754
+ ok: false,
755
+ error: {
756
+ code: 'native_execution_interrupted',
757
+ message: errorMessage || 'Native host disconnected'
758
+ }
759
+ }).catch(() => {});
435
760
  if (pendingRequest.retryClass === 'no_silent_retry') {
436
761
  pendingRequest.reject(createNativeRequestError(
437
762
  'native_execution_interrupted',
@@ -522,7 +847,7 @@ if (!globalThis.CodexOverleafCompatibility) {
522
847
  }
523
848
  }
524
849
 
525
- function forwardNativeEvent(tabId, id, event) {
850
+ function forwardNativeEvent(tabId, id, event, journalSeq = 0) {
526
851
  if (typeof tabId !== 'number') {
527
852
  return false;
528
853
  }
@@ -530,12 +855,186 @@ if (!globalThis.CodexOverleafCompatibility) {
530
855
  chrome.tabs.sendMessage(tabId, {
531
856
  type: 'codex-overleaf/native-event',
532
857
  id,
533
- event
858
+ event,
859
+ journalSeq
534
860
  }, () => {
535
861
  void chrome.runtime.lastError;
536
862
  });
537
863
  return true;
538
864
  }
865
+
866
+ function createRunJournal(id, pendingRequest) {
867
+ const params = pendingRequest.request?.params || {};
868
+ const journal = {
869
+ requestId: id,
870
+ projectKey: String(params.projectId || params.project?.projectId || ''),
871
+ clientRunId: String(params.clientRunId || ''),
872
+ sessionId: String(params.clientSessionId || ''),
873
+ task: sanitizeJournalString(params.task).slice(0, 12000),
874
+ mode: String(params.mode || ''),
875
+ model: String(params.model || ''),
876
+ createdAt: new Date().toISOString(),
877
+ updatedAt: new Date().toISOString(),
878
+ terminal: false,
879
+ ownerLost: false,
880
+ nextSequence: 1,
881
+ events: []
882
+ };
883
+ runJournals.set(id, journal);
884
+ pruneInMemoryRunJournals();
885
+ return queueJournalWrite(id);
886
+ }
887
+
888
+ function appendRunJournalEvent(id, event) {
889
+ const journal = runJournals.get(id);
890
+ if (!journal) {
891
+ return { sequence: 0, write: Promise.resolve() };
892
+ }
893
+ const sequence = journal.nextSequence++;
894
+ journal.events.push({ sequence, event: sanitizeJournalValue(event) });
895
+ journal.updatedAt = new Date().toISOString();
896
+ trimRunJournal(journal);
897
+ return { sequence, write: queueJournalWrite(id) };
898
+ }
899
+
900
+ function finalizeRunJournal(id, response) {
901
+ const journal = runJournals.get(id);
902
+ if (!journal) {
903
+ return Promise.resolve();
904
+ }
905
+ journal.terminal = true;
906
+ journal.updatedAt = new Date().toISOString();
907
+ journal.final = {
908
+ ok: response?.ok === true,
909
+ code: String(response?.error?.code || ''),
910
+ status: String(response?.result?.status || '')
911
+ };
912
+ return queueJournalWrite(id);
913
+ }
914
+
915
+ function trimRunJournal(journal) {
916
+ if (journal.events.length > RUN_JOURNAL_MAX_EVENTS) {
917
+ journal.events.splice(0, journal.events.length - RUN_JOURNAL_MAX_EVENTS);
918
+ }
919
+ while (journal.events.length && JSON.stringify(journal).length * 2 > RUN_JOURNAL_MAX_BYTES) {
920
+ journal.events.shift();
921
+ }
922
+ }
923
+
924
+ function sanitizeJournalValue(value, depth = 0) {
925
+ if (typeof value === 'string') {
926
+ return sanitizeJournalString(value);
927
+ }
928
+ if (value === null || value === undefined || typeof value === 'number' || typeof value === 'boolean') {
929
+ return value;
930
+ }
931
+ if (depth > 10) {
932
+ return '[nested value omitted]';
933
+ }
934
+ if (Array.isArray(value)) {
935
+ return value.slice(0, 200).map(item => sanitizeJournalValue(item, depth + 1));
936
+ }
937
+ if (typeof value === 'object') {
938
+ return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key, item]) => [
939
+ sanitizeJournalString(key).slice(0, 160),
940
+ sanitizeJournalValue(item, depth + 1)
941
+ ]));
942
+ }
943
+ return String(value);
944
+ }
945
+
946
+ function sanitizeJournalString(value) {
947
+ return String(value || '')
948
+ .replace(/\b(?:sk|hf|glpat|npm|ghp|github_pat|AKIA|AIza)[_-]?[A-Za-z0-9_-]{12,}\b/g, '[REDACTED]')
949
+ .replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, '[REDACTED]')
950
+ .replace(/(?:file:\/\/)?\/(?:Users|home|private|var|opt|Library)\/[^\s"'<>]+/g, '[local path]')
951
+ .slice(0, 131072);
952
+ }
953
+
954
+ function queueJournalWrite(id) {
955
+ const storage = chrome.storage?.session;
956
+ if (!storage || !runJournals.has(id)) {
957
+ return Promise.resolve();
958
+ }
959
+ const previous = journalWrites.get(id) || Promise.resolve();
960
+ const snapshot = JSON.parse(JSON.stringify(runJournals.get(id)));
961
+ const next = previous.catch(() => {}).then(() => storage.set({
962
+ [RUN_JOURNAL_PREFIX + id]: snapshot
963
+ }));
964
+ journalWrites.set(id, next);
965
+ return next.finally(() => {
966
+ if (journalWrites.get(id) === next) {
967
+ journalWrites.delete(id);
968
+ }
969
+ });
970
+ }
971
+
972
+ async function listRunJournals(projectKey) {
973
+ await Promise.allSettled(Array.from(journalWrites.values()));
974
+ const storage = chrome.storage?.session;
975
+ if (!storage) {
976
+ return [];
977
+ }
978
+ const values = await storage.get(null);
979
+ const entries = Object.entries(values || {})
980
+ .filter(([key, value]) => key.startsWith(RUN_JOURNAL_PREFIX)
981
+ && (!projectKey || value?.projectKey === projectKey)
982
+ && (value?.terminal || value?.ownerLost))
983
+ .sort((a, b) => String(a[1]?.createdAt || '').localeCompare(String(b[1]?.createdAt || '')));
984
+ return entries.slice(-16).map(([_key, value]) => value);
985
+ }
986
+
987
+ function pruneInMemoryRunJournals() {
988
+ if (runJournals.size <= 16) {
989
+ return;
990
+ }
991
+ const oldest = Array.from(runJournals.values())
992
+ .filter(journal => journal?.terminal || journal?.ownerLost)
993
+ .sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')))
994
+ .slice(0, runJournals.size - 16);
995
+ for (const journal of oldest) {
996
+ runJournals.delete(journal.requestId);
997
+ journalWrites.delete(journal.requestId);
998
+ chrome.storage?.session?.remove?.(RUN_JOURNAL_PREFIX + journal.requestId).catch?.(() => {});
999
+ }
1000
+ }
1001
+
1002
+ async function acknowledgeRunJournal(id) {
1003
+ if (!id) {
1004
+ return;
1005
+ }
1006
+ await (journalWrites.get(id) || Promise.resolve()).catch(() => {});
1007
+ runJournals.delete(id);
1008
+ journalWrites.delete(id);
1009
+ await chrome.storage?.session?.remove?.(RUN_JOURNAL_PREFIX + id);
1010
+ }
1011
+
1012
+ async function interruptOwnerlessRun(binding) {
1013
+ ownerCancelTimers.delete(binding.requestId);
1014
+ if (Array.from(ownerBindings.values()).some(value => value.requestId === binding.requestId)) {
1015
+ return;
1016
+ }
1017
+ const journal = runJournals.get(binding.requestId);
1018
+ if (journal) {
1019
+ journal.ownerLost = true;
1020
+ journal.updatedAt = new Date().toISOString();
1021
+ await queueJournalWrite(binding.requestId).catch(() => {});
1022
+ }
1023
+ if (!pending.has(binding.requestId)) {
1024
+ return;
1025
+ }
1026
+ try {
1027
+ sendNativeCancel({
1028
+ method: 'codex.cancel',
1029
+ params: {
1030
+ requestId: binding.requestId,
1031
+ projectKey: binding.projectKey || undefined
1032
+ }
1033
+ });
1034
+ } catch (_error) {
1035
+ // The original request disconnect path will settle the journal.
1036
+ }
1037
+ }
539
1038
  })();
540
1039
 
541
1040
  (function loadConsentDrivenUpdateRuntime(root) {