@brunobracaioli/aibrain 0.2.0-beta.1 → 0.3.0-beta.7

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.
@@ -5,6 +5,9 @@ import { spawn } from 'node:child_process';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import { dirname, relative, resolve } from 'node:path';
7
7
  import { StringDecoder } from 'node:string_decoder';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { createTelemetryReporter, TELEMETRY_TIMEOUT_MS } from './context-telemetry-reporter.mjs';
10
+ import { preflightSelectionState } from './context-selection-fitting.mjs';
8
11
 
9
12
  const DEFAULT_THRESHOLD = 85;
10
13
  const COOLDOWN_MS = 60_000;
@@ -14,11 +17,72 @@ const MAX_CAPTURE_BYTES = 1 * 1024 * 1024;
14
17
  const MAX_OWNED_REQUESTS = 1_000;
15
18
  const MAX_RETIRED_REQUESTS = 2_000;
16
19
  const MAX_RECOVERY_ITEMS = 128;
20
+ const MAX_RECOVERY_SOURCE_INDEX = 4096;
17
21
  const MAX_NOTE_BYTES = 128 * 1024;
18
22
  const MAX_SELECTION_BYTES = 512 * 1024;
19
23
  const MAX_POLICY_BYTES = 8 * 1024;
20
24
  const MAX_NOTE_ITEM_CHARS = 16_384;
21
25
  const USAGE_FRESHNESS_MS = 60_000;
