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
@@ -7,10 +7,12 @@ const { version: PACKAGE_VERSION } = require('../../package.json');
7
7
  const { SUBAGENT_QUEUE_DIR, collectMirrorChangesDetailed, getProjectMirror, markMirrorDirty, syncOverleafToMirror } = require('./mirrorWorkspace');
8
8
  const { computeLineDiff } = require('./diffEngine');
9
9
  const { prepareSelectionScope, filterSelectionChanges } = require('./selectionScope');
10
+ const { normalizeSafeProjectPath } = require('../../extension/src/shared/projectFiles');
10
11
  const WritingStyleRuntime = require('./writingStyleRuntime');
11
12
  const { createRecoveringStyleRunner } = require('./codexModelRecovery');
12
13
  const { computeTextPatches } = require('./textPatch');
13
14
  const { buildCodexHomeEnv } = require('./codexHome');
15
+ const { applyCodexSkillIsolation } = require('./codexSkillIsolation');
14
16
  const { buildCodexSpeedArgs } = require('./codexArgs');
15
17
  const { truncateText } = require('./debugLog');
16
18
  const {
@@ -31,6 +33,7 @@ const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch
31
33
  const { buildReadProgressRules, createReadProgressController, createRunEventScope } = require('./readProgressGuard');
32
34
  const {
33
35
  createCodexIdleWatchdog,
36
+ createProcessStopIdentity,
34
37
  createOptionalTimeout,
35
38
  getAbortReason,
36
39
  parseOptionalPositiveInteger,
@@ -43,13 +46,13 @@ const PARALLEL_SUBAGENTS_SKILL_ID = 'parallel-subagents';
43
46
 
44
47
  const { materializeTurnAttachments } = require('./turnAttachments');
45
48
 
46
- async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, providerLaunch, signal, onControlReady, onStopUnconfirmed } = {}) {
49
+ async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, providerLaunch, signal, onControlReady, onStopUnconfirmed, onProcessStarted } = {}) {
47
50
  throwIfAborted(signal);
48
51
  if (params.writingStyleBuild) {
49
52
  return WritingStyleRuntime.build({
50
53
  params, env, signal, providerLaunch, onControlReady, onStopUnconfirmed,
51
54
  execute: createRecoveringStyleRunner({
52
- execute: executeCodex || runCodexAppServerSession,
55
+ execute: input => (executeCodex || runCodexAppServerSession)({ ...input, onProcessStarted }),
53
56
  env, emit, locale: params.locale
54
57
  })
55
58
  });
@@ -81,15 +84,20 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
81
84
  });
82
85
  throwIfAborted(signal);
83
86
 
