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
@@ -20,6 +20,7 @@ const path = require('node:path');
20
20
 
21
21
  const { SUBAGENT_QUEUE_DIR } = require('./mirrorWorkspace');
22
22
  const { safeWorkspaceRelativePath } = require('./subagentWorkspacePath');
23
+ const { estimateWorkerWaves } = require('./subagentAdmission');
23
24
  const { compactEvent, publicText } = require('./subagentTelemetry');
24
25
  const { createSubagentWorkspace } = require('./subagentWorkspace');
25
26
 
@@ -46,7 +47,10 @@ function createSubagentBroker(options = {}) {
46
47
  if (typeof runWorkerTask !== 'function') {
47
48
  throw new Error('subagent broker requires a runWorkerTask function');
48
49
  }
49
- const emit = typeof options.emit === 'function' ? options.emit : () => {};
50
+ const emit = (...args) => {
51
+ try { if (typeof options.emit === 'function') options.emit(...args); }
52
+ catch (_error) { /* Observability cannot prevent worker cleanup. */ }
53
+ };
50
54
  const onMirrorDirty = typeof options.onMirrorDirty === 'function' ? options.onMirrorDirty : () => {};
51
55
  const parentSignal = options.signal || null;
52
56
  const env = options.env || process.env;
@@ -84,6 +88,8 @@ function createSubagentBroker(options = {}) {
84
88
  let anyWorkerStarted = false;
85
89
  let cancelled = false;
86
90
  let closed = false;
91
+ let brokerError = null;
92
+ let stopping = null;
87
93
 
88
94
  function start() {
89
95
  // Stale queue dirs from a previous run carry old results/logs — clear
@@ -129,11 +135,22 @@ function createSubagentBroker(options = {}) {
129
135
  intakeJobs();
130
136
  fillSlots();
131
137
  processAdoptions();
132
- } catch (_error) {
133
- // The queue is adversarial model output; the broker never throws on it.
138
+ } catch (error) {
139
+ recordFailure(error);
134
140
  }
135
141
  }
136
142
 
143
+ function recordFailure(error, jobId) {
144
+ accepting = false;
145
+ if (brokerError) return;
146
+ brokerError = Object.assign(new Error('Subagent broker results or cleanup could not be persisted.'), {
147
+ code: 'subagent_io_failed', cause: error
148
+ });
149
+ emit('codex.subagent.failed', 'Subagent result storage failed', {
150
+ jobId, status: 'failed', reason: brokerError.message, code: brokerError.code
151
+ }, 'warning');
152
+ }
153
+
137
154
  function intakeJobs() {
138
155
  if (!accepting || !fs.existsSync(jobsDir)) {
139
156
  return;
@@ -177,11 +194,12 @@ function createSubagentBroker(options = {}) {
177
194
  }
178
195
 
179
196
  function idFromFileName(name) {
180
- return name.replace(/\.json$/, '').slice(0, 32) || 'unknown';
197
+ const id = name.replace(/\.json$/, '');
198
+ return JOB_ID_PATTERN.test(id) ? id : `rejected-${crypto.randomBytes(11).toString('hex')}`;
181
199
  }
182
200
 
183
201
  function jobIdOrFallback(job, name) {
184
- return typeof job?.id === 'string' && job.id ? job.id.slice(0, 32) : idFromFileName(name);
202
+ return typeof job?.id === 'string' && JOB_ID_PATTERN.test(job.id) ? job.id : idFromFileName(name);
185
203
  }
186
204
 
187
205
  function validateJob(job) {
@@ -214,13 +232,11 @@ function createSubagentBroker(options = {}) {
214
232
  }
215
233
  owned.push(safe);
216
234
  }
217
- // Overlapping ownership no longer rejects: same-file jobs are admitted
218
- // and SERIALIZED by the scheduler (fillSlots never runs two jobs whose
219
- // files intersect). Prompt-scoped same-file delegation is then safe —
220
- // the physical lost-update race only exists under concurrency (v1.6.1).
221
- // Wave-aware admission: the parent turn has no default absolute deadline,
222
- // so the broker enforces its own wall-clock envelope across ALL waves.
223
- const projectedWaves = Math.ceil((queued.length + running.size + 1) / limits.maxWorkers);
235
+ // Reserve serial waves for overlapping ownership, matching fillSlots.
236
+ // The budget covers worker admission; parent/OS cleanup has separate limits.
237
+ const projectedWaves = estimateWorkerWaves(
238
+ [...running.keys()].map(id => jobs.get(id).job.files),
239
+ [...queued.map(id => jobs.get(id).job.files), owned], limits.maxWorkers);
224
240
  if (Date.now() + projectedWaves * limits.perWorkerTimeoutMs > startedAt + limits.brokerBudgetMs) {
225
241
  return { reason: 'insufficient_time' };
226
242
  }
@@ -245,6 +261,10 @@ function createSubagentBroker(options = {}) {
245
261
  }
246
262
 
247
263
  function writeResult(result) {
264
+ // Rejection receipts cross the same filesystem boundary as accepted jobs.
265
+ if (typeof result.id !== 'string' || !JOB_ID_PATTERN.test(result.id)) {
266
+ throw new Error('Invalid subagent result ID');
267
+ }
248
268
  atomicWrite(path.join(resultsDir, `${result.id}.json`), JSON.stringify(result, null, 2));
249
269
  }
250
270
 
@@ -265,6 +285,11 @@ function createSubagentBroker(options = {}) {
265
285
  }
266
286
  const [jobId] = queued.splice(index, 1);
267
287
  const entry = jobs.get(jobId);
288
+ if (Date.now() + limits.perWorkerTimeoutMs > startedAt + limits.brokerBudgetMs) {
289
+ entry.status = 'rejected';
290
+ writeRejection({ id: jobId, reason: 'insufficient_time' });
291
+ continue;
292
+ }
268
293
  try {
269
294
  startWorker(entry);
270
295
  } catch (error) {
@@ -338,6 +363,7 @@ function createSubagentBroker(options = {}) {
338
363
  } catch (_error) { /* Observability must not fail or stop the worker. */ }
339
364
  }
340
365
  }).then(workerResult => {
366
+ if (controller.signal.aborted) throw controller.signal.reason || new Error('Subagent aborted');
341
367
  finishWorker(entry, {
342
368
  status: 'completed',
343
369
  summary: String(workerResult?.assistantMessage || '').slice(0, SUMMARY_LIMIT_CHARS),
@@ -345,21 +371,22 @@ function createSubagentBroker(options = {}) {
345
371
  }, workerResult);
346
372
  }).catch(error => {
347
373
  // Any broker-initiated abort that is not the wall-clock deadline
348
- // (parent cancel, drain) reads as 'cancelled'; only genuine worker
349
- // errors read as 'failed'.
350
- const status = timedOut ? 'timeout' : controller.signal.aborted ? 'cancelled' : 'failed';
374
+ // (parent cancel, drain), or an interrupted worker turn, is cancelled.
375
+ const status = timedOut ? 'timeout' : controller.signal.aborted || error?.code === 'codex_cancelled' ? 'cancelled' : 'failed';
351
376
  appendLog(job.id, `${status}: ${error?.stack || error?.message || String(error)}`);
352
377
  finishWorker(entry, { status, reason: error?.message || String(error) });
353
- }).finally(() => {
378
+ }).catch(error => recordFailure(error, job.id)).finally(() => {
354
379
  clearTimeout(deadlineTimer);
355
380
  running.delete(job.id);
356
381
  if (running.size === 0) {
357
382
  endWave();
358
383
  }
384
+ writeBrokerFile(closed ? 'closed' : 'ready');
359
385
  fillSlots();
360
- });
386
+ }).catch(error => recordFailure(error, job.id));
361
387
 
362
388
  running.set(job.id, { controller, deadlineTimer, promise });
389
+ try { writeBrokerFile('ready'); } catch (error) { recordFailure(error, job.id); }
363
390
  }