26
+ const MAX_DIAGNOSTIC_STRING = 128;
27
+ const MAX_BUDGET_SNAPSHOT_AGE_MS = 15_000;
28
+ const CONTEXT_ERROR_CODES = new Set([
29
+ 'INVALID_INPUT',
30
+ 'FEATURE_DISABLED',
31
+ 'PAID_PLAN_REQUIRED',
32
+ 'ENTITLEMENT_UNAVAILABLE',
33
+ 'RATE_LIMITED',
34
+ 'PROVIDER_NOT_CONFIGURED',
35
+ 'BUDGET_EXCEEDED',
36
+ 'PROVIDER_TIMEOUT',
37
+ 'PROVIDER_UNAVAILABLE',
38
+ 'INVALID_PROVIDER_RESPONSE',
39
+ ]);
40
+ const CONTEXT_FAILURE_DETAILS = new Set(['state_budget_exceeded', 'shared_admission_denied']);
41
+ const CONTEXT_ADMISSION_REASONS = new Set([
42
+ 'request_replayed',
43
+ 'account_request_limit',
44
+ 'account_token_limit',
45
+ 'global_request_limit',
46
+ 'global_token_limit',
47
+ 'account_in_flight',
48
+ 'database_unavailable',
49
+ ]);
50
+ const SHARED_QUOTA_ADMISSION_REASONS = new Set([
51
+ 'account_request_limit',
52
+ 'account_token_limit',
53
+ 'global_request_limit',
54
+ 'global_token_limit',
55
+ 'account_in_flight',
56
+ ]);
57
+ const DIAGNOSTIC_STAGES = new Set([
58
+ 'capture',
59
+ 'budget',
60
+ 'selection',
61
+ 'notes',
62
+ 'native',
63
+ 'recovery',
64
+ 'transport',
65
+ ]);
66
+ const DIAGNOSTIC_REASONS = new Set([
67
+ 'requested',
68
+ 'completed',
69
+ 'candidate_window_bounded',
70
+ 'candidate_over_bound',
71
+ 'protected_over_bound',
72
+ 'history_unavailable',
73
+ 'stale_snapshot',
74
+ 'unavailable',
75
+ 'admission_denied',
76
+ 'invalid_response',
77
+ 'request_failed',
78
+ 'source_changed',
79
+ 'timeout',
80
+ 'thread_closed',
81
+ 'protocol_error',
82
+ 'attached',
83
+ 'uncertain',
84
+ 'disconnected',
85
+ ]);
22
86
  const FITTING_STAGES = new Set([
23
87
  'original',
24
88
  'inputs_1000',
@@ -233,10 +297,13 @@ function isValidUsage(value) {
233
297
  isRecord(value) &&
234
298
  isValidBreakdown(value.last) &&
235
299
  isValidBreakdown(value.total) &&
236
- isSafeCounter(value.modelContextWindow) &&
237
- value.modelContextWindow > 0
300
+ (value.modelContextWindow === null ||
301
+ (isSafeCounter(value.modelContextWindow) && value.modelContextWindow > 0))
238
302
  );
239
303
  }
304
+ function hasKnownContextWindow(value) {
305
+ return isRecord(value) && isSafeCounter(value.modelContextWindow) && value.modelContextWindow > 0;
306
+ }
240
307
  function redact(value, limit = 16_384) {
241
308
  return String(value)
242
309
  .replace(/(authorization\s*[:=]\s*bearer\s+)[^\s]+/gi, '$1[REDACTED]')
@@ -247,6 +314,187 @@ function redact(value, limit = 16_384) {
247
314
  .slice(0, limit);
248
315
  }
249
316
 
317
+ function isUuid(value) {
318
+ return (
319
+ typeof value === 'string' &&
320
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value)
321
+ );
322
+ }
323
+
324
+ function boundedDiagnosticValue(value) {
325
+ return typeof value === 'string' && value.length > 0 && value.length <= MAX_DIAGNOSTIC_STRING
326
+ ? value
327
+ : null;
328
+ }
329
+
330
+ function isSafeCorrelationId(value) {
331
+ return typeof value === 'string' && /^[A-Za-z0-9._:-]{1,128}$/u.test(value);
332
+ }
333
+
334
+ function boundedCorrelationValue(value) {
335
+ return typeof value === 'string' && value.length > 0 && value.length <= 256 ? value : null;
336
+ }
337
+
338
+ /**
339
+ * Closed private diagnostic contract consumed by the client sink. It contains
340
+ * stage/reason enums, pseudonymous session identity, operation/correlation
341
+ * identifiers and bounded counts only. It never accepts raw MCP/provider text,
342
+ * prompts, notes, paths or thread IDs.
343
+ */
344
+ export function normalizeCodexDiagnostic(event, { threadId = null } = {}) {
345
+ if (!isRecord(event)) return null;
346
+ const stage = DIAGNOSTIC_STAGES.has(event.stage)
347
+ ? event.stage
348
+ : DIAGNOSTIC_STAGES.has(event.phase)
349
+ ? event.phase
350
+ : null;
351
+ const reason = DIAGNOSTIC_REASONS.has(event.reason)
352
+ ? event.reason
353
+ : event.reasonCode === 'candidate_window_bounded'
354
+ ? 'candidate_window_bounded'
355
+ : null;
356
+ if (stage === null || reason === null) return null;
357
+ const operationId = isUuid(event.operationId) ? event.operationId : undefined;
358
+ const correlationId = boundedDiagnosticValue(event.correlationId);
359
+ const counts = isRecord(event.counts)
360
+ ? Object.fromEntries(
361
+ Object.entries(event.counts).filter(
362
+ ([key, value]) =>
363
+ /^[a-z_]{1,48}$/u.test(key) && isSafeBoundedInteger(value, 0, 1_000_000),
364
+ ),
365
+ )
366
+ : undefined;
367
+ const result = {
368
+ schema_version: 1,
369
+ phase: boundedDiagnosticValue(event.phase) ?? `${stage}_${reason}`,
370
+ stage,
371
+ reason,
372
+ ...(operationId === undefined ? {} : { operation_id: operationId }),
373
+ ...(correlationId === null ? {} : { correlation_id: correlationId }),
374
+ ...(typeof threadId === 'string' && threadId.length > 0
375
+ ? { session_key: hashText(threadId) }
376
+ : {}),
377
+ ...(counts && Object.keys(counts).length > 0 ? { counts } : {}),
378
+ };
379
+ return result;
380
+ }
381
+
382
+ function parseJsonText(value) {
383
+ if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > MAX_SELECTION_BYTES)
384
+ return null;
385
+ try {
386
+ const parsed = JSON.parse(value);
387
+ return isRecord(parsed) ? parsed : null;
388
+ } catch {
389
+ return null;
390
+ }
391
+ }
392
+
393
+ function responseStructuredContent(response) {
394
+ if (!isRecord(response)) return null;
395
+ if (isRecord(response.structuredContent)) return response.structuredContent;
396
+ const content = Array.isArray(response.content) ? response.content : [];
397
+ const textPart = content.find((part) => isRecord(part) && part.type === 'text');
398
+ return textPart ? parseJsonText(textPart.text) : null;
399
+ }
400
+
401
+ /** Parse only the closed Context error envelope or known fixed-code text form. */
402
+ export function parseContextError(response) {
403
+ const structured = responseStructuredContent(response);
404
+ let value = structured;
405
+ if (value === null) {
406
+ const content = Array.isArray(response?.content) ? response.content : [];
407
+ const textPart = content.find((part) => isRecord(part) && part.type === 'text');
408
+ const text = typeof textPart?.text === 'string' ? textPart.text : '';
409
+ const fixed =
410
+ /^(INVALID_INPUT|FEATURE_DISABLED|PAID_PLAN_REQUIRED|ENTITLEMENT_UNAVAILABLE|RATE_LIMITED|PROVIDER_NOT_CONFIGURED|BUDGET_EXCEEDED|PROVIDER_TIMEOUT|PROVIDER_UNAVAILABLE|INVALID_PROVIDER_RESPONSE)(?::|$)/u.exec(
411
+ text,
412
+ );
413
+ if (!fixed) return null;
414
+ value = { error_code: fixed[1] };
415
+ }
416
+ if (
417
+ !CONTEXT_ERROR_CODES.has(value.error_code) ||
418
+ (value.failure_detail !== undefined && !CONTEXT_FAILURE_DETAILS.has(value.failure_detail)) ||
419
+ (value.admission_reason !== undefined &&
420
+ !CONTEXT_ADMISSION_REASONS.has(value.admission_reason)) ||
421
+ (value.correlation_id !== undefined && !isSafeCorrelationId(value.correlation_id)) ||
422
+ (value.request_id !== undefined && !isUuid(value.request_id)) ||
423
+ (value.retry_after_ms !== undefined &&
424
+ !isSafeBoundedInteger(value.retry_after_ms, 0, 3_600_000))
425
+ )
426
+ return null;
427
+ return {
428
+ errorCode: value.error_code,
429
+ failureDetail: value.failure_detail ?? null,
430
+ admissionReason: value.admission_reason ?? null,
431
+ correlationId: boundedCorrelationValue(value.correlation_id),
432
+ requestId: isUuid(value.request_id) ? value.request_id : null,
433
+ retryAfterMs: value.retry_after_ms ?? null,
434
+ quota:
435
+ value.error_code === 'RATE_LIMITED' ||
436
+ value.failure_detail === 'shared_admission_denied' ||
437
+ SHARED_QUOTA_ADMISSION_REASONS.has(value.admission_reason),
438
+ };
439
+ }
440
+
441
+ export function parseBudgetSnapshot(value) {
442
+ if (
443
+ !isRecord(value) ||
444
+ !hasOnlyKeys(value, [
445
+ 'schema_version',
446
+ 'server_time',
447
+ 'expires_at',
448
+ 'window_end',
449
+ 'available_requests',
450
+ 'available_tokens',
451
+ 'output_token_allowance',
452
+ 'max_batches_per_operation',
453
+ 'account_in_flight',
454
+ 'retry_after_ms',
455
+ 'provider_batches',
456
+ 'provider_model',
457
+ 'correlation_id',
458
+ ]) ||
459
+ value.schema_version !== 1 ||
460
+ !isTimestamp(value.server_time) ||
461
+ !isTimestamp(value.expires_at) ||
462
+ !isTimestamp(value.window_end) ||
463
+ Date.parse(value.expires_at) <= Date.parse(value.server_time) ||
464
+ Date.parse(value.expires_at) - Date.parse(value.server_time) > MAX_BUDGET_SNAPSHOT_AGE_MS ||
465
+ Date.parse(value.expires_at) > Date.parse(value.window_end) ||
466
+ !isSafeBoundedInteger(value.available_requests, 0, 1_000_000) ||
467
+ !isSafeBoundedInteger(value.available_tokens, 0, Number.MAX_SAFE_INTEGER) ||
468
+ !isSafeBoundedInteger(value.output_token_allowance, 0, Number.MAX_SAFE_INTEGER) ||
469
+ !isSafeBoundedInteger(value.max_batches_per_operation, 0, 4) ||
470
+ typeof value.account_in_flight !== 'boolean' ||
471
+ !isSafeBoundedInteger(value.retry_after_ms, 0, 3_600_000) ||
472
+ value.provider_batches !== 0 ||
473
+ value.provider_model !== null ||
474
+ !isSafeCorrelationId(value.correlation_id)
475
+ )
476
+ return null;
477
+ return value;
478
+ }
479
+
480
+ function budgetSnapshotFresh(snapshot, receivedAt, currentTime) {
481
+ if (!isRecord(snapshot) || !Number.isSafeInteger(receivedAt) || currentTime < receivedAt)
482
+ return false;
483
+ const lifetime = Date.parse(snapshot.expires_at) - Date.parse(snapshot.server_time);
484
+ return (
485
+ lifetime > 0 && lifetime <= MAX_BUDGET_SNAPSHOT_AGE_MS && currentTime - receivedAt <= lifetime
486
+ );
487
+ }
488
+
489
+ function diagnosticReasonForError(error) {
490
+ const message = error instanceof Error ? error.message : String(error ?? '');
491
+ if (/tim(?:e|ed)[ -]?out/iu.test(message)) return 'timeout';
492
+ if (/closed|deleted/iu.test(message)) return 'thread_closed';
493
+ if (/source changed|stale/iu.test(message)) return 'source_changed';
494
+ if (/candidate|history|thread\/read/iu.test(message)) return 'history_unavailable';
495
+ return 'request_failed';
496
+ }
497
+
250
498
  async function ensurePrivateDirectory(directory) {
251
499
  const target = resolve(directory);
252
500
  await mkdir(target, { recursive: true, mode: 0o700 });
@@ -351,12 +599,23 @@ function validateThreadRead(value, threadId) {
351
599
  return value;
352
600
  }
353
601
 
354
- export function buildRecoveryCandidates(read, revision, policy = null) {
602
+ export function buildRecoveryCandidates(read, revision, policy = null, options = {}) {
355
603
  const turns = read.thread.turns;
356
- const rawItems = turns.flatMap((turn) => (Array.isArray(turn.items) ? turn.items : []));
357
- if (rawItems.length > MAX_RECOVERY_ITEMS)
604
+ const rawItems = turns.flatMap((turn, turnIndex) =>
605
+ (Array.isArray(turn.items) ? turn.items : []).map((item, itemIndex) => ({
606
+ item,
607
+ turnIndex,
608
+ itemIndex,
609
+ })),
610
+ );
611
+ const maxItems = options.maxItems ?? MAX_RECOVERY_ITEMS;
612
+ if (!Number.isSafeInteger(maxItems) || maxItems < 1 || maxItems > MAX_RECOVERY_ITEMS)
613
+ throw new Error('recovery candidate bound is invalid');
614
+ if (rawItems.length > maxItems && options.allowWindow !== true)
358
615
  throw new Error('recovery candidate count exceeds bound');
359
- const items = rawItems.map((rawItem, index) => {
616
+ if (rawItems.length > MAX_RECOVERY_SOURCE_INDEX)
617
+ throw new Error('recovery source index exceeds bound');
618
+ let items = rawItems.map(({ item: rawItem, turnIndex, itemIndex }, index) => {
360
619
  const item = isRecord(rawItem) ? rawItem : {};
361
620
  const type = typeof item.type === 'string' ? item.type : 'unknown';
362
621
  const commandOutput =
@@ -439,10 +698,58 @@ export function buildRecoveryCandidates(read, revision, policy = null) {
439
698
  pinReasons,
440
699
  source: rawItem,
441
700
  sourceItemId: typeof item.id === 'string' ? item.id : `item-${index + 1}`,
701
+ sourceLocation: { flatIndex: index, turnIndex, itemIndex },
702
+ sourceRefs: null,
442
703
  sourceBodyChars: commandOutput.length,
443
704
  kind,
444
705
  };
445
706
  });
707
+ if (items.length > maxItems) {
708
+ const protectedItems = items.filter((item) => item.protected);
709
+ const anchorIds = new Set();
710
+ const first = items[0];
711
+ if (first?.protected) anchorIds.add(first.id);
712
+ const currentGoal = items.findLast((item) => item.kind === 'user_message');
713
+ if (currentGoal?.protected) anchorIds.add(currentGoal.id);
714
+ const recentCount = policy?.preserveRecentMessages ?? 6;
715
+ for (const item of items.slice(-recentCount)) {
716
+ if (item.protected) anchorIds.add(item.id);
717
+ }
718
+ const individualProtected = protectedItems.filter((item) => anchorIds.has(item.id));
719
+ const bundledProtected = protectedItems.filter((item) => !anchorIds.has(item.id));
720
+ if (individualProtected.length >= maxItems)
721
+ throw new Error('recovery protected anchors exceed bound');
722
+ const selected = new Set(individualProtected.map((item) => item.id));
723
+ if (bundledProtected.length > 0) {
724
+ const bundle = {
725
+ id: 'recovery-protected-bundle-1',
726
+ text: `protected reference bundle: ${bundledProtected.length} items retained in the verified immutable capture`,
727
+ localText: '',
728
+ localChars: 0,
729
+ protected: true,
730
+ pinReasons: ['reference'],
731
+ source: null,
732
+ sourceItemId: null,
733
+ sourceLocation: null,
734
+ sourceRefs: bundledProtected.map((item) => ({
735
+ ...item.sourceLocation,
736
+ sourceItemId: item.sourceItemId,
737
+ })),
738
+ sourceBodyChars: 0,
739
+ kind: 'reference',
740
+ };
741
+ items = [...items, bundle];
742
+ selected.add(bundle.id);
743
+ }
744
+ for (const item of items
745
+ .filter((candidate) => !candidate.protected)
746
+ .slice()
747
+ .reverse()) {
748
+ if (selected.size >= maxItems) break;
749
+ selected.add(item.id);
750
+ }
751
+ items = items.filter((item) => selected.has(item.id));
752
+ }
446
753
  const projection = {
447
754
  goal:
448
755
  policy?.goal !== undefined
@@ -467,6 +774,104 @@ export function buildRecoveryCandidates(read, revision, policy = null) {
467
774
  return { items, projection, projectionHash: hashJson(projection), revision };
468
775
  }
469
776
 
777
+ function selectionFittingProjection(candidates) {
778
+ return {
779
+ goal: candidates.projection.goal,
780
+ items: candidates.items.map(
781
+ ({ id, kind, text, localChars, protected: protectedItem, pinReasons }) => ({
782
+ id,
783
+ kind,
784
+ text,
785
+ localChars,
786
+ protected: protectedItem,
787
+ pinReasons,
788
+ }),
789
+ ),
790
+ };
791
+ }
792
+
793
+ function selectionWireProjection(goal, items) {
794
+ return {
795
+ goal,
796
+ items: items.map(({ id, kind, text, localChars, protected: protectedItem, pinReasons }) => ({
797
+ id,
798
+ kind,
799
+ text,
800
+ local_chars: localChars,
801
+ protected: protectedItem,
802
+ pin_reasons: pinReasons,
803
+ })),
804
+ };
805
+ }
806
+
807
+ function selectionFittingOptions(policy, maxRequestTokens) {
808
+ return {
809
+ preserveRecentItems: policy?.preserveRecentMessages ?? 6,
810
+ maxStateTokens: policy?.maxStateTokens ?? 25_000,
811
+ maxRequestTokens,
812
+ };
813
+ }
814
+
815
+ function fitWithinCapacity(fit, capacity) {
816
+ if (!fit.ok) return false;
817
+ const batchCount = fit.batches.length;
818
+ return (
819
+ batchCount <= capacity.requests &&
820
+ batchCount * (fit.requestTokens + capacity.outputTokenAllowance) <= capacity.tokens
821
+ );
822
+ }
823
+
824
+ export function boundCandidatesToBudget(candidates, availableTokens, policy = null) {
825
+ const capacity =
826
+ typeof availableTokens === 'number'
827
+ ? {
828
+ tokens: availableTokens,
829
+ requests: Number.MAX_SAFE_INTEGER,
830
+ maxRequestTokens: 30_000,
831
+ outputTokenAllowance: 0,
832
+ }
833
+ : { outputTokenAllowance: 0, ...availableTokens };
834
+ if (
835
+ !isRecord(capacity) ||
836
+ !isSafeBoundedInteger(capacity.tokens, 1, Number.MAX_SAFE_INTEGER) ||
837
+ !isSafeBoundedInteger(capacity.requests, 1, Number.MAX_SAFE_INTEGER) ||
838
+ !isSafeBoundedInteger(capacity.maxRequestTokens, 1_000, 30_000) ||
839
+ !isSafeBoundedInteger(capacity.outputTokenAllowance, 0, Number.MAX_SAFE_INTEGER)
840
+ )
841
+ throw new Error('recovery budget has no usable token capacity');
842
+ const protectedItems = candidates.items.filter((item) => item.protected);
843
+ if (protectedItems.length > MAX_RECOVERY_ITEMS)
844
+ throw new Error('recovery protected candidate count exceeds bound');
845
+ const eligibleItems = candidates.items.filter((item) => !item.protected);
846
+ const fitFor = (items) =>
847
+ preflightSelectionState({
848
+ projection: selectionFittingProjection({ ...candidates, items }),
849
+ options: selectionFittingOptions(policy, capacity.maxRequestTokens),
850
+ });
851
+ for (let keepCount = eligibleItems.length; keepCount >= 0; keepCount -= 1) {
852
+ const retainedEligible = new Set(
853
+ (keepCount === 0 ? [] : eligibleItems.slice(-keepCount)).map((item) => item.id),
854
+ );
855
+ const items = candidates.items.filter(
856
+ (item) => item.protected || retainedEligible.has(item.id),
857
+ );
858
+ const fit = fitFor(items);
859
+ if (!fitWithinCapacity(fit, capacity)) continue;
860
+ const projection = selectionFittingProjection({ ...candidates, items });
861
+ const wireProjection = selectionWireProjection(candidates.projection.goal, items);
862
+ return {
863
+ items,
864
+ projection: wireProjection,
865
+ projectionHash: hashJson(wireProjection),
866
+ revision: candidates.revision,
867
+ bounded: items.length !== candidates.items.length,
868
+ preflight: fit,
869
+ providerProjection: projection,
870
+ };
871
+ }
872
+ throw new Error('recovery protected candidate projection exceeds budget');
873
+ }
874
+
470
875
  export function parseSelection(value, input, candidates, currentTime = Date.now()) {
471
876
  const v2 = isRecord(input) && input.schema_version === 2;
472
877
  const baseKeys = [
@@ -610,6 +1015,36 @@ export function recoveryNotesText(
610
1015
  candidates,
611
1016
  decisions,
612
1017
  ) {
1018
+ const formatSourceLocations = (refs) => {
1019
+ const sorted = refs
1020
+ .filter(
1021
+ (ref) =>
1022
+ isRecord(ref) &&
1023
+ isSafeBoundedInteger(ref.flatIndex, 0, MAX_RECOVERY_SOURCE_INDEX) &&
1024
+ isSafeBoundedInteger(ref.turnIndex, 0, MAX_RECOVERY_SOURCE_INDEX) &&
1025
+ isSafeBoundedInteger(ref.itemIndex, 0, MAX_RECOVERY_SOURCE_INDEX),
1026
+ )
1027
+ .sort((left, right) => left.flatIndex - right.flatIndex);
1028
+ const ranges = [];
1029
+ for (const ref of sorted) {
1030
+ const previous = ranges.at(-1);
1031
+ if (previous && ref.flatIndex === previous.end + 1) previous.end = ref.flatIndex;
1032
+ else ranges.push({ start: ref.flatIndex, end: ref.flatIndex });
1033
+ }
1034
+ return {
1035
+ positions: ranges
1036
+ .map((range) =>
1037
+ range.start === range.end ? String(range.start) : `${range.start}-${range.end}`,
1038
+ )
1039
+ .join(','),
1040
+ locations: sorted.map((ref) => `${ref.turnIndex}:${ref.itemIndex}`).join(','),
1041
+ ids: sorted
1042
+ .map((ref) =>
1043
+ typeof ref.sourceItemId === 'string' ? escapeReference(ref.sourceItemId) : 'unknown',
1044
+ )
1045
+ .join(','),
1046
+ };
1047
+ };
613
1048
  const selected = new Map(
614
1049
  decisions.filter((decision) => decision.keep).map((decision) => [decision.id, decision]),
615
1050
  );
@@ -623,6 +1058,7 @@ export function recoveryNotesText(
623
1058
  `- capture_base: ${escapeReference(captureBase)}`,
624
1059
  `- capture_file: ${escapeReference(captureFile)} (private checkpoint directory)`,
625
1060
  `- capture_source_sha256: ${revision}`,
1061
+ '- capture_verified: immutable private capture read and revision checked before native compaction',
626
1062
  `- source: private local capture`,
627
1063
  '',
628
1064
  '## Selected context',
@@ -631,7 +1067,12 @@ export function recoveryNotesText(
631
1067
  let inlineBudget = Math.floor(MAX_NOTE_BYTES / 2);
632
1068
  for (const item of candidates.items) {
633
1069
  if (!selected.has(item.id)) continue;
634
- const reference = `[selected local content retained in ${escapeReference(captureFile)}; source_item=${escapeReference(item.sourceItemId)}; local_chars=${item.localChars}]`;
1070
+ const reference = item.sourceRefs
1071
+ ? (() => {
1072
+ const source = formatSourceLocations(item.sourceRefs);
1073
+ return `[protected reference bundle retained in verified capture ${escapeReference(captureFile)}; source_positions=${source.positions}; thread_locations=${source.locations}; source_ids=${source.ids}]`;
1074
+ })()
1075
+ : `[selected local content retained in ${escapeReference(captureFile)}; source_item=${escapeReference(item.sourceItemId)}; source_location=${item.sourceLocation?.turnIndex ?? 'unknown'}:${item.sourceLocation?.itemIndex ?? 'unknown'}; local_chars=${item.localChars}]`;
635
1076
  const inline =
636
1077
  item.kind !== 'tool_result' &&
637
1078
  item.kind !== 'reference' &&
@@ -698,6 +1139,13 @@ export function createCodexContextProxy({
698
1139
  },
699
1140
  recoveryEnabled = process.env.CODEX_CONTEXT_RECOVERY_ENABLED === '1',
700
1141
  recoveryDisclosed = process.env.CODEX_CONTEXT_RECOVERY_DISCLOSED === '1',
1142
+ telemetryEnabled = process.env.CODEX_CONTEXT_TELEMETRY_ENABLED === '1',
1143
+ telemetryDisclosed = process.env.CODEX_CONTEXT_TELEMETRY_DISCLOSED === '1',
1144
+ telemetryTimeoutMs = TELEMETRY_TIMEOUT_MS,
1145
+ telemetrySetTimer = setTimeout,
1146
+ telemetryClearTimer = clearTimeout,
1147
+ budgetEnabled = process.env.CODEX_CONTEXT_BUDGET_ENABLED !== '0',
1148
+ createDiagnosticSink = null,
701
1149
  brainServer = process.env.AI_BRAIN_MCP_SERVER ?? 'brain',
702
1150
  clientVersion = process.env.CODEX_CLIENT_VERSION ?? '0.161.0',
703
1151
  }) {
@@ -707,10 +1155,86 @@ export function createCodexContextProxy({
707
1155
  const ownedRequests = new Map();
708
1156
  const internalReads = new Map();
709
1157
  const retiredRequests = new Set();
1158
+ const telemetryByThread = new Map();
1159
+ const diagnosticSinks = new Map();
710
1160
  let stopped = false;
711
1161
  function emitDiagnostic(event) {
712
1162
  try {
713
- onDiagnostic(event);
1163
+ const phase = typeof event?.phase === 'string' ? event.phase : '';
1164
+ const stage =
1165
+ event?.stage ??
1166
+ (phase === 'selection_accepted' || phase === 'select' ? 'selection' : null) ??
1167
+ (phase === 'read' || phase === 'capture' ? 'capture' : null) ??
1168
+ (phase === 'compact' || phase === 'native' || phase === 'thread_failure'
1169
+ ? 'native'
1170
+ : null) ??
1171
+ (phase === 'inject' || phase === 'recovery' ? 'recovery' : null) ??
1172
+ (phase === 'transport_end' || phase === 'transport_close' || phase === 'process_error'
1173
+ ? 'transport'
1174
+ : null);
1175
+ const reason =
1176
+ event?.reason ??
1177
+ (DIAGNOSTIC_REASONS.has(event?.reasonCode) ? event.reasonCode : null) ??
1178
+ (phase === 'selection_accepted' ? 'completed' : null) ??
1179
+ (phase === 'transport_close' || phase === 'transport_end' ? 'disconnected' : null) ??
1180
+ (phase === 'thread_failure' ? 'protocol_error' : null) ??
1181
+ (event?.errorCode !== undefined ? 'protocol_error' : null);
1182
+ const normalized = normalizeCodexDiagnostic(
1183
+ {
1184
+ ...event,
1185
+ stage,
1186
+ reason,
1187
+ operationId: event?.operationId ?? threads.get(event?.threadId)?.pending?.operationId,
1188
+ },
1189
+ { threadId: event?.threadId },
1190
+ );
1191
+ if (normalized === null) return;
1192
+ if (
1193
+ typeof event?.schemaVersion === 'number' &&
1194
+ isSafeBoundedInteger(event.schemaVersion, 1, 2)
1195
+ )
1196
+ normalized.schemaVersion = event.schemaVersion;
1197
+ for (const [source, target] of [
1198
+ ['optionsHash', 'options_hash'],
1199
+ ['fittingStage', 'fitting_stage'],
1200
+ ['batchCount', 'batch_count'],
1201
+ ['providerModel', 'provider_model'],
1202
+ ]) {
1203
+ if (
1204
+ (typeof event[source] === 'string' && event[source].length <= MAX_DIAGNOSTIC_STRING) ||
1205
+ (source === 'batchCount' && isSafeBoundedInteger(event[source], 0, 4)) ||
1206
+ (source === 'providerModel' && event[source] === null)
1207
+ )
1208
+ normalized[target] = event[source];
1209
+ if (source === 'optionsHash' && typeof event[source] === 'string')
1210
+ normalized.optionsHash = event[source];
1211
+ if (source === 'fittingStage' && typeof event[source] === 'string')
1212
+ normalized.fittingStage = event[source];
1213
+ if (source === 'batchCount' && isSafeBoundedInteger(event[source], 0, 4))
1214
+ normalized.batchCount = event[source];
1215
+ if (
1216
+ source === 'providerModel' &&
1217
+ (typeof event[source] === 'string' || event[source] === null)
1218
+ )
1219
+ normalized.providerModel = event[source];
1220
+ }
1221
+ onDiagnostic(normalized);
1222
+ if (typeof createDiagnosticSink === 'function' && typeof event?.threadId === 'string') {
1223
+ let sink = diagnosticSinks.get(event.threadId);
1224
+ if (!sink) {
1225
+ try {
1226
+ sink = createDiagnosticSink(event.threadId);
1227
+ if (typeof sink === 'function') diagnosticSinks.set(event.threadId, sink);
1228
+ } catch {
1229
+ sink = null;
1230
+ }
1231
+ }
1232
+ try {
1233
+ sink?.(normalized);
1234
+ } catch {
1235
+ /* private diagnostics cannot affect compaction */
1236
+ }
1237
+ }
714
1238
  } catch {
715
1239
  /* diagnostics must never affect compaction */
716
1240
  }
@@ -751,7 +1275,15 @@ export function createCodexContextProxy({
751
1275
  cleanupRequest(pending.compactId);
752
1276
  pending.deferred.reject(new Error(reason));
753
1277
  state.pending = null;
754
- emitDiagnostic({ phase: 'thread_failure', reasonCode: reason.replace(/[^a-z0-9]+/gi, '_') });
1278
+ const operationId = pending.operationId;
1279
+ if (operationId) telemetryFor(threadId).failed({ operationId, reason: 'native_error' });
1280
+ emitDiagnostic({
1281
+ phase: 'thread_failure',
1282
+ stage: pending.stage ?? 'native',
1283
+ reason: diagnosticReasonForError(new Error(reason)),
1284
+ threadId,
1285
+ operationId,
1286
+ });
755
1287
  stderr.write(`codex-context: native compaction preserved (${reason})\n`);
756
1288
  }
757
1289
  function failAll(reason) {
@@ -792,6 +1324,7 @@ export function createCodexContextProxy({
792
1324
  recoveryLoaded: false,
793
1325
  recoveryLoadPromise: null,
794
1326
  recoveryAttach: null,
1327
+ telemetryConnected: false,
795
1328
  };
796
1329
  threads.set(threadId, created);
797
1330
  return created;
@@ -810,6 +1343,187 @@ export function createCodexContextProxy({
810
1343
  }
811
1344
  return id;
812
1345
  }
1346
+ function telemetryFor(threadId) {
1347
+ const existing = telemetryByThread.get(threadId);
1348
+ if (existing) return existing;
1349
+ const reporter = createTelemetryReporter({
1350
+ client: 'codex-cli',
1351
+ clientVersion,
1352
+ sessionId: threadId,
1353
+ enabled: telemetryEnabled,
1354
+ disclosed: telemetryDisclosed,
1355
+ now,
1356
+ timeoutMs: telemetryTimeoutMs,
1357
+ setTimer: telemetrySetTimer,
1358
+ clearTimer: telemetryClearTimer,
1359
+ send: async (event) => {
1360
+ const waiter = deferred();
1361
+ void waiter.promise.catch(() => undefined);
1362
+ let id;
1363
+ try {
1364
+ id = sendOwned(
1365
+ 'mcpServer/tool/call',
1366
+ {
1367
+ threadId,
1368
+ server: brainServer,
1369
+ tool: 'brain_context_report',
1370
+ arguments: event,
1371
+ },
1372
+ 'telemetry',
1373
+ threadId,
1374
+ waiter,
1375
+ );
1376
+ let timer;
1377
+ try {
1378
+ const result = await Promise.race([
1379
+ waiter.promise,
1380
+ new Promise((_, reject) => {
1381
+ timer = telemetrySetTimer(
1382
+ () => reject(new Error('telemetry_timeout')),
1383
+ telemetryTimeoutMs,
1384
+ );
1385
+ }),
1386
+ ]);
1387
+ if (!isRecord(result) || result.isError === true)
1388
+ throw new Error('telemetry_report_rejected');
1389
+ const structured = isRecord(result.structuredContent)
1390
+ ? result.structuredContent
1391
+ : result;
1392
+ if (!isRecord(structured) || structured.accepted !== true)
1393
+ throw new Error('telemetry_report_rejected');
1394
+ return result;
1395
+ } finally {
1396
+ if (timer !== undefined) telemetryClearTimer(timer);
1397
+ }
1398
+ } finally {
1399
+ cleanupRequest(id);
1400
+ }
1401
+ },
1402
+ });
1403
+ telemetryByThread.set(threadId, reporter);
1404
+ return reporter;
1405
+ }
1406
+ async function requestBudget(threadId, pending) {
1407
+ const waiter = deferred();
1408
+ void waiter.promise.catch(() => undefined);
1409
+ const budgetRequestId = sendOwned(
1410
+ 'mcpServer/tool/call',
1411
+ {
1412
+ threadId,
1413
+ server: brainServer,
1414
+ tool: 'brain_context_budget',
1415
+ arguments: { schema_version: 1 },
1416
+ },
1417
+ 'budget',
1418
+ threadId,
1419
+ waiter,
1420
+ );
1421
+ pending.auxIds.push(budgetRequestId);
1422
+ emitDiagnostic({
1423
+ phase: 'budget',
1424
+ stage: 'budget',
1425
+ reason: 'requested',
1426
+ threadId,
1427
+ operationId: pending.operationId,
1428
+ });
1429
+ let response;
1430
+ try {
1431
+ response = await waiter.promise;
1432
+ } catch {
1433
+ emitDiagnostic({
1434
+ phase: 'budget',
1435
+ stage: 'budget',
1436
+ reason: 'unavailable',
1437
+ threadId,
1438
+ operationId: pending.operationId,
1439
+ });
1440
+ return { snapshot: null, error: null };
1441
+ }
1442
+ const error = parseContextError(response);
1443
+ if (error !== null && response?.isError === true) {
1444
+ emitDiagnostic({
1445
+ phase: 'budget',
1446
+ stage: 'budget',
1447
+ reason: error.quota ? 'admission_denied' : 'request_failed',
1448
+ threadId,
1449
+ operationId: pending.operationId,
1450
+ correlationId: error.correlationId,
1451
+ });
1452
+ return { snapshot: null, error };
1453
+ }
1454
+ const receivedAt = now();
1455
+ const snapshot = parseBudgetSnapshot(responseStructuredContent(response));
1456
+ if (snapshot === null) {
1457
+ emitDiagnostic({
1458
+ phase: 'budget',
1459
+ stage: 'budget',
1460
+ reason: 'stale_snapshot',
1461
+ threadId,
1462
+ operationId: pending.operationId,
1463
+ });
1464
+ return { snapshot: null, error: null };
1465
+ }
1466
+ emitDiagnostic({
1467
+ phase: 'budget',
1468
+ stage: 'budget',
1469
+ reason: 'completed',
1470
+ threadId,
1471
+ operationId: pending.operationId,
1472
+ correlationId: snapshot.correlation_id,
1473
+ counts: {
1474
+ available_requests: snapshot.available_requests,
1475
+ available_tokens: Math.min(snapshot.available_tokens, 1_000_000),
1476
+ },
1477
+ });
1478
+ return { snapshot, error: null, receivedAt };
1479
+ }
1480
+ function emitNativeFallback(threadId, pending, reason, correlationId = null) {
1481
+ telemetryFor(threadId).fallback({
1482
+ operationId: pending.operationId,
1483
+ reason:
1484
+ reason === 'admission_denied'
1485
+ ? 'rate_limited'
1486
+ : reason === 'provider_unavailable'
1487
+ ? 'provider_unavailable'
1488
+ : 'recovery_pending',
1489
+ });
1490
+ emitDiagnostic({
1491
+ phase: 'budget',
1492
+ stage: 'budget',
1493
+ reason: reason === 'provider_unavailable' ? 'request_failed' : reason,
1494
+ threadId,
1495
+ operationId: pending.operationId,
1496
+ correlationId,
1497
+ });
1498
+ }
1499
+ function emitOutcome(outcome) {
1500
+ try {
1501
+ onOutcome(outcome);
1502
+ } catch {
1503
+ /* lifecycle observers cannot affect compaction */
1504
+ }
1505
+ const reporter = telemetryFor(outcome.threadId);
1506
+ const operationId =
1507
+ outcome.operationId ??
1508
+ threads.get(outcome.threadId)?.recovery?.operationId ??
1509
+ threads.get(outcome.threadId)?.pending?.operationId;
1510
+ if (
1511
+ !outcome.telemetryAlreadyEmitted &&
1512
+ (outcome.status === 'native_completed' || outcome.status === 'completed') &&
1513
+ operationId
1514
+ )
1515
+ reporter.nativeCompleted({ operationId });
1516
+ else if (outcome.status === 'recovery_attached' && operationId)
1517
+ reporter.recoveryAttached({ operationId });
1518
+ else if (outcome.status === 'recovery_uncertain' && operationId)
1519
+ reporter.uncertain({ operationId, reason: 'recovery_uncertain' });
1520
+ else if (outcome.status === 'recovery_pending' && operationId)
1521
+ reporter.fallback({ operationId, reason: 'recovery_pending' });
1522
+ else if (outcome.status === 'recovery_abandoned' && operationId)
1523
+ reporter.fallback({ operationId, reason: 'stale_context' });
1524
+ else if (outcome.status === 'failed' && operationId)
1525
+ reporter.failed({ operationId, reason: 'native_error' });
1526
+ }
813
1527
  function assertCurrent(threadId, state, pending, revision) {
814
1528
  if (pending.canceled || state.pending !== pending || !state.idle || state.revision !== revision)
815
1529
  throw new Error('Codex state changed during compaction');
@@ -851,14 +1565,22 @@ export function createCodexContextProxy({
851
1565
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
852
1566
  value.requestId,
853
1567
  ) &&
1568
+ (value.operationId === undefined ||
1569
+ (typeof value.operationId === 'string' &&
1570
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
1571
+ value.operationId,
1572
+ ))) &&
854
1573
  typeof value.itemId === 'string' &&
855
1574
  value.itemId.length <= 128 &&
856
1575
  (!['pending_attachment', 'injecting', 'recovery_uncertain'].includes(value.status) ||
857
1576
  typeof value.turnId === 'string')
858
1577
  )
859
- return value.status === 'injecting'
860
- ? { ...value, status: 'recovery_uncertain' }
861
- : value;
1578
+ return {
1579
+ ...(value.status === 'injecting'
1580
+ ? { ...value, status: 'recovery_uncertain' }
1581
+ : value),
1582
+ operationId: value.operationId ?? value.requestId,
1583
+ };
862
1584
  } catch (error) {
863
1585
  if (error?.code !== 'ENOENT') stderr.write('codex-context: recovery state unavailable\n');
864
1586
  }
@@ -962,7 +1684,14 @@ export function createCodexContextProxy({
962
1684
  await persistRecoveryState(threadId, attached);
963
1685
  if (attachment.invalidated || attachment.canceled) return;
964
1686
  state.recovery = attached;
965
- onOutcome({ threadId, status: 'recovery_attached', turnId: record.turnId });
1687
+ emitDiagnostic({
1688
+ phase: 'recovery_attached',
1689
+ stage: 'recovery',
1690
+ reason: 'attached',
1691
+ threadId,
1692
+ operationId: record.operationId,
1693
+ });
1694
+ emitOutcome({ threadId, status: 'recovery_attached', turnId: record.turnId });
966
1695
  return;
967
1696
  }
968
1697
  if (record.status === 'recovery_uncertain') {
@@ -973,7 +1702,14 @@ export function createCodexContextProxy({
973
1702
  };
974
1703
  await persistRecoveryState(threadId, uncertain).catch(() => undefined);
975
1704
  state.recovery = uncertain;
976
- onOutcome({ threadId, status: 'recovery_uncertain', turnId: record.turnId });
1705
+ emitDiagnostic({
1706
+ phase: 'recovery_uncertain',
1707
+ stage: 'recovery',
1708
+ reason: 'uncertain',
1709
+ threadId,
1710
+ operationId: record.operationId,
1711
+ });
1712
+ emitOutcome({ threadId, status: 'recovery_uncertain', turnId: record.turnId });
977
1713
  return;
978
1714
  }
979
1715
  if (!state.idle) throw new Error('thread became active before recovery attachment');
@@ -1015,7 +1751,7 @@ export function createCodexContextProxy({
1015
1751
  };
1016
1752
  await persistRecoveryState(threadId, attached);
1017
1753
  state.recovery = attached;
1018
- onOutcome({ threadId, status: 'recovery_attached', turnId: record.turnId });
1754
+ emitOutcome({ threadId, status: 'recovery_attached', turnId: record.turnId });
1019
1755
  } catch {
1020
1756
  if (attachment.injectSent) {
1021
1757
  const uncertain = {
@@ -1025,7 +1761,14 @@ export function createCodexContextProxy({
1025
1761
  };
1026
1762
  await persistRecoveryState(threadId, uncertain).catch(() => undefined);
1027
1763
  state.recovery = uncertain;
1028
- onOutcome({ threadId, status: 'recovery_uncertain', turnId: record.turnId });
1764
+ emitDiagnostic({
1765
+ phase: 'recovery_uncertain',
1766
+ stage: 'recovery',
1767
+ reason: 'uncertain',
1768
+ threadId,
1769
+ operationId: record.operationId,
1770
+ });
1771
+ emitOutcome({ threadId, status: 'recovery_uncertain', turnId: record.turnId });
1029
1772
  } else if (!attachment.canceled) {
1030
1773
  if (attachment.stale) {
1031
1774
  state.recovery = {
@@ -1034,8 +1777,8 @@ export function createCodexContextProxy({
1034
1777
  abandonedAt: new Date(now()).toISOString(),
1035
1778
  };
1036
1779
  await persistRecoveryState(threadId, state.recovery).catch(() => undefined);
1037
- onOutcome({ threadId, status: 'recovery_abandoned', turnId: record.turnId });
1038
- } else onOutcome({ threadId, status: 'recovery_pending', turnId: record.turnId });
1780
+ emitOutcome({ threadId, status: 'recovery_abandoned', turnId: record.turnId });
1781
+ } else emitOutcome({ threadId, status: 'recovery_pending', turnId: record.turnId });
1039
1782
  }
1040
1783
  } finally {
1041
1784
  if (attachment.timer) attachment.timer();
@@ -1060,6 +1803,7 @@ export function createCodexContextProxy({
1060
1803
  state.pending ||
1061
1804
  !state.usage ||
1062
1805
  !isValidUsage(state.usage) ||
1806
+ !hasKnownContextWindow(state.usage) ||
1063
1807
  !Number.isFinite(state.usageReceivedAt) ||
1064
1808
  currentTime < state.usageReceivedAt ||
1065
1809
  currentTime - state.usageReceivedAt > USAGE_FRESHNESS_MS ||
@@ -1092,12 +1836,15 @@ export function createCodexContextProxy({
1092
1836
  itemCompleted: false,
1093
1837
  turnCompleted: false,
1094
1838
  canceled: false,
1839
+ stage: 'capture',
1840
+ operationId: randomUUID(),
1095
1841
  deferred: completion,
1096
1842
  timer: null,
1097
1843
  };
1098
1844
  state.pending = pending;
1099
1845
  pending.timer = scheduleTimeout(() => fail(threadId, 'native compaction timed out'), timeoutMs);
1100
1846
  try {
1847
+ pending.stage = 'capture';
1101
1848
  pending.readId = sendOwned(
1102
1849
  'thread/read',
1103
1850
  { threadId, includeTurns: true },
@@ -1136,8 +1883,23 @@ export function createCodexContextProxy({
1136
1883
  `${safeThreadName(threadId)}.${sourceRevision}.json`,
1137
1884
  );
1138
1885
  await atomicWriteImmutablePrivate(immutableCapturePath, snapshot, sourceRevision);
1886
+ emitDiagnostic({
1887
+ phase: 'capture',
1888
+ stage: 'capture',
1889
+ reason: 'completed',
1890
+ threadId,
1891
+ operationId: pending.operationId,
1892
+ counts: {
1893
+ item_count: read.thread.turns.reduce(
1894
+ (count, turn) => count + (turn.items?.length ?? 0),
1895
+ 0,
1896
+ ),
1897
+ },
1898
+ });
1139
1899
  assertCurrent(threadId, state, pending, revision);
1140
1900
  let recoveryNotes;
1901
+ let nativeFallback = false;
1902
+ let budgetReceipt = null;
1141
1903
  if (recoveryEnabled && recoveryDisclosed) {
1142
1904
  strictThreadName(threadId);
1143
1905
  if (
@@ -1155,127 +1917,317 @@ export function createCodexContextProxy({
1155
1917
  await persistRecoveryState(threadId, state.recovery).catch(() => undefined);
1156
1918
  throw new Error('saved recovery selection is stale');
1157
1919
  } else {
1158
- const candidates = buildRecoveryCandidates(read, sourceRevision, contextPolicy);
1159
- const selectionInput = contextPolicy
1160
- ? {
1161
- schema_version: 2,
1162
- client: 'codex_app_server',
1163
- client_version: clientVersion,
1164
- session_ref: threadId,
1165
- revision: sourceRevision,
1166
- observed_at: new Date(now()).toISOString(),
1167
- request_id: randomUUID(),
1168
- policy_version: 'context-recovery-v2',
1169
- options: selectionOptions(contextPolicy),
1170
- projection: candidates.projection,
1920
+ let candidates;
1921
+ if (budgetEnabled) {
1922
+ pending.stage = 'budget';
1923
+ const budget = await requestBudget(threadId, pending);
1924
+ budgetReceipt = budget;
1925
+ if (budget.error?.quota === true) {
1926
+ nativeFallback = true;
1927
+ emitNativeFallback(threadId, pending, 'admission_denied', budget.error.correlationId);
1928
+ } else if (budget.snapshot === null) {
1929
+ nativeFallback = true;
1930
+ emitNativeFallback(threadId, pending, 'unavailable', null);
1931
+ } else if (!budgetSnapshotFresh(budget.snapshot, budget.receivedAt, now())) {
1932
+ nativeFallback = true;
1933
+ emitNativeFallback(
1934
+ threadId,
1935
+ pending,
1936
+ 'stale_snapshot',
1937
+ budget.snapshot.correlation_id,
1938
+ );
1939
+ } else if (
1940
+ budget.snapshot.account_in_flight === true ||
1941
+ budget.snapshot.available_requests < 1 ||
1942
+ budget.snapshot.available_tokens < 1 ||
1943
+ budget.snapshot.max_batches_per_operation < 1
1944
+ ) {
1945
+ nativeFallback = true;
1946
+ emitNativeFallback(
1947
+ threadId,
1948
+ pending,
1949
+ 'admission_denied',
1950
+ budget.snapshot.correlation_id,
1951
+ );
1952
+ } else {
1953
+ try {
1954
+ candidates = buildRecoveryCandidates(read, sourceRevision, contextPolicy, {
1955
+ allowWindow: true,
1956
+ });
1957
+ const bounded = boundCandidatesToBudget(
1958
+ candidates,
1959
+ {
1960
+ tokens: budget.snapshot.available_tokens,
1961
+ requests: Math.min(
1962
+ budget.snapshot.available_requests,
1963
+ budget.snapshot.max_batches_per_operation,
1964
+ ),
1965
+ maxRequestTokens: contextPolicy?.maxRequestTokens ?? 30_000,
1966
+ outputTokenAllowance: budget.snapshot.output_token_allowance,
1967
+ },
1968
+ contextPolicy,
1969
+ );
1970
+ if (bounded.bounded) {
1971
+ emitDiagnostic({
1972
+ phase: 'candidate_window_bounded',
1973
+ stage: 'selection',
1974
+ reason: 'candidate_window_bounded',
1975
+ threadId,
1976
+ operationId: pending.operationId,
1977
+ correlationId: budget.snapshot.correlation_id,
1978
+ counts: {
1979
+ candidate_count: bounded.items.length,
1980
+ deferred_count: Math.max(0, candidates.items.length - bounded.items.length),
1981
+ provider_batch_count: bounded.preflight.batches.length,
1982
+ provider_request_tokens: bounded.preflight.requestTokens,
1983
+ },
1984
+ });
1985
+ }
1986
+ candidates = bounded;
1987
+ } catch (error) {
1988
+ nativeFallback = true;
1989
+ emitNativeFallback(
1990
+ threadId,
1991
+ pending,
1992
+ error?.message?.includes('protected')
1993
+ ? 'protected_over_bound'
1994
+ : 'candidate_over_bound',
1995
+ budget.snapshot.correlation_id,
1996
+ );
1171
1997
  }
1172
- : {
1173
- schema_version: 1,
1174
- client: 'codex_app_server',
1175
- client_version: clientVersion,
1176
- session_ref: threadId,
1177
- revision: sourceRevision,
1178
- observed_at: new Date(now()).toISOString(),
1179
- request_id: randomUUID(),
1180
- policy_version: 'context-recovery-v1',
1181
- projection: candidates.projection,
1182
- };
1998
+ }
1999
+ } else {
2000
+ pending.stage = 'selection';
2001
+ try {
2002
+ candidates = buildRecoveryCandidates(read, sourceRevision, contextPolicy);
2003
+ } catch {
2004
+ nativeFallback = true;
2005
+ emitNativeFallback(threadId, pending, 'candidate_over_bound');
2006
+ }
2007
+ }
1183
2008
  if (
1184
- typeof clientVersion !== 'string' ||
1185
- clientVersion.length < 1 ||
1186
- clientVersion.length > 64 ||
1187
- Buffer.byteLength(JSON.stringify(selectionInput), 'utf8') > MAX_SELECTION_BYTES
1188
- )
1189
- throw new Error('recovery selection input exceeds the contract bound');
1190
- const selectionWaiter = deferred();
1191
- pending.auxIds.push(
1192
- sendOwned(
1193
- 'mcpServer/tool/call',
1194
- {
1195
- threadId,
1196
- server: brainServer,
1197
- tool: 'brain_context_select',
1198
- arguments: selectionInput,
1199
- },
1200
- 'select',
2009
+ !nativeFallback &&
2010
+ budgetReceipt?.snapshot !== null &&
2011
+ budgetReceipt?.snapshot !== undefined &&
2012
+ !budgetSnapshotFresh(budgetReceipt.snapshot, budgetReceipt.receivedAt, now())
2013
+ ) {
2014
+ nativeFallback = true;
2015
+ emitNativeFallback(
1201
2016
  threadId,
1202
- selectionWaiter,
1203
- ),
1204
- );
1205
- const selectionResponse = await selectionWaiter.promise;
1206
- if (!isRecord(selectionResponse) || selectionResponse.isError === true)
1207
- throw new Error('recovery selection MCP call failed');
1208
- const selectionPayload = selectionResponse.structuredContent;
1209
- const decisions = parseSelection(selectionPayload, selectionInput, candidates, now());
1210
- if (selectionInput.schema_version === 2) {
2017
+ pending,
2018
+ 'stale_snapshot',
2019
+ budgetReceipt.snapshot.correlation_id,
2020
+ );
2021
+ }
2022
+ if (nativeFallback) candidates = null;
2023
+ if (candidates === null || candidates === undefined) {
1211
2024
  emitDiagnostic({
1212
- phase: 'selection_accepted',
1213
- schemaVersion: 2,
1214
- optionsHash:
1215
- isRecord(selectionPayload) && typeof selectionPayload.options_hash === 'string'
1216
- ? selectionPayload.options_hash
1217
- : null,
1218
- fittingStage:
1219
- isRecord(selectionPayload) && isRecord(selectionPayload.fitting)
1220
- ? selectionPayload.fitting.stage
1221
- : null,
1222
- batchCount:
1223
- isRecord(selectionPayload) && isRecord(selectionPayload.stats)
1224
- ? selectionPayload.stats.batch_count
1225
- : null,
1226
- providerModel:
1227
- isRecord(selectionPayload) &&
1228
- (typeof selectionPayload.provider_model === 'string' ||
1229
- selectionPayload.provider_model === null)
1230
- ? selectionPayload.provider_model
1231
- : null,
2025
+ phase: 'selection',
2026
+ stage: 'selection',
2027
+ reason: 'unavailable',
2028
+ threadId,
2029
+ operationId: pending.operationId,
1232
2030
  });
2031
+ } else {
2032
+ pending.stage = 'selection';
2033
+ const selectionInput = contextPolicy
2034
+ ? {
2035
+ schema_version: 2,
2036
+ client: 'codex_app_server',
2037
+ client_version: clientVersion,
2038
+ session_ref: threadId,
2039
+ revision: sourceRevision,
2040
+ observed_at: new Date(now()).toISOString(),
2041
+ request_id: pending.operationId,
2042
+ policy_version: 'context-recovery-v2',
2043
+ options: selectionOptions(contextPolicy),
2044
+ projection: candidates.projection,
2045
+ }
2046
+ : {
2047
+ schema_version: 1,
2048
+ client: 'codex_app_server',
2049
+ client_version: clientVersion,
2050
+ session_ref: threadId,
2051
+ revision: sourceRevision,
2052
+ observed_at: new Date(now()).toISOString(),
2053
+ request_id: pending.operationId,
2054
+ policy_version: 'context-recovery-v1',
2055
+ projection: candidates.projection,
2056
+ };
2057
+ if (
2058
+ typeof clientVersion !== 'string' ||
2059
+ clientVersion.length < 1 ||
2060
+ clientVersion.length > 64 ||
2061
+ Buffer.byteLength(JSON.stringify(selectionInput), 'utf8') > MAX_SELECTION_BYTES
2062
+ )
2063
+ throw new Error('recovery selection input exceeds the contract bound');
2064
+ const selectionWaiter = deferred();
2065
+ pending.auxIds.push(
2066
+ sendOwned(
2067
+ 'mcpServer/tool/call',
2068
+ {
2069
+ threadId,
2070
+ server: brainServer,
2071
+ tool: 'brain_context_select',
2072
+ arguments: selectionInput,
2073
+ },
2074
+ 'select',
2075
+ threadId,
2076
+ selectionWaiter,
2077
+ ),
2078
+ );
2079
+ const selectionResponse = await selectionWaiter.promise;
2080
+ const selectionError = parseContextError(selectionResponse);
2081
+ if (!isRecord(selectionResponse) || selectionResponse.isError === true) {
2082
+ if (selectionError?.quota === true) {
2083
+ emitNativeFallback(
2084
+ threadId,
2085
+ pending,
2086
+ 'admission_denied',
2087
+ selectionError.correlationId,
2088
+ );
2089
+ nativeFallback = true;
2090
+ } else if (selectionError !== null) {
2091
+ emitNativeFallback(
2092
+ threadId,
2093
+ pending,
2094
+ 'provider_unavailable',
2095
+ selectionError.correlationId,
2096
+ );
2097
+ nativeFallback = true;
2098
+ } else {
2099
+ emitDiagnostic({
2100
+ phase: 'selection',
2101
+ stage: 'selection',
2102
+ reason: 'request_failed',
2103
+ threadId,
2104
+ operationId: pending.operationId,
2105
+ correlationId: selectionError?.correlationId,
2106
+ });
2107
+ throw new Error('recovery selection MCP call failed');
2108
+ }
2109
+ }
2110
+ if (nativeFallback) {
2111
+ emitDiagnostic({
2112
+ phase: 'selection',
2113
+ stage: 'selection',
2114
+ reason: 'admission_denied',
2115
+ threadId,
2116
+ operationId: pending.operationId,
2117
+ });
2118
+ } else {
2119
+ const selectionPayload = responseStructuredContent(selectionResponse);
2120
+ const decisions = parseSelection(selectionPayload, selectionInput, candidates, now());
2121
+ const selectionStats =
2122
+ isRecord(selectionPayload) && isRecord(selectionPayload.stats)
2123
+ ? selectionPayload.stats
2124
+ : {};
2125
+ telemetryFor(threadId).selectionCompleted({
2126
+ operationId: pending.operationId,
2127
+ counts: {
2128
+ eligible: Number.isSafeInteger(selectionStats.eligible_count)
2129
+ ? selectionStats.eligible_count
2130
+ : candidates.items.filter((item) => !item.protected).length,
2131
+ protected: Number.isSafeInteger(selectionStats.protected_count)
2132
+ ? selectionStats.protected_count
2133
+ : candidates.items.filter((item) => item.protected).length,
2134
+ selected: Number.isSafeInteger(selectionStats.selected_count)
2135
+ ? selectionStats.selected_count
2136
+ : decisions.filter((item) => item.keep).length,
2137
+ },
2138
+ });
2139
+ if (selectionInput.schema_version === 2) {
2140
+ emitDiagnostic({
2141
+ phase: 'selection_accepted',
2142
+ schemaVersion: 2,
2143
+ optionsHash:
2144
+ isRecord(selectionPayload) && typeof selectionPayload.options_hash === 'string'
2145
+ ? selectionPayload.options_hash
2146
+ : null,
2147
+ fittingStage:
2148
+ isRecord(selectionPayload) && isRecord(selectionPayload.fitting)
2149
+ ? selectionPayload.fitting.stage
2150
+ : null,
2151
+ batchCount:
2152
+ isRecord(selectionPayload) && isRecord(selectionPayload.stats)
2153
+ ? selectionPayload.stats.batch_count
2154
+ : null,
2155
+ providerModel:
2156
+ isRecord(selectionPayload) &&
2157
+ (typeof selectionPayload.provider_model === 'string' ||
2158
+ selectionPayload.provider_model === null)
2159
+ ? selectionPayload.provider_model
2160
+ : null,
2161
+ counts: {
2162
+ provider_eligible_count: candidates.items.filter((item) => !item.protected)
2163
+ .length,
2164
+ protected_candidate_count: candidates.items.filter((item) => item.protected)
2165
+ .length,
2166
+ protected_bundle_source_count: candidates.items
2167
+ .filter((item) => Array.isArray(item.sourceRefs))
2168
+ .reduce((count, item) => count + item.sourceRefs.length, 0),
2169
+ },
2170
+ });
2171
+ }
2172
+ const rereadWaiter = deferred();
2173
+ const rereadId = sendOwned(
2174
+ 'thread/read',
2175
+ { threadId, includeTurns: true },
2176
+ 'read',
2177
+ threadId,
2178
+ rereadWaiter,
2179
+ );
2180
+ pending.auxIds.push(rereadId);
2181
+ const reread = validateThreadRead(await rereadWaiter.promise, threadId);
2182
+ if (hashJson(reread) !== sourceRevision)
2183
+ throw new Error('recovery source changed during selection');
2184
+ assertCurrent(threadId, state, pending, revision);
2185
+ recoveryNotes = recoveryNotesText(
2186
+ threadId,
2187
+ sourceRevision,
2188
+ selectionInput.request_id,
2189
+ snapshot.capturedAt,
2190
+ relative(recoveryDir, immutableCapturePath).replaceAll('\\', '/'),
2191
+ resolve(recoveryDir),
2192
+ candidates,
2193
+ decisions,
2194
+ );
2195
+ pending.stage = 'notes';
2196
+ await atomicWriteTextPrivate(
2197
+ resolve(recoveryDir, `NOTES.${strictThreadName(threadId)}.md`),
2198
+ recoveryNotes,
2199
+ );
2200
+ telemetryFor(threadId).notesWritten({ operationId: pending.operationId });
2201
+ state.recovery = {
2202
+ version: 1,
2203
+ status: 'awaiting_native',
2204
+ threadId,
2205
+ sourceTurnId,
2206
+ sourceRevision,
2207
+ requestId: selectionInput.request_id,
2208
+ notes: recoveryNotes,
2209
+ notesHash: hashText(recoveryNotes),
2210
+ notesFile: `NOTES.${strictThreadName(threadId)}.md`,
2211
+ itemId: `recovery-${sourceRevision}`,
2212
+ operationId: pending.operationId,
2213
+ createdAt: new Date(now()).toISOString(),
2214
+ };
2215
+ state.recoveryLoaded = true;
2216
+ await persistRecoveryState(threadId, state.recovery);
2217
+ emitDiagnostic({
2218
+ phase: 'selection',
2219
+ stage: 'selection',
2220
+ reason: 'completed',
2221
+ threadId,
2222
+ operationId: pending.operationId,
2223
+ correlationId: selectionPayload.correlation_id,
2224
+ });
2225
+ }
1233
2226
  }
1234
- const rereadWaiter = deferred();
1235
- const rereadId = sendOwned(
1236
- 'thread/read',
1237
- { threadId, includeTurns: true },
1238
- 'read',
1239
- threadId,
1240
- rereadWaiter,
1241
- );
1242
- pending.auxIds.push(rereadId);
1243
- const reread = validateThreadRead(await rereadWaiter.promise, threadId);
1244
- if (hashJson(reread) !== sourceRevision)
1245
- throw new Error('recovery source changed during selection');
1246
- assertCurrent(threadId, state, pending, revision);
1247
- recoveryNotes = recoveryNotesText(
1248
- threadId,
1249
- sourceRevision,
1250
- selectionInput.request_id,
1251
- snapshot.capturedAt,
1252
- relative(recoveryDir, immutableCapturePath).replaceAll('\\', '/'),
1253
- resolve(recoveryDir),
1254
- candidates,
1255
- decisions,
1256
- );
1257
- await atomicWriteTextPrivate(
1258
- resolve(recoveryDir, `NOTES.${strictThreadName(threadId)}.md`),
1259
- recoveryNotes,
1260
- );
1261
- state.recovery = {
1262
- version: 1,
1263
- status: 'awaiting_native',
1264
- threadId,
1265
- sourceTurnId,
1266
- sourceRevision,
1267
- requestId: selectionInput.request_id,
1268
- notes: recoveryNotes,
1269
- notesHash: hashText(recoveryNotes),
1270
- notesFile: `NOTES.${strictThreadName(threadId)}.md`,
1271
- itemId: `recovery-${sourceRevision}`,
1272
- createdAt: new Date(now()).toISOString(),
1273
- };
1274
- state.recoveryLoaded = true;
1275
- await persistRecoveryState(threadId, state.recovery);
1276
2227
  }
1277
2228
  assertCurrent(threadId, state, pending, revision);
1278
2229
  }
2230
+ pending.stage = 'native';
1279
2231
  pending.compactId = sendOwned('thread/compact/start', { threadId }, 'compact', threadId);
1280
2232
  pending.lifecycleArmed = true;
1281
2233
  await completion.promise;
@@ -1297,17 +2249,32 @@ export function createCodexContextProxy({
1297
2249
  completedAt: new Date(now()).toISOString(),
1298
2250
  };
1299
2251
  await persistRecoveryState(threadId, state.recovery);
2252
+ telemetryFor(threadId).nativeCompleted({ operationId: pending.operationId });
1300
2253
  await attachRecovery(threadId, state);
1301
- onOutcome({
2254
+ emitOutcome({
1302
2255
  threadId,
1303
2256
  status: 'native_completed',
1304
2257
  turnId: pending.turnId,
1305
- recoveryStatus: state.recovery?.status ?? 'pending_attachment',
2258
+ operationId: pending.operationId,
2259
+ recoveryStatus: 'pending_attachment',
2260
+ telemetryAlreadyEmitted: true,
1306
2261
  });
1307
2262
  } else {
1308
- onOutcome({ threadId, status: 'completed', turnId: pending.turnId });
2263
+ emitOutcome({
2264
+ threadId,
2265
+ status: 'completed',
2266
+ operationId: pending.operationId,
2267
+ turnId: pending.turnId,
2268
+ });
1309
2269
  }
1310
- } catch {
2270
+ } catch (error) {
2271
+ emitDiagnostic({
2272
+ phase: 'operation_failed',
2273
+ stage: pending.stage ?? 'native',
2274
+ reason: diagnosticReasonForError(error),
2275
+ threadId,
2276
+ operationId: pending.operationId,
2277
+ });
1311
2278
  fail(threadId, 'checkpoint, request, or native completion failed');
1312
2279
  } finally {
1313
2280
  if (pending.timer) pending.timer();
@@ -1328,7 +2295,13 @@ export function createCodexContextProxy({
1328
2295
  const owned = ownedRequests.get(message.id);
1329
2296
  ownedRequests.delete(message.id);
1330
2297
  retire(message.id);
1331
- if (owned.kind === 'read' || owned.kind === 'select' || owned.kind === 'inject') {
2298
+ if (
2299
+ owned.kind === 'read' ||
2300
+ owned.kind === 'select' ||
2301
+ owned.kind === 'budget' ||
2302
+ owned.kind === 'inject' ||
2303
+ owned.kind === 'telemetry'
2304
+ ) {
1332
2305
  const waiter = internalReads.get(message.id);
1333
2306
  internalReads.delete(message.id);
1334
2307
  if (waiter) {
@@ -1359,6 +2332,12 @@ export function createCodexContextProxy({
1359
2332
  const state = threadState(message.params.threadId);
1360
2333
  state.status =
1361
2334
  typeof message.params.status.type === 'string' ? message.params.status.type : 'unknown';
2335
+ if (!state.telemetryConnected) {
2336
+ state.telemetryConnected = true;
2337
+ telemetryFor(message.params.threadId).connected({
2338
+ triggerPercent: effectiveThreshold,
2339
+ });
2340
+ }
1362
2341
  state.idle = state.status === 'idle';
1363
2342
  if (!state.idle) invalidateAttachment(state);
1364
2343
  if (state.idle) void requestCompaction(message.params.threadId, state);
@@ -1384,6 +2363,16 @@ export function createCodexContextProxy({
1384
2363
  const state = threadState(params.threadId);
1385
2364
  state.usage = params.tokenUsage;
1386
2365
  state.usageReceivedAt = now();
2366
+ telemetryFor(params.threadId).usage({
2367
+ measurement: {
2368
+ unit: 'tokens',
2369
+ used: params.tokenUsage.last.totalTokens,
2370
+ ...(hasKnownContextWindow(params.tokenUsage)
2371
+ ? { capacity: params.tokenUsage.modelContextWindow }
2372
+ : {}),
2373
+ },
2374
+ triggerPercent: effectiveThreshold,
2375
+ });
1387
2376
  state.turnId = params.turnId;
1388
2377
  state.revision = `${state.turnId ?? 'unknown'}:${String(params.tokenUsage.last?.totalTokens ?? '')}:${String(params.tokenUsage.modelContextWindow ?? '')}`;
1389
2378
  if (
@@ -1427,6 +2416,13 @@ export function createCodexContextProxy({
1427
2416
  pending.turnId = typeof p.turnId === 'string' ? p.turnId : null;
1428
2417
  pending.started = true;
1429
2418
  pending.itemId = p.item.id ?? null;
2419
+ emitDiagnostic({
2420
+ phase: 'native_started',
2421
+ stage: 'native',
2422
+ reason: 'requested',
2423
+ threadId: p.threadId,
2424
+ operationId: pending.operationId,
2425
+ });
1430
2426
  }
1431
2427
  }
1432
2428
  if (
@@ -1445,6 +2441,13 @@ export function createCodexContextProxy({
1445
2441
  pending.itemId === (p.item.id ?? null)
1446
2442
  ) {
1447
2443
  pending.itemCompleted = true;
2444
+ emitDiagnostic({
2445
+ phase: 'native_item_completed',
2446
+ stage: 'native',
2447
+ reason: 'completed',
2448
+ threadId: p.threadId,
2449
+ operationId: pending.operationId,
2450
+ });
1448
2451
  if (pending.turnCompleted) pending.deferred.resolve();
1449
2452
  }
1450
2453
  }
@@ -1460,7 +2463,16 @@ export function createCodexContextProxy({
1460
2463
  if (pending?.lifecycleArmed && pending.turnId === turn.id) {
1461
2464
  if (turn.status === 'completed') pending.turnCompleted = true;
1462
2465
  else fail(message.params.threadId, 'native compaction turn failed');
1463
- if (pending.itemCompleted && pending.turnCompleted) pending.deferred.resolve();
2466
+ if (pending.itemCompleted && pending.turnCompleted) {
2467
+ emitDiagnostic({
2468
+ phase: 'native_completed',
2469
+ stage: 'native',
2470
+ reason: 'completed',
2471
+ threadId: message.params.threadId,
2472
+ operationId: pending.operationId,
2473
+ });
2474
+ pending.deferred.resolve();
2475
+ }
1464
2476
  }
1465
2477
  }
1466
2478
  if (
@@ -1497,9 +2509,10 @@ export function createCodexContextProxy({
1497
2509
  };
1498
2510
  const processLine = (line) => {
1499
2511
  if (Buffer.byteLength(line) > MAX_LINE_BYTES) {
1500
- if (line.includes('"id":"ai-brain-context-')) {
2512
+ const ownedId = internalRequestId(line);
2513
+ if (ownedId !== null) {
1501
2514
  stderr.write('codex-context: oversized internal response rejected\n');
1502
- failAll('oversized internal response');
2515
+ failOversizedInternal(ownedId);
1503
2516
  } else forward(`${line}\n`);
1504
2517
  return;
1505
2518
  }
@@ -1511,29 +2524,63 @@ export function createCodexContextProxy({
1511
2524
  }
1512
2525
  if (forwardLine) forward(`${line}\n`);
1513
2526
  };
2527
+ function internalRequestId(line) {
2528
+ const match =
2529
+ /^\s*\{\s*(?:"id"\s*:\s*"(ai-brain-context-[A-Za-z0-9._:-]{1,128})"|"jsonrpc"\s*:\s*"2\.0"\s*,\s*"id"\s*:\s*"(ai-brain-context-[A-Za-z0-9._:-]{1,128})")/u.exec(
2530
+ line,
2531
+ );
2532
+ return match?.[1] ?? match?.[2] ?? null;
2533
+ }
2534
+ function failCompactions(reason) {
2535
+ for (const [threadId, state] of threads) {
2536
+ if (state.pending) fail(threadId, reason);
2537
+ }
2538
+ }
2539
+ function failOversizedInternal(id) {
2540
+ const owned = ownedRequests.get(id);
2541
+ if (!owned) {
2542
+ failCompactions('oversized internal response');
2543
+ return;
2544
+ }
2545
+ cleanupRequest(id);
2546
+ if (owned.kind === 'telemetry') return;
2547
+ fail(owned.threadId, 'oversized internal response');
2548
+ }
1514
2549
  child.stdout.on('data', (chunk) => {
1515
2550
  if (stopped) return;
1516
- inputBuffer += decoder.write(chunk);
2551
+ const decoded = decoder.write(chunk);
2552
+ if (discardOversized) {
2553
+ const newline = decoded.indexOf('\n');
2554
+ if (newline < 0) return;
2555
+ discardOversized = false;
2556
+ inputBuffer = decoded.slice(newline + 1);
2557
+ } else inputBuffer += decoded;
1517
2558
  while (true) {
1518
2559
  const newline = inputBuffer.indexOf('\n');
1519
2560
  if (newline < 0) break;
1520
2561
  const line = inputBuffer.slice(0, newline);
1521
2562
  inputBuffer = inputBuffer.slice(newline + 1);
1522
- if (passthrough) {
2563
+ if (discardOversized) {
2564
+ discardOversized = false;
2565
+ } else if (passthrough) {
1523
2566
  forward(`${line}\n`);
1524
2567
  passthrough = false;
1525
- } else if (discardOversized) {
1526
- discardOversized = false;
1527
2568
  } else processLine(line);
1528
2569
  }
1529
2570
  if (Buffer.byteLength(inputBuffer) > MAX_LINE_BYTES) {
1530
- if (inputBuffer.includes('"id":"ai-brain-context-')) {
2571
+ if (passthrough) {
2572
+ forward(inputBuffer);
2573
+ inputBuffer = '';
2574
+ } else if (internalRequestId(inputBuffer) !== null) {
1531
2575
  stderr.write('codex-context: oversized internal response rejected\n');
1532
- failAll('oversized internal response');
2576
+ failOversizedInternal(internalRequestId(inputBuffer));
1533
2577
  discardOversized = true;
1534
- } else forward(inputBuffer);
1535
- inputBuffer = '';
1536
- if (!passthrough) passthrough = true;
2578
+ inputBuffer = '';
2579
+ } else {
2580
+ forward(inputBuffer);
2581
+ inputBuffer = '';
2582
+ passthrough = true;
2583
+ }
1537
2584
  }
1538
2585
  });
1539
2586
  child.stdout.on('end', () => {
@@ -1553,17 +2600,45 @@ export function createCodexContextProxy({
1553
2600
  failAll('app-server disconnected');
1554
2601
  emitDiagnostic({ phase: 'transport_close' });
1555
2602
  });
2603
+ // Native app-server stderr is operational logging and may contain private
2604
+ // paths, prompts or tool output. Drain the owned pipe without forwarding or
2605
+ // persisting its contents; protocol errors remain on stdout and process exit
2606
+ // status is still propagated by the launcher.
2607
+ if (child.stderr && typeof child.stderr.on === 'function') {
2608
+ child.stderr.on('data', () => undefined);
2609
+ child.stderr.resume?.();
2610
+ }
1556
2611
  return {
1557
2612
  threads,
1558
- stop() {
1559
- if (stopped) return;
2613
+ async stop() {
2614
+ if (stopped) {
2615
+ await Promise.all(
2616
+ [...diagnosticSinks.values()].map((sink) =>
2617
+ typeof sink.flush === 'function'
2618
+ ? Promise.resolve(sink.flush()).catch(() => undefined)
2619
+ : undefined,
2620
+ ),
2621
+ );
2622
+ return;
2623
+ }
2624
+ for (const reporter of telemetryByThread.values()) {
2625
+ reporter.disconnected();
2626
+ void reporter.flush();
2627
+ }
1560
2628
  stopped = true;
1561
2629
  failAll('adapter stopped');
2630
+ await Promise.all(
2631
+ [...diagnosticSinks.values()].map((sink) =>
2632
+ typeof sink.flush === 'function'
2633
+ ? Promise.resolve(sink.flush()).catch(() => undefined)
2634
+ : undefined,
2635
+ ),
2636
+ );
1562
2637
  },
1563
2638
  };
1564
2639
  }
1565
2640
 
1566
- function main() {
2641
+ async function main() {
1567
2642
  if (process.argv.includes('--help')) {
1568
2643
  process.stderr.write(
1569
2644
  'Usage: node scripts/codex-context-compaction.mjs [app-server arguments...]\nStarts an owned Codex app-server and requests native compaction at CODEX_CONTEXT_COMPACT_AT (default 85).\n',
@@ -1572,19 +2647,58 @@ function main() {
1572
2647
  }
1573
2648
  const policy = parseContextPolicy();
1574
2649
  const command = process.env.CODEX_BIN ?? 'codex';
1575
- const child = spawn(command, ['app-server', ...process.argv.slice(2)], {
1576
- stdio: ['pipe', 'pipe', 'inherit'],
1577
- env: codexChildEnvironment(),
1578
- });
1579
- createCodexContextProxy({
2650
+ const profile = process.env.CODEX_APP_SERVER_PROFILE;
2651
+ if (profile !== undefined && !/^[A-Za-z0-9._-]{1,64}$/u.test(profile)) {
2652
+ throw new Error('CODEX_APP_SERVER_PROFILE is invalid');
2653
+ }
2654
+ const child = spawn(
2655
+ command,
2656
+ [
2657
+ ...(profile === undefined ? [] : ['--profile', profile]),
2658
+ 'app-server',
2659
+ ...process.argv.slice(2),
2660
+ ],
2661
+ {
2662
+ stdio: ['pipe', 'pipe', 'pipe'],
2663
+ env: codexChildEnvironment(),
2664
+ },
2665
+ );
2666
+ let diagnosticFactory = null;
2667
+ const diagnosticModule = process.env.CODEX_CONTEXT_DIAGNOSTICS_MODULE;
2668
+ if (
2669
+ process.env.CODEX_CONTEXT_DIAGNOSTICS_ENABLED === '1' &&
2670
+ typeof diagnosticModule === 'string' &&
2671
+ diagnosticModule.length > 0
2672
+ ) {
2673
+ try {
2674
+ const module = await import(pathToFileURL(resolve(diagnosticModule)).href);
2675
+ if (typeof module.createCodexDiagnosticSink === 'function') {
2676
+ const stateDir = process.env.CODEX_CONTEXT_DIAGNOSTICS_DIR ?? process.cwd();
2677
+ diagnosticFactory = (threadId) =>
2678
+ module.createCodexDiagnosticSink({
2679
+ stateDir,
2680
+ sessionId: threadId,
2681
+ clientVersion: process.env.CODEX_CLIENT_VERSION ?? '0.161.0',
2682
+ enabled: true,
2683
+ });
2684
+ }
2685
+ } catch {
2686
+ diagnosticFactory = null;
2687
+ }
2688
+ }
2689
+ const proxy = createCodexContextProxy({
1580
2690
  child,
1581
2691
  threshold: numberEnv('CODEX_CONTEXT_COMPACT_AT', DEFAULT_THRESHOLD),
1582
2692
  timeoutMs: numberEnv('CODEX_CONTEXT_COMPACT_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
1583
2693
  policy,
2694
+ createDiagnosticSink: diagnosticFactory,
1584
2695
  });
1585
2696
  process.stdin.pipe(child.stdin);
1586
2697
  process.stdin.on('end', () => child.stdin.end());
1587
- child.on('exit', (code, signal) => (process.exitCode = code ?? (signal ? 1 : 0)));
2698
+ child.on('exit', async (code, signal) => {
2699
+ await proxy.stop();
2700
+ process.exitCode = code ?? (signal ? 1 : 0);
2701
+ });
1588
2702
  }
1589
2703
  if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname))
1590
- main();
2704
+ void main();