codex-overleaf-link 2.5.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -2,7 +2,12 @@
2
2
 
3
3
  const { buildCodexRuntimeEvent } = require('./codexRuntimeIdentity');
4
4
 
5
+ const { runOwnedNativeHelper } = require('./nativeHelperLifecycle');
6
+ const { stopCodexAppServer, throwIfAborted } = require('./codexSessionTiming');
5
7
  const { spawn } = require('node:child_process');
8
+ const { normalizeSafeProjectPath } = require('../../extension/src/shared/projectFiles');
9
+ const { createHash } = require('node:crypto');
10
+ const NativeRequestIdentity = require('../../extension/src/shared/nativeRequestIdentity');
6
11
  const { buildOperationSummary, splitDeletePlan } = require('../../extension/src/shared/summary');
7
12
  const {
8
13
  MIN_COMPATIBLE_EXTENSION_VERSION,
@@ -36,6 +41,7 @@ const {
36
41
  const { version: PACKAGE_VERSION } = require('../../package.json');
37
42
 
38
43
  const processStopBarrier = require('./processStopBarrier');
44
+ const runLifecycle = require('./nativeRunLifecycle');
39
45
  const activeProjectLocks = new Map();
40
46
  const activeRunControllers = new Map();
41
47
  const activeRunEntries = new Map();
@@ -44,7 +50,7 @@ const activeRunEntries = new Map();
44
50
  // Overleaf tab was reloaded — the requestId lived in content-side JS state
45
51
  // and is gone, but the native-host-side controller is still running).
46
52
  const activeRunByProject = new Map();
47
- const CODEX_RUN_PASSTHROUGH_ERROR_CODES = new Set(['thread_resume_failed', 'codex_no_usable_result', 'codex_process_stop_unconfirmed', 'selected_context_unresolved']);
53
+ const CODEX_RUN_PASSTHROUGH_ERROR_CODES = new Set(['thread_resume_failed', 'codex_no_usable_result', 'codex_process_stop_unconfirmed', 'selected_context_unresolved', 'codex_local_skill_unavailable']);
48
54
 
49
55
  async function handleRequest(request, env = process.env, emit = () => {}) {
50
56
  if (!request || typeof request !== 'object') {
@@ -66,7 +72,7 @@ async function handleRequest(request, env = process.env, emit = () => {}) {
66
72
  platform: getNativeRuntimePlatform({ env }),
67
73
  protocolVersion: 2,
68
74
  supportedProtocol: { ...SUPPORTED_NATIVE_PROTOCOL },
69
- capabilities: Object.fromEntries(REQUIRED_CAPABILITIES.map(capability => [capability, true])),
75
+ capabilities: { ...Object.fromEntries(REQUIRED_CAPABILITIES.map(capability => [capability, true])), literalProjectPaths: true },
70
76
  minExtensionVersion: MIN_COMPATIBLE_EXTENSION_VERSION,
71
77
  version: PACKAGE_VERSION,
72
78
  environment: summarizeNativeEnvironment(env)
@@ -204,6 +210,8 @@ function quotaErrorResponse(id, violation) {
204
210
  }
205
211
 
206
212
  async function handleCodexRun(request, env, emit) {
213
+ const identity = NativeRequestIdentity.validate(request.id);
214
+ if (!identity.ok) return errorResponse(request.id, identity.error.code, identity.error.message, identity.error.details);
207
215
  let params = request.params || {};
208
216
  if (params.writingStyleBuild != null) {
209
217
  if (typeof params.writingStyleBuild !== 'object' || Array.isArray(params.writingStyleBuild)) {
@@ -215,7 +223,7 @@ async function handleCodexRun(request, env, emit) {
215
223
  useExistingMirror: false,
216
224
  fileOverlays: [],
217
225
  writingStyle: null,
218
- writingStyleBuild: { ...params.writingStyleBuild, requestId: String(request.id || '') }
226
+ writingStyleBuild: { ...params.writingStyleBuild, requestId: String(request.id) }
219
227
  };
220
228
  }
221
229
  if (isCodexMissing(env)) {
@@ -236,17 +244,16 @@ async function handleCodexRun(request, env, emit) {
236
244
  return errorResponse(request.id, 'project_locked', `Project ${projectKey} is currently in use by codex.run`);
237
245
  }
238
246
  const abortController = new AbortController();
247
+ const lifetime = runLifecycle.register({ controller: abortController, projectKey, rootDir: env.CODEX_OVERLEAF_MIRROR_ROOT });
239
248
  const activeEntry = {
240
- id: request.id || '',
249
+ id: request.id,
241
250
  projectKey,
242
251
  controller: abortController,
243
252
  control: null
244
253
  };
245
- if (request.id) {
246
- activeRunControllers.set(request.id, abortController);
247
- activeRunEntries.set(request.id, activeEntry);
248
- activeRunByProject.set(projectKey, activeEntry);
249
- }
254
+ activeRunControllers.set(request.id, abortController);
255
+ activeRunEntries.set(request.id, activeEntry);
256
+ activeRunByProject.set(projectKey, activeEntry);
250
257
  let providerResolution;
251
258
  try {
252
259
  try {
@@ -264,6 +271,7 @@ async function handleCodexRun(request, env, emit) {
264
271
  return providerErrorResponse(request.id, error);
265
272
  }
266
273
 
274
+ abortController.signal.throwIfAborted();
267
275
  if (params.useExistingMirror) {
268
276
  const { getMirrorStatus, applyFileOverlays } = require('./mirrorWorkspace');
269
277
  const rootDir = env.CODEX_OVERLEAF_MIRROR_ROOT;
@@ -288,7 +296,9 @@ async function handleCodexRun(request, env, emit) {
288
296
  );
289
297
  }
290
298
 
291
- if (Array.isArray(params.fileOverlays) && params.fileOverlays.length) {
299
+ // OT overlays prove that the current page agrees with the trusted OT
300
+ // baseline; they must not rewrite it or supply a replacement hash.
301
+ if (!isOtWarmMirrorReuseRequest(params) && Array.isArray(params.fileOverlays) && params.fileOverlays.length) {
292
302
  await applyFileOverlays({ projectId: projectKey, overlays: params.fileOverlays, rootDir });
293
303
  }
294
304
  } else if (!params.writingStyleBuild && !isSnapshotlessSkillInstallerRun(params) && !hasRunnableProjectSnapshotEvidence(params)) {
@@ -306,6 +316,7 @@ async function handleCodexRun(request, env, emit) {
306
316
  rootDir: env.CODEX_OVERLEAF_MIRROR_ROOT,
307
317
  providerLaunch: providerResolution.providerLaunch,
308
318
  onStopUnconfirmed: error => processStopBarrier.record(projectKey, env.CODEX_OVERLEAF_MIRROR_ROOT, error),
319
+ onProcessStarted: lifetime.trackProcess,
309
320
  onControlReady: control => {
310
321
  activeEntry.control = control;
311
322
  },
@@ -346,16 +357,17 @@ async function handleCodexRun(request, env, emit) {
346
357
  });
347
358
  return errorResponse(request.id, 'codex_run_failed', truncateText(error.message, 12000));
348
359
  } finally {
349
- if (request.id && activeRunControllers.get(request.id) === abortController) {
360
+ if (activeRunControllers.get(request.id) === abortController) {
350
361
  activeRunControllers.delete(request.id);
351
362
  }
352
- if (request.id && activeRunEntries.get(request.id) === activeEntry) {
363
+ if (activeRunEntries.get(request.id) === activeEntry) {
353
364
  activeRunEntries.delete(request.id);
354
365
  }
355
366
  if (activeRunByProject.get(projectKey)?.controller === abortController) {
356
367
  activeRunByProject.delete(projectKey);
357
368
  }
358
369
  releaseProjectLock(projectKey, lockToken);
370
+ lifetime.complete();
359
371
  }
360
372
  }
361
373
 
@@ -400,8 +412,8 @@ async function handleCodexSteer(request) {
400
412
  }
401
413
 
402
414
  function findActiveRunEntry(params = {}) {
403
- const targetId = String(params.requestId || params.id || '');
404
- if (targetId && activeRunEntries.has(targetId)) {
415
+ const targetId = params.requestId ?? params.id;
416
+ if (activeRunEntries.has(targetId)) {
405
417
  return activeRunEntries.get(targetId);
406
418
  }
407
419
  const projectKey = String(params.projectKey || '');
@@ -420,11 +432,11 @@ function findActiveRunEntry(params = {}) {
420
432
  // can't punch through a real live run.
421
433
  function handleCodexCancel(request, env = process.env) {
422
434
  const params = request.params || {};
423
- const targetId = params.requestId || params.id;
435
+ const targetId = params.requestId ?? params.id;
424
436
  const projectKey = typeof params.projectKey === 'string' ? params.projectKey : '';
425
437
  const force = params.force === true;
426
438
 
427
- if (targetId && activeRunControllers.has(targetId)) {
439
+ if (activeRunControllers.has(targetId)) {
428
440
  activeRunControllers.get(targetId).abort(createCancellationError());
429
441
  return okResponse(request.id, {
430
442
  cancelled: true,
@@ -439,7 +451,7 @@ function handleCodexCancel(request, env = process.env) {
439
451
  return okResponse(request.id, {
440
452
  cancelled: true,
441
453
  projectKey,
442
- requestId: entry.id || ''
454
+ requestId: entry.id
443
455
  });
444
456
  }
445
457
  if (processStopBarrier.isBlocked(projectKey, env.CODEX_OVERLEAF_MIRROR_ROOT)) {
@@ -473,8 +485,10 @@ function handleCodexHistoryClear(request, env) {
473
485
  }
474
486
  }
475
487
 
476
- function handleSkillsList(request, env) {
488
+ async function handleSkillsList(request, env) {
477
489
  try {
490
+ if (request.params?.scope === 'codex-local') return okResponse(request.id,
491
+ await require('./codexLocalSkills').listCodexLocalSkills({ env }));
478
492
  const { CODEX_OVERLEAF_SKILL_SCOPE, listCodexOverleafSkills, listProjectSkills } = require('./localSkills');
479
493
  if (request.params?.scope === CODEX_OVERLEAF_SKILL_SCOPE) {
480
494
  // Install/restore official skills before listing so newly shipped ones
@@ -493,9 +507,9 @@ function handleSkillsList(request, env) {
493
507
  return errorResponse(request.id, 'skills_list_failed', error.message);
494
508
  }
495
509
  }
496
-
497
510
  function handleSkillsInstall(request, env) {
498
511
  try {
512
+ if (request.params?.scope === 'codex-local') throw new Error('Local Codex skills are managed by Codex.');
499
513
  const { CODEX_OVERLEAF_SKILL_SCOPE, installCodexOverleafSkill, installProjectSkill } = require('./localSkills');
500
514
  if (request.params?.scope === CODEX_OVERLEAF_SKILL_SCOPE) {
501
515
  return okResponse(request.id, installCodexOverleafSkill({
@@ -514,9 +528,9 @@ function handleSkillsInstall(request, env) {
514
528
  return errorResponse(request.id, 'skills_install_failed', error.message);
515
529
  }
516
530
  }
517
-
518
531
  function handleSkillsRemove(request, env) {
519
532
  try {
533
+ if (request.params?.scope === 'codex-local') throw new Error('Local Codex skills are managed by Codex.');
520
534
  const { CODEX_OVERLEAF_SKILL_SCOPE, removeCodexOverleafSkill, removeProjectSkill } = require('./localSkills');
521
535
  if (request.params?.scope === CODEX_OVERLEAF_SKILL_SCOPE) {
522
536
  return okResponse(request.id, removeCodexOverleafSkill({
@@ -544,13 +558,13 @@ function hasRunnableProjectSnapshotEvidence(params = {}) {
544
558
  if (params.restrictToFocusFiles !== true) {
545
559
  return false;
546
560
  }
547
- const normalizedFocusFiles = normalizeSnapshotEvidencePaths(params.focusFiles);
561
+ const normalizedFocusFiles = normalizeSnapshotEvidencePaths(params.focusFiles, params.focusFilesAreLiteral === true);
548
562
  if (!normalizedFocusFiles.length || !Array.isArray(params.project?.files)) {
549
563
  return false;
550
564
  }
551
565
  const evidenceFiles = new Map();
552
566
  for (const file of params.project.files) {
553
- const filePath = normalizeSnapshotEvidencePath(file?.path);
567
+ const filePath = normalizeSafeProjectPath(file?.path);
554
568
  if (!filePath || !isUsableSnapshotEvidenceContent(file?.content)) {
555
569
  continue;
556
570
  }
@@ -561,19 +575,14 @@ function hasRunnableProjectSnapshotEvidence(params = {}) {
561
575
 
562
576
  function isSnapshotlessSkillInstallerRun(params = {}) {
563
577
  return params.skipMirrorSync === true
564
- && String(params.skillInvocation?.id || '').trim() === 'skill-installer';
578
+ && String(params.skillInvocation?.id || '').trim() === 'skill-installer'
579
+ && (!params.skillInvocation?.scope || params.skillInvocation.scope === 'builtin');
565
580
  }
566
581
 
567
582
  function validateOtFocusedWarmMirrorReuse(params = {}, status = {}) {
568
583
  if (!isOtWarmMirrorReuseRequest(params)) {
569
584
  return { ok: false };
570
585
  }
571
- if (Array.isArray(params.fileOverlays) && params.fileOverlays.length) {
572
- return {
573
- ok: false,
574
- message: 'OT warm mirror reuse does not accept file overlays'
575
- };
576
- }
577
586
  if (status?.exists !== true) {
578
587
  return {
579
588
  ok: false,
@@ -586,7 +595,7 @@ function validateOtFocusedWarmMirrorReuse(params = {}, status = {}) {
586
595
  message: 'OT warm mirror reuse requires restrictToFocusFiles=true'
587
596
  };
588
597
  }
589
- const normalizedFocusFiles = normalizeSnapshotEvidencePaths(params.focusFiles);
598
+ const normalizedFocusFiles = normalizeSnapshotEvidencePaths(params.focusFiles, params.focusFilesAreLiteral === true);
590
599
  if (!normalizedFocusFiles.length) {
591
600
  return {
592
601
  ok: false,
@@ -594,14 +603,14 @@ function validateOtFocusedWarmMirrorReuse(params = {}, status = {}) {
594
603
  };
595
604
  }
596
605
 
597
- const freshFiles = new Set();
606
+ const freshFiles = new Map();
598
607
  for (const file of Array.isArray(status.otFreshFiles) ? status.otFreshFiles : []) {
599
608
  if (file?.state !== 'fresh') {
600
609
  continue;
601
610
  }
602
- const filePath = normalizeSnapshotEvidencePath(file.path);
611
+ const filePath = normalizeSafeProjectPath(file.path);
603
612
  if (filePath) {
604
- freshFiles.add(filePath);
613
+ freshFiles.set(filePath, file);
605
614
  }
606
615
  }
607
616
  const missingFiles = normalizedFocusFiles.filter(filePath => !freshFiles.has(filePath));
@@ -612,6 +621,22 @@ function validateOtFocusedWarmMirrorReuse(params = {}, status = {}) {
612
621
  };
613
622
  }
614
623
 
624
+ if (Array.isArray(params.fileOverlays) && params.fileOverlays.length) {
625
+ const overlays = new Set();
626
+ for (const overlay of params.fileOverlays) {
627
+ const filePath = normalizeSafeProjectPath(overlay?.path);
628
+ if (!normalizedFocusFiles.includes(filePath) || overlays.has(filePath)
629
+ || typeof overlay?.content !== 'string' || !/^[a-f0-9]{64}$/i.test(freshFiles.get(filePath)?.sha256 || '')
630
+ || createHash('sha256').update(overlay.content).digest('hex') !== freshFiles.get(filePath).sha256.toLowerCase()) {
631
+ return { ok: false, message: 'OT warm mirror overlay does not match the current focused OT baseline' };
632
+ }
633
+ overlays.add(filePath);
634
+ }
635
+ if (normalizedFocusFiles.some(filePath => !overlays.has(filePath))) {
636
+ return { ok: false, message: 'OT warm mirror overlays do not cover all focused files' };
637
+ }
638
+ }
639
+
615
640
  return { ok: true };
616
641
  }
617
642
 
@@ -627,11 +652,11 @@ function isUsableSnapshotEvidenceContent(content) {
627
652
  return Boolean(text) && !/^(loading|loading\.{3}|loading…)$/i.test(text);
628
653
  }
629
654
 
630
- function normalizeSnapshotEvidencePaths(value) {
655
+ function normalizeSnapshotEvidencePaths(value, literal = false) {
631
656
  const seen = new Set();
632
657
  const paths = [];
633
658
  for (const item of Array.isArray(value) ? value : []) {
634
- const filePath = normalizeSnapshotEvidencePath(item);
659
+ const filePath = literal ? normalizeSafeProjectPath(item) : normalizeSnapshotEvidencePath(item);
635
660
  if (!filePath || seen.has(filePath)) {
636
661
  continue;
637
662
  }
@@ -656,6 +681,7 @@ function createCancellationError() {
656
681
  }
657
682
 
658
683
  function isCancellationError(error = {}) {
684
+ if (['codex_turn_failed', 'codex_turn_status_invalid'].includes(error.code)) return false;
659
685
  return error.code === 'codex_cancelled'
660
686
  || error.name === 'AbortError'
661
687
  || /cancelled by the user|was cancelled/i.test(error.message || '');
@@ -753,6 +779,7 @@ async function handleMirrorConfirmWriteback(request, env) {
753
779
  const result = await confirmWritebackFiles({
754
780
  projectId,
755
781
  paths: params.paths,
782
+ expectedFiles: params.expectedFiles,
756
783
  rootDir
757
784
  });
758
785
  return okResponse(request.id, result);
@@ -1127,10 +1154,12 @@ function parseAgentArgsJson(value) {
1127
1154
  }
1128
1155
 
1129
1156
  function runExternalAgent(agentSpec, params, emit = () => {}, options = {}) {
1130
- return new Promise((resolve, reject) => {
1157
+ return runOwnedNativeHelper(signal => new Promise((resolve, reject) => {
1158
+ throwIfAborted(signal);
1131
1159
  const child = spawn(agentSpec.file, agentSpec.args || [], {
1132
1160
  env: options.env || process.env,
1133
1161
  shell: false,
1162
+ detached: process.platform !== 'win32',
1134
1163
  stdio: ['pipe', 'pipe', 'pipe']
1135
1164
  });
1136
1165
  const timeoutMs = parseOptionalPositiveInteger(options.timeoutMs);
@@ -1140,6 +1169,8 @@ function runExternalAgent(agentSpec, params, emit = () => {}, options = {}) {
1140
1169
  let stderrRemainder = '';
1141
1170
  let outputBytes = 0;
1142
1171
  let settled = false;
1172
+ const onAbort = () => fail(signal.reason);
1173
+ signal.addEventListener('abort', onAbort, { once: true });
1143
1174
 
1144
1175
  const timeout = timeoutMs
1145
1176
  ? setTimeout(() => {
@@ -1164,10 +1195,8 @@ function runExternalAgent(agentSpec, params, emit = () => {}, options = {}) {
1164
1195
  if (timeout) {
1165
1196
  clearTimeout(timeout);
1166
1197
  }
1167
- if (child.exitCode === null && !child.killed) {
1168
- child.kill('SIGTERM');
1169
- }
1170
- reject(error);
1198
+ signal.removeEventListener('abort', onAbort);
1199
+ stopCodexAppServer(child, { processGroup: process.platform !== 'win32', reason: error }).then(() => reject(error), reject);
1171
1200
  }
1172
1201
 
1173
1202
  function succeed(result) {
@@ -1178,7 +1207,8 @@ function runExternalAgent(agentSpec, params, emit = () => {}, options = {}) {
1178
1207
  if (timeout) {
1179
1208
  clearTimeout(timeout);
1180
1209
  }
1181
- resolve(result);
1210
+ signal.removeEventListener('abort', onAbort);
1211
+ stopCodexAppServer(child, { processGroup: process.platform !== 'win32' }).then(() => resolve(result), reject);
1182
1212
  }
1183
1213
 
1184
1214
  child.stdout.setEncoding('utf8');
@@ -1200,6 +1230,7 @@ function runExternalAgent(agentSpec, params, emit = () => {}, options = {}) {
1200
1230
  emitTaskEvent(emit, event.type || 'agent.progress', event.title || event.type || 'Agent progress', event.detail || {}, event.status || 'running');
1201
1231
  }
1202
1232
  });
1233
+ child.stdin.on('error', fail);
1203
1234
  child.on('error', fail);
1204
1235
  child.on('close', code => {
1205
1236
  if (settled) {
@@ -1226,7 +1257,7 @@ function runExternalAgent(agentSpec, params, emit = () => {}, options = {}) {
1226
1257
  });
1227
1258
 
1228
1259
  child.stdin.end(JSON.stringify(params));
1229
- });
1260
+ }));
1230
1261
  }
1231
1262
 
1232
1263
  function parsePositiveInteger(value, fallback) {
@@ -1299,27 +1330,19 @@ function errorResponse(id, code, message, details = {}) {
1299
1330
  };
1300
1331
  }
1301
1332
 
1302
- function getActiveNativeWorkState() {
1333
+ function getActiveNativeWorkState(env = process.env) {
1303
1334
  return {
1304
1335
  projectLocks: activeProjectLocks.size,
1336
+ processStopBarriers: processStopBarrier.getBlockedProjectCount(env.CODEX_OVERLEAF_MIRROR_ROOT, { env }),
1305
1337
  runControllers: activeRunControllers.size,
1306
1338
  activeTurnControls: Array.from(activeRunEntries.values()).filter(entry => entry.control).length
1307
1339
  };
1308
1340
  }
1309
1341
 
1310
- function abortAllActiveOperations(reason = 'Native messaging transport disconnected.') {
1311
- const error = new Error(String(reason || 'Native messaging transport disconnected.'));
1312
- error.code = 'native_transport_closed';
1313
- for (const controller of activeRunControllers.values()) {
1314
- if (controller && !controller.signal?.aborted) {
1315
- controller.abort(error);
1316
- }
1317
- }
1318
- }
1319
-
1320
1342
  module.exports = {
1321
1343
  NATIVE_REQUEST_QUOTAS,
1322
- abortAllActiveOperations,
1344
+ abortAllActiveOperations: runLifecycle.abortAllActiveOperations,
1345
+ quarantineActiveOperations: runLifecycle.quarantineActiveOperations,
1323
1346
  buildDefaultTaskResult,
1324
1347
  getActiveNativeWorkState,
1325
1348
  handleRequest,
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ // Preserve TOML source text while finding complete logical statements. This is
4
+ // a lexical boundary scanner, not a TOML value/schema parser. Filters must never
5
+ // treat an apparent key or table header inside a multiline value as syntax.
6
+ function splitTomlStatements(content) {
7
+ const text = String(content || '').replace(/\r\n/g, '\n');
8
+ const statements = [];
9
+ const containers = [];
10
+ let start = 0;
11
+ let quote = '';
12
+ let multiline = false;
13
+ let comment = false;
14
+
15
+ for (let index = 0; index < text.length; index++) {
16
+ const char = text[index];
17
+ if (comment) {
18
+ if (char !== '\n') continue;
19
+ comment = false;
20
+ }
21
+ if (quote) {
22
+ if (quote === '"' && char === '\\') {
23
+ // Basic strings escape the next character, including the first quote
24
+ // of a potential delimiter. Literal strings have no escape syntax.
25
+ index++;
26
+ } else if (char === quote) {
27
+ if (!multiline) {
28
+ quote = '';
29
+ } else if (text.slice(index, index + 3) === quote.repeat(3)) {
30
+ // A legal closing delimiter can have one or two literal quotes just
31
+ // before it: consume all four/five instead of opening another string.
32
+ let count = 3;
33
+ while (count < 5 && text[index + count] === quote) count++;
34
+ index += count - 1;
35
+ quote = '';
36
+ }
37
+ }
38
+ continue;
39
+ }
40
+ if (char === '#') {
41
+ comment = true;
42
+ } else if (char === '"' || char === "'") {
43
+ quote = char;
44
+ multiline = text.slice(index, index + 3) === char.repeat(3);
45
+ if (multiline) index += 2;
46
+ } else if (char === '[' || char === '{') {
47
+ containers.push(char);
48
+ } else if (char === ']' || char === '}') {
49
+ const opener = char === ']' ? '[' : '{';
50
+ if (containers.pop() !== opener) throw incompleteValue();
51
+ } else if (char === '\n' && containers.length === 0) {
52
+ statements.push(text.slice(start, index + 1));
53
+ start = index + 1;
54
+ }
55
+ }
56
+
57
+ // Do not silently remove the remaining config if a selected value is broken.
58
+ // The caller prepares the full string before writing the destination file.
59
+ if (quote || containers.length) throw incompleteValue();
60
+ if (start < text.length) statements.push(text.slice(start));
61
+ return statements;
62
+ }
63
+
64
+ function incompleteValue() {
65
+ return new Error('Codex configuration contains an unterminated or mismatched TOML value.');
66
+ }
67
+
68
+ // Parse only a statement's key path, leaving its value and original formatting
69
+ // untouched. A quoted dot belongs to a key; an unquoted dot separates keys.
70
+ function readTomlStatementPath(statement) {
71
+ const text = String(statement || '');
72
+ let index = 0;
73
+ const whitespace = () => { while (text[index] === ' ' || text[index] === '\t') index++; };
74
+ whitespace();
75
+ if (!text[index] || text[index] === '#' || text[index] === '\n') return null;
76
+ const table = text[index] === '[';
77
+ let brackets = 0;
78
+ if (table) {
79
+ brackets = text[index + 1] === '[' ? 2 : 1;
80
+ index += brackets;
81
+ }
82
+ const keys = [];
83
+ while (true) {
84
+ whitespace();
85
+ let key = '';
86
+ const quote = text[index];
87
+ if (quote === '"' || quote === "'") {
88
+ index++;
89
+ while (index < text.length && text[index] !== quote) {
90
+ const char = text[index++];
91
+ if (char === '\n' || char === '\r') throw invalidKey();
92
+ if (quote !== '"' || char !== '\\') { key += char; continue; }
93
+ const escape = text[index++];
94
+ const simple = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' };
95
+ if (Object.prototype.hasOwnProperty.call(simple, escape)) { key += simple[escape]; continue; }
96
+ const count = escape === 'u' ? 4 : escape === 'U' ? 8 : 0;
97
+ const hex = text.slice(index, index + count);
98
+ if (!count || hex.length !== count || !/^[0-9a-f]+$/i.test(hex)) throw invalidKey();
99
+ const point = Number.parseInt(hex, 16);
100
+ if (point > 0x10ffff || (point >= 0xd800 && point <= 0xdfff)) throw invalidKey();
101
+ key += String.fromCodePoint(point);
102
+ index += count;
103
+ }
104
+ if (text[index++] !== quote) throw invalidKey();
105
+ } else {
106
+ const match = text.slice(index).match(/^[A-Za-z0-9_-]+/);
107
+ if (!match) return null;
108
+ key = match[0]; index += key.length;
109
+ }
110
+ keys.push(key);
111
+ whitespace();
112
+ if (text[index] !== '.') break;
113
+ index++;
114
+ }
115
+ if (table) {
116
+ if (text.slice(index, index + brackets) !== ']'.repeat(brackets)) return null;
117
+ index += brackets; whitespace();
118
+ if (text[index] && text[index] !== '#' && text[index] !== '\n') return null;
119
+ } else if (text[index] !== '=') return null;
120
+ return { table, keys, ...(table ? {} : { valueStart: index + 1 }) };
121
+ }
122
+
123
+ function filterTomlRootKeys(content, excludedKeys) {
124
+ const excluded = new Set(excludedKeys);
125
+ const output = [];
126
+ let section = [];
127
+ for (const statement of splitTomlStatements(content)) {
128
+ const parsed = readTomlStatementPath(statement);
129
+ if (parsed?.table) section = parsed.keys;
130
+ const root = section.length ? section[0] : parsed?.keys[0];
131
+ if (!excluded.has(root)) output.push(statement);
132
+ }
133
+ return output.join('');
134
+ }
135
+
136
+ function invalidKey() {
137
+ return new Error('Codex configuration contains an invalid TOML key.');
138
+ }
139
+
140
+ module.exports = { splitTomlStatements, filterTomlRootKeys, readTomlStatementPath };