364
391
 
365
392
  function finishWorker(entry, resultFields, workerResult) {
@@ -451,7 +478,6 @@ function createSubagentBroker(options = {}) {
451
478
  // Workers have exited before this capability is published. A worker
452
479
  // cannot pre-authorize its own incomplete output while still running.
453
480
  adoptionToken = crypto.randomBytes(24).toString('hex');
454
- writeBrokerFile('ready');
455
481
  }
456
482
  }
457
483
 
@@ -498,69 +524,56 @@ function createSubagentBroker(options = {}) {
498
524
  for (const { controller } of running.values()) {
499
525
  controller.abort(new Error('run cancelled'));
500
526
  }
501
- for (const jobId of queued.splice(0)) {
502
- const entry = jobs.get(jobId);
503
- entry.status = 'cancelled';
504
- writeResult({ id: jobId, status: 'cancelled', reason: 'run cancelled' });
505
- emit('codex.subagent.failed', publicText(entry.job.title || jobId, 160), {
506
- jobId, status: 'cancelled', reason: 'run cancelled'
507
- }, 'warning');
508
- }
509
527
  if (anyWorkerStarted) {
510
- // Partial worker edits must be deterministically discarded: cancellation
511
- // already skips writeback, and the dirty mark forces the next run's
512
- // mirror sync to rebuild the workspace from Overleaf (spec P1-3 fix).
513
- onMirrorDirty();
528
+ try { onMirrorDirty(); } catch (error) { recordFailure(error); }
514
529
  }
515
- close();
530
+ // Event listeners cannot expose an async rejection. The same sticky stop
531
+ // promise is still awaited by the parent runner and reports any I/O error.
532
+ stop({ drain: false }).catch(() => {});
516
533
  }
