@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.
- package/README.md +48 -4
- package/dist/assets/claude/.claude-plugin/plugin.json +48 -0
- package/dist/assets/claude/hooks/context-compaction.ts +4702 -150
- package/dist/assets/claude/hooks/provider-window.js +263 -0
- package/dist/assets/claude/hooks/state-fitting.d.mts +47 -0
- package/dist/assets/claude/hooks/state-fitting.mjs +324 -0
- package/dist/assets/codex-context-compaction.mjs +1271 -157
- package/dist/assets/context-selection-fitting.mjs +194 -0
- package/dist/assets/context-state-fitting.mjs +324 -0
- package/dist/assets/context-telemetry-reporter.mjs +305 -0
- package/dist/assets/jev-paid-pilot-terminal.mjs +571 -125
- package/dist/claude-checkpoint.mjs +1338 -0
- package/dist/claude-observation.mjs +1224 -0
- package/dist/cli.mjs +215 -6
- package/dist/codex-diagnostics.mjs +411 -0
- package/dist/codex-permissions.mjs +590 -0
- package/dist/context-telemetry.mjs +257 -0
- package/dist/launch.mjs +332 -17
- package/dist/mcp.mjs +329 -7
- package/dist/monitor.mjs +11 -0
- package/dist/package.json +1 -1
- package/dist/platform.mjs +1 -1
- package/dist/state.mjs +107 -4
- package/dist/telemetry-report.mjs +133 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
237
|
-
|
|
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) =>
|
|
357
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1038
|
-
} else
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
)
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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: '
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
2254
|
+
emitOutcome({
|
|
1302
2255
|
threadId,
|
|
1303
2256
|
status: 'native_completed',
|
|
1304
2257
|
turnId: pending.turnId,
|
|
1305
|
-
|
|
2258
|
+
operationId: pending.operationId,
|
|
2259
|
+
recoveryStatus: 'pending_attachment',
|
|
2260
|
+
telemetryAlreadyEmitted: true,
|
|
1306
2261
|
});
|
|
1307
2262
|
} else {
|
|
1308
|
-
|
|
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 (
|
|
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)
|
|
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
|
-
|
|
2512
|
+
const ownedId = internalRequestId(line);
|
|
2513
|
+
if (ownedId !== null) {
|
|
1501
2514
|
stderr.write('codex-context: oversized internal response rejected\n');
|
|
1502
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
2576
|
+
failOversizedInternal(internalRequestId(inputBuffer));
|
|
1533
2577
|
discardOversized = true;
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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)
|
|
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
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
}
|
|
1579
|
-
|
|
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) =>
|
|
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();
|