84
- emitCodexEvent(emit, 'overleaf.sync.completed', 'Overleaf project synced to local workspace', {
87
+ emitCodexEvent(emit, 'overleaf.sync.completed', mirror.skippedFiles?.length
88
+ ? 'Readable Overleaf files synced; some asset contents are unavailable'
89
+ : 'Overleaf project synced to local workspace', {
85
90
  projectId: mirror.projectKey,
86
91
  workspacePath: mirror.workspacePath,
87
- fileCount: mirror.fileCount
92
+ fileCount: mirror.fileCount,
93
+ contentComplete: mirror.contentComplete !== false,
94
+ skippedFiles: mirror.skippedFiles || []
88
95
  }, 'completed');
89
96
  }
90
-
91
97
  if (skillInstallTurn && params.selectionContext) throw new Error('Remove the selected text before installing skills.');
92
98
  const selectionScope = prepareSelectionScope(params.selectionContext, mirror.workspacePath, projectId);
99
+ if (selectionScope?.rebased) emitCodexEvent(emit, 'overleaf.selection.rebased', 'The unchanged selection was mapped to the current document.',
100
+ { path: selectionScope.selection.path, from: selectionScope.selection.from, to: selectionScope.selection.to, original: selectionScope.original }, 'info');
93
101
  const projectLocalSkills = loadProjectLocalSkillsContext(params, mirror);
94
102
  if (projectLocalSkills.missing.length) {
95
103
  emitCodexEvent(emit, 'codex.local_skills.missing', 'Selected project-local skills were missing', {
@@ -99,12 +107,15 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
99
107
  const turnAttachments = materializeTurnAttachments(params.attachments, mirror.workspacePath);
100
108
  const settings = buildCodexSettings(params);
101
109
  const skillLoading = normalizeSkillLoadingSettings(params);
102
- const codexSkillInvocationContext = loadCodexSkillInvocationContext({
110
+ const codexSkillInvocationContext = await loadCodexSkillInvocationContext({
103
111
  skillInvocation,
112
+ loadCodexLocalSkills: skillLoading.loadCodexLocalSkills,
113
+ enabledCodexLocalSkillIds: params.enabledCodexLocalSkillIds,
104
114
  loadCodexOverleafSkills: skillLoading.loadCodexOverleafSkills,
105
115
  enabledCodexOverleafSkillIds: params.enabledCodexOverleafSkillIds,
106
116
  env,
107
- emit
117
+ emit,
118
+ signal
108
119
  });
109
120
  const effectiveSkillInvocation = getEffectiveSkillInvocation(codexSkillInvocationContext);
110
121
  const runnerWorkspacePath = skillInstallTurn
@@ -116,7 +127,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
116
127
  const execute = executeCodex || runCodexAppServerSession;
117
128
  let unconfirmedStopError = null;
118
129
  const runner = async input => {
119
- try { return await execute(input); }
130
+ try { return await execute({ ...input, onProcessStarted }); }
120
131
  catch (error) {
121
132
  if (error?.code === 'codex_process_stop_unconfirmed') {
122
133
  unconfirmedStopError = error;
@@ -125,11 +136,12 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
125
136
  throw error;
126
137
  }
127
138
  };
128
- // Parallel-subagents broker (v1.6): activated solely by the official skill
129
- // being enabled for this run. Workers are sibling Codex runs spawned by the
139
+ // The file broker is available only to writable runs with the official
140
+ // skill enabled. Ask keeps its existing read-only sandbox. Workers are sibling Codex runs spawned by the
130
141
  // host (fresh sandbox — never nested), confined to the same mirror; the
131
142
  // broker enforces file ownership and wall-clock deadlines (spec §5-§7).
132
143
  const subagentBroker = !skillInstallTurn
144
+ && settings.sandboxMode === 'workspace-write'
133
145
  && skillLoading.loadCodexOverleafSkills !== false
134
146
  && Array.isArray(params.enabledCodexOverleafSkillIds)
135
147
  && params.enabledCodexOverleafSkillIds.includes(PARALLEL_SUBAGENTS_SKILL_ID)
@@ -150,6 +162,8 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
150
162
  speedTier: normalizeSpeedTier(params.speedTier),
151
163
  loadCodexLocalSkills: skillLoading.loadCodexLocalSkills,
152
164
  loadCodexOverleafSkills: skillLoading.loadCodexOverleafSkills,
165
+ enabledCodexOverleafSkillIds: params.enabledCodexOverleafSkillIds,
166
+ enabledCodexLocalSkillIds: params.enabledCodexLocalSkillIds,
153
167
  skillInvocation: null,
154
168
  installCodexOverleafSkillsTarget: false,
155
169
  projectLocalSkills: null,
@@ -195,6 +209,8 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
195
209
  speedTier: normalizeSpeedTier(params.speedTier),
196
210
  loadCodexLocalSkills: skillLoading.loadCodexLocalSkills,
197
211
  loadCodexOverleafSkills: skillLoading.loadCodexOverleafSkills,
212
+ enabledCodexOverleafSkillIds: params.enabledCodexOverleafSkillIds,
213
+ enabledCodexLocalSkillIds: params.enabledCodexLocalSkillIds,
198
214
  skillInvocation: effectiveSkillInvocation,
199
215
  installCodexOverleafSkillsTarget: skillInstallTurn,
200
216
  projectLocalSkills: null,
@@ -243,6 +259,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
243
259
  const filteredChanges = filterSyncChangesForFocus({
244
260
  changes: selectionChanges.changes,
245
261
  focusFiles: params.focusFiles || params.session?.focusFiles,
262
+ focusFilesAreLiteral: params.focusFilesAreLiteral === true,
246
263
  restrictToFocusFiles: params.restrictToFocusFiles
247
264
  });
248
265
  let rawSyncChanges = filteredChanges.changes;
@@ -257,13 +274,14 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
257
274
  // syncChanges would write directly.
258
275
  const subagentViolationPaths = subagentBroker ? subagentBroker.getViolationPaths() : null;
259
276
  if (subagentViolationPaths && subagentViolationPaths.size) {
260
- const blocked = rawSyncChanges.filter(change => subagentViolationPaths.has(change.path));
261
- rawSyncChanges = rawSyncChanges.filter(change => !subagentViolationPaths.has(change.path));
277
+ const violatedPath = change => [change.path, change.to].find(file => file && subagentViolationPaths.has(file));
278
+ const blocked = rawSyncChanges.filter(change => violatedPath(change));
279
+ rawSyncChanges = rawSyncChanges.filter(change => !violatedPath(change));
262
280
  for (const change of blocked) {
263
281
  unsupportedChanges.push({
264
282
  type: 'unsupported-local-file',
265
283
  path: change.path,
266
- reason: subagentBroker.getBlockedReason(change.path)
284
+ reason: subagentBroker.getBlockedReason(violatedPath(change))
267
285
  });
268
286
  }
269
287
  }
@@ -324,7 +342,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
324
342
  files: response.syncChanges.map(change => change.path),
325
343
  unsupportedCount: response.unsupportedChanges.length,
326
344
  unsupportedFiles: response.unsupportedChanges.map(change => change.path)
327
- }, 'completed');
345
+ }, response.unsupportedChanges.length ? 'warning' : 'completed');
328
346
 
329
347
  return response;
330
348
  }
@@ -344,11 +362,11 @@ function buildCodexTurnPrompt(params = {}, mirror = {}, projectLocalSkills, turn
344
362
 
345
363
 
346
364
 
347
- function normalizeFocusFiles(value) {
365
+ function normalizeFocusFiles(value, literal = false) {
348
366
  const seen = new Set();
349
367
  const files = [];
350
368
  for (const item of Array.isArray(value) ? value : []) {
351
- const filePath = normalizeProjectPath(item);
369
+ const filePath = literal ? normalizeSafeProjectPath(item) : normalizeProjectPath(item);
352
370
  if (!filePath || seen.has(filePath)) {
353
371
  continue;
354
372
  }
@@ -360,20 +378,20 @@ function normalizeFocusFiles(value) {
360
378
  }
361
379
  return files;
362
380
  }
363
-
364
- function filterSyncChangesForFocus({ changes = [], focusFiles = [], restrictToFocusFiles = false } = {}) {
381
+ function filterSyncChangesForFocus({ changes = [], focusFiles = [], restrictToFocusFiles = false, focusFilesAreLiteral = false } = {}) {
365
382
  if (!restrictToFocusFiles) {
366
383
  return { changes, unsupportedChanges: [] };
367
384
  }
368
- const focusSet = new Set(normalizeFocusFiles(focusFiles));
369
- if (!focusSet.size) {
385
+ const focusSet = new Set(normalizeFocusFiles(focusFiles, focusFilesAreLiteral === true));
386
+ if (!focusSet.size && focusFilesAreLiteral !== true) {
370
387
  return { changes, unsupportedChanges: [] };
371
388
  }
372
389
 
373
390
  const accepted = [];
374
391
  const rejected = [];
375
392
  for (const change of changes || []) {
376
- if (focusSet.has(normalizeProjectPath(change?.path))) {
393
+ if (focusSet.has(normalizeSafeProjectPath(change?.path))
394
+ && (!change?.to || focusSet.has(normalizeSafeProjectPath(change.to)))) {
377
395
  accepted.push(change);
378
396
  } else if (change?.path) {
379
397
  rejected.push({
@@ -388,7 +406,6 @@ function filterSyncChangesForFocus({ changes = [], focusFiles = [], restrictToFo
388
406
  unsupportedChanges: rejected
389
407
  };
390
408
  }
391
-
392
409
  function normalizeProjectPath(value) {
393
410
  return String(value || '')
394
411
  .replace(/^@file:/i, '')
@@ -418,12 +435,15 @@ function normalizeSkillLoadingSettings(params = {}) {
418
435
  };
419
436
  }
420
437
 
421
- function loadCodexSkillInvocationContext({
438
+ async function loadCodexSkillInvocationContext({
422
439
  skillInvocation,
440
+ loadCodexLocalSkills = true,
441
+ enabledCodexLocalSkillIds,
423
442
  loadCodexOverleafSkills = true,
424
443
  enabledCodexOverleafSkillIds,
425
444
  env = process.env,
426
- emit = () => {}
445
+ emit = () => {},
446
+ signal
427
447
  } = {}) {
428
448
  const invocation = normalizeSkillInvocation(skillInvocation);
429
449
  if (!invocation) {
@@ -433,19 +453,37 @@ function loadCodexSkillInvocationContext({
433
453
  return { invocation, skill: null, missing: [], ignored: [] };
434
454
  }
435
455
 
436
- const result = loadSelectedCodexOverleafSkill({
456
+ const local = invocation.scope === 'codex-local';
457
+ const result = local ? await require('./codexLocalSkills').loadSelectedCodexLocalSkill({
458
+ skillId: invocation.id,
459
+ loadCodexLocalSkills,
460
+ enabledCodexLocalSkillIds,
461
+ env,
462
+ signal
463
+ }).catch(error => {
464
+ if (signal?.aborted || (!String(error?.code || '').startsWith('codex_local_skills_')
465
+ && error?.code !== 'codex_not_found')) throw error;
466
+ throw Object.assign(new Error('The selected local Codex skill is unavailable. Refresh the skill list and select it again.'),
467
+ { code: 'codex_local_skill_unavailable', cause: error });
468
+ }) : loadSelectedCodexOverleafSkill({
437
469
  skillId: invocation.id,
438
470
  loadCodexOverleafSkills,
439
471
  enabledCodexOverleafSkillIds,
440
472
  env
441
473
  });
474
+ if (local && !result.skill) {
475
+ throw Object.assign(new Error('The selected local Codex skill is unavailable. Refresh the skill list and select it again.'),
476
+ { code: 'codex_local_skill_unavailable' });
477
+ }
442
478
  if (result.missing.length) {
443
- emitCodexEvent(emit, 'codex.overleaf_skills.missing', 'Selected Codex Overleaf skill was missing', {
479
+ emitCodexEvent(emit, local ? 'codex.local_skills.missing' : 'codex.overleaf_skills.missing',
480
+ local ? 'Selected local Codex skill was missing' : 'Selected Codex Overleaf skill was missing', {
444
481
  missingSkillIds: result.missing
445
482
  }, 'failed');
446
483
  }
447
484
  if (result.ignored.length) {
448
- emitCodexEvent(emit, 'codex.overleaf_skill_invocation.ignored', 'Selected Codex Overleaf skill was ignored', {
485
+ emitCodexEvent(emit, local ? 'codex.local_skill_invocation.ignored' : 'codex.overleaf_skill_invocation.ignored',
486
+ local ? 'Selected local Codex skill was ignored' : 'Selected Codex Overleaf skill was ignored', {
449
487
  ignoredSkillIds: result.ignored.map(item => item.id),
450
488
  reason: result.ignored[0]?.reason || 'ignored'
451
489
  }, 'warning');
@@ -476,17 +514,18 @@ function normalizeSkillInvocation(value) {
476
514
  return null;
477
515
  }
478
516
  const title = String(value?.title || 'Skill Installer').trim().slice(0, 80) || 'Skill Installer';
479
- if (id === 'skill-installer') {
517
+ if (id === 'skill-installer' && (!value?.scope || value.scope === 'builtin')) {
480
518
  return { id, title };
481
519
  }
482
- if (value?.scope !== 'codex-overleaf') {
520
+ if (!['codex-overleaf', 'codex-local'].includes(value?.scope)) {
483
521
  return null;
484
522
  }
485
- return { id, title, scope: 'codex-overleaf' };
523
+ return { id, title, scope: value.scope };
486
524
  }
487
525
 
488
526
  function isSkillInstallerInvocation(value) {
489
- return normalizeSkillInvocation(value)?.id === 'skill-installer';
527
+ const invocation = normalizeSkillInvocation(value);
528
+ return invocation?.id === 'skill-installer' && !invocation.scope;
490
529
  }
491
530
 
492
531
  function isSafeSkillId(id) {
@@ -551,146 +590,6 @@ function buildCodexAppServerArgs(input = {}) {
551
590
  ];
552
591
  }
553
592
 
554
- async function applyCodexSkillIsolation({ input = {}, childEnv = process.env, request, emit = () => {} } = {}) {
555
- if (input.loadCodexLocalSkills !== false) {
556
- return { disabled: [] };
557
- }
558
- if (typeof request !== 'function') {
559
- throw new Error('Codex skill isolation requires an app-server request function');
560
- }
561
-
562
- const listResult = await request('skills/list', {
563
- cwd: input.workspacePath,
564
- includeDisabled: true
565
- });
566
- const skills = flattenCodexSkillsList(listResult);
567
- const disabled = [];
568
- for (const skill of skills) {
569
- if (skill?.enabled === false || !shouldDisableCodexSkillForIsolation(skill, input, childEnv)) {
570
- continue;
571
- }
572
- const params = buildSkillDisableParams(skill);
573
- if (!params) {
574
- continue;
575
- }
576
- await request('skills/config/write', params);
577
- disabled.push(String(skill.name || skill.path || '').trim());
578
- }
579
- if (disabled.length) {
580
- emitCodexEvent(emit, 'codex.skill_isolation.applied', 'Disabled non-Overleaf Codex skills for this turn', {
581
- disabledSkillNames: disabled.filter(Boolean)
582
- }, 'completed');
583
- }
584
- return { disabled };
585
- }
586
-
587
- // Recursion guard (spec S6): disable specific Codex-Overleaf skills for one
588
- // app-server child process — used to strip `parallel-subagents` from worker
589
- // runs so an inheriting worker cannot discover the queue and fan out again.
590
- // Skill directories are named by skill id (<root>/<id>/SKILL.md).
591
- async function applyWorkerSkillStrip({ input = {}, request } = {}) {
592
- const ids = Array.isArray(input.disableCodexOverleafSkillIds)
593
- ? input.disableCodexOverleafSkillIds.filter(Boolean)
594
- : [];
595
- if (!ids.length || typeof request !== 'function') {
596
- return { disabled: [] };
597
- }
598
- const listResult = await request('skills/list', {
599
- cwd: input.workspacePath,
600
- includeDisabled: true
601
- });
602
- const disabled = [];
603
- for (const skill of flattenCodexSkillsList(listResult)) {
604
- if (skill?.enabled === false) {
605
- continue;
606
- }
607
- const skillDirName = path.basename(path.dirname(String(skill?.path || '')));
608
- if (!ids.includes(skillDirName)) {
609
- continue;
610
- }
611
- const params = buildSkillDisableParams(skill);
612
- if (!params) {
613
- continue;
614
- }
615
- await request('skills/config/write', params);
616
- disabled.push(skillDirName);
617
- }
618
- return { disabled };
619
- }
620
-
621
- function flattenCodexSkillsList(listResult = {}) {
622
- const data = Array.isArray(listResult?.data) ? listResult.data : [];
623
- return data.flatMap(entry => Array.isArray(entry?.skills) ? entry.skills : []);
624
- }
625
-
626
- function shouldDisableCodexSkillForIsolation(skill = {}, input = {}, childEnv = process.env) {
627
- if (isCodexSystemSkill(skill)) {
628
- return !isAllowedSystemSkillForIsolation(skill, input);
629
- }
630
- return !isAllowedCodexOverleafSkillPath(skill.path, input, childEnv);
631
- }
632
-
633
- function isCodexSystemSkill(skill = {}) {
634
- return String(skill.scope || '') === 'system' || isSystemSkillPath(skill.path);
635
- }
636
-
637
- function isAllowedSystemSkillForIsolation(skill = {}, input = {}) {
638
- return input.installCodexOverleafSkillsTarget === true && String(skill.name || '') === 'skill-installer';
639
- }
640
-
641
- function isAllowedCodexOverleafSkillPath(skillPath, input = {}, childEnv = process.env) {
642
- if (input.loadCodexOverleafSkills === false && input.installCodexOverleafSkillsTarget !== true) {
643
- return false;
644
- }
645
- const pathText = String(skillPath || '');
646
- if (!pathText || !path.isAbsolute(pathText) || isSystemSkillPath(pathText)) {
647
- return false;
648
- }
649
- const roots = [
650
- path.join(String(childEnv.CODEX_HOME || ''), 'skills'),
651
- getCodexOverleafSkillsRoot({ env: childEnv })
652
- ].filter(Boolean);
653
- return roots.some(root => isInsideOrSamePath(pathText, root));
654
- }
655
-
656
- function isSystemSkillPath(skillPath) {
657
- return String(skillPath || '').split(path.sep).includes('.system');
658
- }
659
-
660
- function buildSkillDisableParams(skill = {}) {
661
- const name = String(skill.name || '').trim();
662
- if (isCodexSystemSkill(skill) && name) {
663
- return { name, enabled: false };
664
- }
665
- const skillPath = String(skill.path || '').trim();
666
- if (path.isAbsolute(skillPath)) {
667
- return { path: skillPath, enabled: false };
668
- }
669
- if (name) {
670
- return { name, enabled: false };
671
- }
672
- return null;
673
- }
674
-
675
- function isInsideOrSamePath(target, root) {
676
- const targetPaths = comparablePaths(target);
677
- const rootPaths = comparablePaths(root);
678
- return targetPaths.some(targetPath => rootPaths.some(rootPath => (
679
- targetPath === rootPath || targetPath.startsWith(rootPath + path.sep)
680
- )));
681
- }
682
-
683
- function comparablePaths(value) {
684
- const resolved = path.resolve(String(value || ''));
685
- const candidates = [resolved];
686
- try {
687
- candidates.push(fs.realpathSync.native(resolved));
688
- } catch (_) {
689
- // Fall back to the lexical path when the file is not present yet.
690
- }
691
- return Array.from(new Set(candidates));
692
- }
693
-
694
593
  function buildTurnStartParams(input = {}, threadId = input.threadId || '') {
695
594
  const params = {
696
595
  threadId,
@@ -731,6 +630,10 @@ function runCodexAppServerProcess(input) {
731
630
  return;
732
631
  }
733
632
  const childEnv = applyProviderEnvironment(buildCodexHomeEnv(input.env || process.env, {
633
+ isolateRuntimeHome: true,
634
+ enabledCodexOverleafSkillIds: input.enabledCodexOverleafSkillIds,
635
+ enabledCodexLocalSkillIds: input.enabledCodexLocalSkillIds,
636
+ disableCodexOverleafSkillIds: input.disableCodexOverleafSkillIds,
734
637
  loadCodexLocalSkills: input.loadCodexLocalSkills !== false,
735
638
  loadCodexOverleafSkills: input.loadCodexOverleafSkills !== false,
736
639
  installCodexOverleafSkillsTarget: input.installCodexOverleafSkillsTarget === true,
@@ -824,6 +727,8 @@ function runCodexAppServerProcess(input) {
824
727
  fail(new Error(stderr || `codex app-server exited before turn completed with code ${code}`));
825
728
  });
826
729
 
730
+ try { input.onProcessStarted?.(createProcessStopIdentity(child, { processGroup: process.platform !== 'win32' })); }
731
+ catch (error) { fail(error); return; }
827
732
  start().catch(fail);
828
733
 
829
734
  async function start() {
@@ -841,10 +746,6 @@ function runCodexAppServerProcess(input) {
841
746
  request,
842
747
  emit: input.emit
843
748
  });
844
- await applyWorkerSkillStrip({
845
- input,
846
- request
847
- });
848
749
 
849
750
  if (input.threadId) {
850
751
  try {
@@ -934,7 +835,9 @@ function runCodexAppServerProcess(input) {
934
835
  }
935
836
  pending.delete(message.id);
936
837
  if (message.error) {
937
- pendingRequest.reject(new Error(message.error.message || JSON.stringify(message.error)));
838
+ const error = new Error(message.error.message || JSON.stringify(message.error));
839
+ if (typeof message.error.code === 'number' || typeof message.error.code === 'string') error.code = message.error.code;
840
+ pendingRequest.reject(error);
938
841
  } else {
939
842
  if (pendingRequest.method === 'thread/start' || pendingRequest.method === 'thread/resume') {
940
843
  activeThreadId = message.result?.thread?.id || message.result?.threadId || pendingRequest.params?.threadId || activeThreadId;
@@ -974,7 +877,10 @@ function runCodexAppServerProcess(input) {
974
877
  if (message.method === 'item/completed' && eventScope.owns(message.params) &&
975
878
  !readProgressController.observe(message.params?.item || {})) return;
976
879
  if (message.method === 'turn/completed' && activeTurnId && (message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
977
- succeed(message.params?.turn || {});
880
+ const turn = message.params?.turn || {};
881
+ const terminalError = getTurnCompletionError(turn);
882
+ if (terminalError) fail(terminalError);
883
+ else succeed(turn);
978
884
  }
979
885
  if (message.method === 'error' && isTransientCodexAppServerError(message.params)) {
980
886
  return;
@@ -1083,17 +989,23 @@ function runCodexAppServerProcess(input) {
1083
989
  if (settled || stopping) {
1084
990
  return;
1085
991
  }
1086
- settled = true;
992
+ stopping = true;
1087
993
  const turnEndedError = new Error('Codex turn ended before the pending app-server request completed.');
1088
994
  turnEndedError.code = 'codex_turn_ended';
1089
995
  rejectPendingRequests(pending, turnEndedError);
1090
996
  cleanup();
1091
- child.kill('SIGTERM');
1092
- resolve({
997
+ const result = {
1093
998
  assistantMessage: buildFinalAssistantMessage(assistantMessages, assistantMessageOrder, {
1094
999
  turnCompleted: turn.status === undefined || turn.status === 'completed'
1095
1000
  }),
1096
1001
  threadId: activeThreadId
1002
+ };
1003
+ return stopCodexAppServer(child, { processGroup: process.platform !== 'win32' }).then(() => {
1004
+ settled = true;
1005
+ resolve(result);
1006
+ }, error => {
1007
+ settled = true;
1008
+ reject(error);
1097
1009
  });
1098
1010
  }
1099
1011
 
@@ -1404,6 +1316,10 @@ function emitCodexEvent(emit, type, title, detail = {}, status = 'running') {
1404
1316
  }
1405
1317
 
1406
1318
  function inferNotificationStatus(message) {
1319
+ if (message.method === 'turn/completed') {
1320
+ const error = getTurnCompletionError(message.params?.turn || {});
1321
+ if (error) return error.code === 'codex_cancelled' ? 'cancelled' : 'failed';
1322
+ }
1407
1323
  if (message.method === 'error' && isTransientCodexAppServerError(message.params)) {
1408
1324
  return 'warning';
1409
1325
  }
@@ -1413,6 +1329,19 @@ function inferNotificationStatus(message) {
1413
1329
  return 'running';
1414
1330
  }
1415
1331
 
1332
+ function getTurnCompletionError(turn) {
1333
+ const status = turn.status;
1334
+ // Older peers omit status; explicit error evidence still prevents success.
1335
+ if ((status === undefined || status === 'completed') && turn.error == null) return null;
1336
+ const interrupted = status === 'interrupted';
1337
+ const invalid = status !== undefined && !['completed', 'failed', 'interrupted'].includes(status);
1338
+ const error = new Error(extractCodexAppServerErrorMessage(turn) || (interrupted
1339
+ ? 'Codex turn was interrupted.' : invalid ? 'Codex turn ended with an invalid terminal status.' : 'Codex turn failed.'));
1340
+ error.code = interrupted ? 'codex_cancelled' : invalid ? 'codex_turn_status_invalid' : 'codex_turn_failed';
1341
+ error.turnStatus = status;
1342
+ return error;
1343
+ }
1344
+
1416
1345
  function isTransientCodexAppServerError(params = {}) {
1417
1346
  const message = extractCodexAppServerErrorMessage(params);
1418
1347
  return /^Reconnecting(?:\.\.\.|…)?\s+\d+\s*\/\s*\d+\.?$/i.test(message);
@@ -1446,9 +1375,7 @@ function isUnsupportedReasoningSummaryError(error) {
1446
1375
  return /unsupported_parameter/i.test(message) && /reasoning\.summary|summary/i.test(message);
1447
1376
  }
1448
1377
 
1449
- function cleanAssistantMessage(value) {
1450
- return String(value || '').trim();
1451
- }
1378
+ const cleanAssistantMessage = value => String(value || '').trim();
1452
1379
 
1453
1380
  module.exports = {
1454
1381
  applyCodexSkillIsolation,
@@ -41,20 +41,25 @@ function rejectPendingRequests(pending, error) {
41
41
  pending.clear();
42
42
  }
43
43
 
44
- async function stopCodexAppServer(child, {
45
- interrupt, reason, processGroup = false, platform = process.platform,
46
- interruptTimeoutMs = 1500, terminateTimeoutMs = 500, forceTimeoutMs = 1500,
47
- kill = process.kill.bind(process), taskkill = defaultTaskkill
48
- } = {}) {
44
+ function createProcessStopIdentity(child, { processGroup = false, platform = process.platform, kill = process.kill.bind(process) } = {}) {
49
45
  const pid = Number(child?.pid);
50
- if (!Number.isInteger(pid) || pid <= 0) return;
51
46
  const grouped = processGroup && platform !== 'win32';
52
47
  const exited = () => child.exitCode != null || child.signalCode != null;
53
48
  const groupGone = () => {
54
49
  if (!grouped) return true;
55
50
  try { kill(-pid, 0); return false; } catch (error) { return error?.code === 'ESRCH'; }
56
51
  };
57
- const stopped = () => exited() && groupGone();
52
+ return { processId: pid, processGroup: grouped, isStopped: () => exited() && groupGone() };
53
+ }
54
+
55
+ async function stopCodexAppServer(child, {
56
+ interrupt, reason, processGroup = false, platform = process.platform,
57
+ interruptTimeoutMs = 1500, terminateTimeoutMs = 500, forceTimeoutMs = 1500,
58
+ kill = process.kill.bind(process), taskkill = defaultTaskkill
59
+ } = {}) {
60
+ const { processId: pid, processGroup: grouped, isStopped: stopped } = createProcessStopIdentity(child, { processGroup, platform, kill });
61
+ if (!Number.isInteger(pid) || pid <= 0) return;
62
+ const exited = () => child.exitCode != null || child.signalCode != null;
58
63
  const waitForStop = async timeoutMs => {
59
64
  const deadline = Date.now() + timeoutMs;
60
65
  while (!stopped() && Date.now() < deadline) {
@@ -99,13 +104,18 @@ async function stopCodexAppServer(child, {
99
104
  }
100
105
 
101
106
  function defaultTaskkill(pid, timeout) {
107
+ const path = require('node:path');
108
+ const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || process.env.windir || process.env.WINDIR;
109
+ const command = systemRoot && path.win32.isAbsolute(systemRoot)
110
+ ? path.win32.join(systemRoot, 'System32', 'taskkill.exe') : 'taskkill';
102
111
  return new Promise((resolve, reject) => {
103
- require('node:child_process').execFile('taskkill', ['/pid', String(pid), '/t', '/f'],
112
+ require('node:child_process').execFile(command, ['/pid', String(pid), '/t', '/f'],
104
113
  { windowsHide: true, timeout }, error => error ? reject(error) : resolve());
105
114
  });
106
115
  }
107
116
 
108
117
  module.exports = {
118
+ createProcessStopIdentity,
109
119
  createCodexIdleWatchdog,
110
120
  createOptionalTimeout,
111
121
  getAbortReason,