517
534
 
518
- async function stop({ drain = true } = {}) {
519
- processAdoptions();
520
- accepting = false;
521
- // Queued-but-unstarted jobs would otherwise vanish with no result file,
522
- // hanging a lead still polling for its job count. Settle them first
523
- // (mirrors onParentAbort's queue handling).
535
+ function cancelQueued(reason) {
524
536
  for (const jobId of queued.splice(0)) {
525
537
  const entry = jobs.get(jobId);
526
- if (entry) {
527
- entry.status = 'cancelled';
528
- }
529
- writeResult({ id: jobId, status: 'cancelled', reason: 'The run ended before this subagent started.' });
538
+ if (entry) entry.status = 'cancelled';
539
+ try { writeResult({ id: jobId, status: 'cancelled', reason }); }
540
+ catch (error) { recordFailure(error, jobId); }
530
541
  emit('codex.subagent.failed', publicText(entry?.job?.title || jobId, 160), {
531
- jobId, status: 'cancelled', reason: 'The run ended before this subagent started.'
542
+ jobId, status: 'cancelled', reason
532
543
  }, 'warning');
533
544
  }
534
- if (drain && running.size) {
535
- let graceTimer = null;
536
- const grace = new Promise(resolve => {
537
- graceTimer = setTimeout(resolve, limits.drainGraceMs);
538
- });
539
- await Promise.race([
540
- Promise.allSettled([...running.values()].map(worker => worker.promise)),
541
- grace
542
- ]);
543
- // The grace timer gates an awaited race; if the workers settled first it
544
- // must be cleared or it keeps the event loop alive for drainGraceMs
545
- // (the "keep timers gated, then clear" discipline from c3cd357).
546
- if (graceTimer) {
547
- clearTimeout(graceTimer);
545
+ }
546
+
547
+ function stop({ drain = true } = {}) {
548
+ accepting = false;
549
+ if (!stopping) stopping = Promise.resolve().then(() => stopWorkers(drain));
550
+ return stopping.then(() => { if (brokerError) throw brokerError; });
551
+ }
552
+
553
+ async function stopWorkers(drain) {
554
+ accepting = false;
555
+ try {
556
+ try { processAdoptions(); } catch (error) { recordFailure(error); }
557
+ cancelQueued(cancelled ? 'run cancelled' : 'The run ended before this subagent started.');
558
+ if (drain && !cancelled && !brokerError) await settleRunningWithin(limits.drainGraceMs);
559
+ } finally {
560
+ for (const { controller } of running.values()) controller.abort(new Error('broker drained'));
561
+ // Keep bounded waiting for workers that ignore abort; their own deadline
562
+ // remains active until settlement. OS exit proof belongs to the runner.
563
+ try { await settleRunningWithin(limits.drainGraceMs); } finally {
564
+ for (const jobId of running.keys()) for (const file of jobs.get(jobId).job.files) {
565
+ quarantined.set(file, { jobId, status: 'cancelled', adoptedHash: '' });
566
+ }
567
+ close();
548
568
  }
549
569
  }
550
- for (const { controller } of running.values()) {
551
- controller.abort(new Error('broker drained'));
552
- }
553
- // Bounded final settle: a worker that ignores its abort must not hang
554
- // stop() forever — that strands codex.run and leaks the project lock (the
555
- // very zombie-lock case handleCodexCancel exists to recover from).
556
- await settleRunningWithin(limits.drainGraceMs);
557
570
  if (jobs.size) {
558
571
  emit('codex.subagent.drained', 'subagents drained', {
559
572
  jobs: jobs.size,
560
573
  violations: violations.length
561
- }, 'completed');
574
+ }, brokerError ? 'warning' : 'completed');
562
575
  }
563
- close();
576
+ if (brokerError) throw brokerError;
564
577
  }
565
578
 
566
579
  async function settleRunningWithin(timeoutMs) {
@@ -591,7 +604,7 @@ function createSubagentBroker(options = {}) {
591
604
  parentSignal?.removeEventListener?.('abort', onParentAbort);
592
605
  try {
593
606
  writeBrokerFile('closed');
594
- } catch (_error) { /* workspace may already be gone */ }
607
+ } catch (error) { recordFailure(error); }
595
608
  }
596
609
 
597
610
  return {
@@ -3,15 +3,14 @@
3
3
  // Observation only: never resumes a thread, starts a turn or settles a run.
4
4
  const CHILD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,159}$/;
5
5
  const MAX_CHILDREN = 16;
6
+ const MAX_HISTORY_READ_FAILURES = 3;
6
7
  const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
7
8
  const EVIDENCE = { history: 1, agent: 2, turn: 3 };
8
9
 
9
10
  function publicText(value, limit = 4096) {
10
- return String(value ?? '')
11
- .replace(/\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{12,}/g, '[REDACTED_SECRET]')
12
- .replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED_SECRET]')
13
- .replace(/((?:api[_-]?key|access[_-]?token|secret|password|authorization)\s*[:=]\s*["']?)[^\s"',;]+/gi, '$1[REDACTED_SECRET]')
14
- .slice(0, limit);
11
+ // Preserve the delegated task and model/tool text. The event allowlist
12
+ // excludes credential fields; metadataText separately hides local paths.
13
+ return String(value ?? '').slice(0, limit);
15
14
  }
16
15
 
17
16
  function fields(value, names, limit = 4096) {
@@ -185,7 +184,7 @@ function createNativeSubagentObserver({ emit, request, getRoot, ownsParent, isSt
185
184
  return false;
186
185
  }
187
186
  function snapshot(child, terminal = false) {
188
- if (!active() || child.readDisabled) return;
187
+ if (!active() || child.readDisabled || (child.readFailures || 0) >= MAX_HISTORY_READ_FAILURES) return;
189
188
  if (child.reading) { child.readAgain ||= terminal; return; }
190
189
  if (!terminal && Date.now() - child.lastRead < 2000) return;
191
190
  child.reading = true; child.lastRead = Date.now();
@@ -204,6 +203,11 @@ function createNativeSubagentObserver({ emit, request, getRoot, ownsParent, isSt
204
203
  if (!turn?.id || turn.id === getRoot().turnId
205
204
  || child.turnId && child.turnId !== turn.id) return;
206
205
  child.historyTurnId = turn.id;
206
+ child.readFailures = 0;
207
+ if (child.historyUnavailable) {
208
+ child.historyUnavailable = false;
209
+ publish(child, null, { historyUnavailable: false });
210
+ }
207
211
  const items = Array.isArray(turn.items) ? turn.items : [];
208
212
  // Recover real child input before compactEvent drops user content.
209
213
  // A fork's parent context and encrypted delegation are not task text.
@@ -231,11 +235,14 @@ function createNativeSubagentObserver({ emit, request, getRoot, ownsParent, isSt
231
235
  ? (interruptionObserved ? 'cancelled' : undefined) : turn.status;
232
236
  if (setStatus(child, next, EVIDENCE.history)) publish(child, null);
233
237
  }
234
- }).catch(() => {
238
+ }).catch(error => {
235
239
  if (!active() || child.generation !== generation) return;
236
- // Older app servers may lack this capability. Keep observed records;
237
- // reading history must never interrupt or resume the parent or child.
238
- child.readDisabled = true;
240
+ // Unsupported history stays disabled; other errors get a bounded retry
241
+ // only on later activity. Observation never interrupts or resumes work.
242
+ child.readDisabled = error?.code === -32601 || error?.code === '-32601'
243
+ || /(?:thread[ /]read.*(?:unsupported|not supported)|method (?:not found|not supported))/i.test(String(error?.message || ''));
244
+ child.readFailures = (child.readFailures || 0) + 1;
245
+ child.historyUnavailable = true;
239
246
  publish(child, null, { historyUnavailable: true });
240
247
  }).finally(() => {
241
248
  child.reading = false;
@@ -258,6 +265,8 @@ function createNativeSubagentObserver({ emit, request, getRoot, ownsParent, isSt
258
265
  if (known.retiredTurns.size > 100) known.retiredTurns.delete(known.retiredTurns.values().next().value);
259
266
  known.turnId = turnId;
260
267
  known.generation += 1;
268
+ known.readFailures = 0;
269
+ known.lastRead = -Infinity;
261
270
  known.statusEvidence = 0;
262
271
  known.interruptionObserved = false;
263
272
  }
@@ -7,14 +7,17 @@ const { SUBAGENT_QUEUE_DIR } = require('./mirrorWorkspace');
7
7
  if (typeof value !== 'string' || !value.trim()) {
8
8
  return null;
9
9
  }
10
- const normalized = value.trim().replace(/\\/g, '/').replace(/^\.\//, '');
10
+ const normalized = value.trim().replace(/\\/g, '/');
11
11
  if (!normalized || path.isAbsolute(normalized)) {
12
12
  return null;
13
13
  }
14
- const segments = normalized.split('/');
14
+ let segments = normalized.split('/');
15
15
  if (segments.some(segment => segment === '..' || segment === '')) {
16
16
  return null;
17
17
  }
18
+ // All consumers use the same identity, including control guards and locks.
19
+ segments = segments.filter(segment => segment !== '.');
20
+ if (!segments.length) return null;
18
21
  if (segments[0] === SUBAGENT_QUEUE_DIR) {
19
22
  // The queue control plane (jobs/results/logs/broker.json) is never
20
23
  // ownable — but the work/ scratch zone IS: single-file fan-out slices