@brunobracaioli/aibrain 0.3.0-beta.16 → 0.3.0-beta.18
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 +6 -1
- package/dist/assets/claude/hooks/context-compaction.ts +132 -16
- package/dist/assets/claude/hooks/native-host-contract.test.ts +9 -0
- package/dist/assets/codex-context-compaction.mjs +498 -95
- package/dist/claude-observation.mjs +2 -1
- package/dist/cli.mjs +1 -1
- package/dist/codex-compatibility.mjs +106 -92
- package/dist/codex-protocol-fingerprint.json +10 -10
- package/dist/package.json +1 -1
- package/dist/platform.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ notes or lock files to work around a reported storage error.
|
|
|
11
11
|
|
|
12
12
|
The reviewed native compatibility set is Claude Code 2.1.294, 2.1.295, 2.1.296,
|
|
13
13
|
2.1.299, 2.1.300, 2.1.301, 2.1.302 or 2.1.303, and Codex CLI 0.161.0,
|
|
14
|
-
0.162.0, 0.162.1 or 0.
|
|
14
|
+
0.162.0, 0.162.1, 0.163.0 or 0.164.0. A newer stable Codex 0.x release may continue only
|
|
15
15
|
after its locally generated experimental protocol schemas and transport flags
|
|
16
16
|
match the bundled adapter contract; prereleases, older releases and new majors
|
|
17
17
|
remain rejected. Starting with beta14, newer stable Claude 2.1 patches can
|
|
@@ -22,6 +22,11 @@ equivalence with a tested native release. Unknown older patches, prereleases
|
|
|
22
22
|
and other major/minor families remain rejected. No credentials, conversations,
|
|
23
23
|
model requests or JEV calls are used by these compatibility probes.
|
|
24
24
|
|
|
25
|
+
Beta17 refreshes the Codex contract to 0.164.0, including its image-generation
|
|
26
|
+
record formats. Native launch/resume qualification is separate from a new
|
|
27
|
+
model/JEV compaction qualification. A later native release that changes the
|
|
28
|
+
consumed contract can still require an AI Brain compatibility update.
|
|
29
|
+
|
|
25
30
|
The Codex terminal bridge permits the native session picker's separate read-only
|
|
26
31
|
connection while keeping the primary conversation and monitor intact. A picker
|
|
27
32
|
cannot compact, start a turn, inject history or take ownership of the primary
|
|
@@ -107,6 +107,7 @@ type HookApi = {
|
|
|
107
107
|
};
|
|
108
108
|
};
|
|
109
109
|
type HookOptions = Record<string, unknown>;
|
|
110
|
+
type MonotonicClock = () => number | undefined;
|
|
110
111
|
type PreservationMode = 'preserve_internal' | 'upstream_compatible';
|
|
111
112
|
type CompactionStrategy = 'direct_experimental' | 'native_summary_notes';
|
|
112
113
|
type ToolPolicy = 'read_only' | 'compatible';
|
|
@@ -150,7 +151,7 @@ type Register = (
|
|
|
150
151
|
) => void;
|
|
151
152
|
|
|
152
153
|
const MAX_RESULT_BYTES = 256 * 1024;
|
|
153
|
-
const CLAUDE_HOOK_VERSION = '0.3.0-beta.
|
|
154
|
+
const CLAUDE_HOOK_VERSION = '0.3.0-beta.18';
|
|
154
155
|
const MAX_DIAGNOSTIC_BYTE_COUNT = 128 * 1024 * 1024;
|
|
155
156
|
const COOLDOWN_MS = 60_000;
|
|
156
157
|
const MAX_TRUNCATED_CODE_POINTS = 300;
|
|
@@ -686,6 +687,8 @@ type ContextBudgetSnapshot = {
|
|
|
686
687
|
readonly accountInFlight: boolean;
|
|
687
688
|
readonly retryAfterMs: number;
|
|
688
689
|
readonly expiresAt: string;
|
|
690
|
+
readonly requestStartedAtMonotonic: number;
|
|
691
|
+
readonly lifetimeMs: number;
|
|
689
692
|
readonly windowEnd: string;
|
|
690
693
|
readonly correlationId: string;
|
|
691
694
|
};
|
|
@@ -699,7 +702,25 @@ type ContextBudgetRead =
|
|
|
699
702
|
readonly error?: Error | undefined;
|
|
700
703
|
};
|
|
701
704
|
|
|
702
|
-
function
|
|
705
|
+
function monotonicNow(): number | undefined {
|
|
706
|
+
const host = globalThis as unknown as {
|
|
707
|
+
readonly performance?: { readonly now?: () => number };
|
|
708
|
+
};
|
|
709
|
+
const now = host.performance?.now;
|
|
710
|
+
if (typeof now !== 'function') return undefined;
|
|
711
|
+
try {
|
|
712
|
+
const value = now.call(host.performance);
|
|
713
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
714
|
+
} catch {
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function parseContextBudget(
|
|
720
|
+
value: unknown,
|
|
721
|
+
requestStartedAt: number,
|
|
722
|
+
responseReceivedAt: number,
|
|
723
|
+
): ContextBudgetSnapshot | undefined {
|
|
703
724
|
if (!isRecord(value)) return undefined;
|
|
704
725
|
const allowed = [
|
|
705
726
|
'schema_version',
|
|
@@ -727,9 +748,22 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
727
748
|
!Number.isFinite(Date.parse(value.window_end))
|
|
728
749
|
)
|
|
729
750
|
return undefined;
|
|
751
|
+
const serverTime = Date.parse(value.server_time);
|
|
730
752
|
const expiresAt = Date.parse(value.expires_at);
|
|
731
753
|
const windowEnd = Date.parse(value.window_end);
|
|
732
|
-
|
|
754
|
+
const snapshotLifetimeMs = expiresAt - serverTime;
|
|
755
|
+
const requestElapsedMs = responseReceivedAt - requestStartedAt;
|
|
756
|
+
if (
|
|
757
|
+
!Number.isFinite(requestStartedAt) ||
|
|
758
|
+
!Number.isFinite(responseReceivedAt) ||
|
|
759
|
+
requestStartedAt < 0 ||
|
|
760
|
+
requestElapsedMs < 0 ||
|
|
761
|
+
!Number.isSafeInteger(snapshotLifetimeMs) ||
|
|
762
|
+
snapshotLifetimeMs <= 0 ||
|
|
763
|
+
snapshotLifetimeMs > BUDGET_SNAPSHOT_MAX_AGE_MS ||
|
|
764
|
+
requestElapsedMs >= snapshotLifetimeMs ||
|
|
765
|
+
windowEnd < expiresAt
|
|
766
|
+
)
|
|
733
767
|
return undefined;
|
|
734
768
|
const nonnegative = (candidate: unknown): candidate is number =>
|
|
735
769
|
typeof candidate === 'number' && Number.isSafeInteger(candidate) && candidate >= 0;
|
|
@@ -742,10 +776,8 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
742
776
|
!nonnegative(value.retry_after_ms) ||
|
|
743
777
|
value.retry_after_ms > 3_600_000 ||
|
|
744
778
|
typeof value.account_in_flight !== 'boolean' ||
|
|
745
|
-
|
|
746
|
-
value.
|
|
747
|
-
(value.provider_model !== null &&
|
|
748
|
-
(typeof value.provider_model !== 'string' || value.provider_model.length > 128))
|
|
779
|
+
value.provider_batches !== 0 ||
|
|
780
|
+
value.provider_model !== null
|
|
749
781
|
)
|
|
750
782
|
return undefined;
|
|
751
783
|
const correlationId = boundedSafeIdentifier(value.correlation_id);
|
|
@@ -758,6 +790,8 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
758
790
|
accountInFlight: value.account_in_flight,
|
|
759
791
|
retryAfterMs: value.retry_after_ms,
|
|
760
792
|
expiresAt: value.expires_at,
|
|
793
|
+
requestStartedAtMonotonic: requestStartedAt,
|
|
794
|
+
lifetimeMs: snapshotLifetimeMs,
|
|
761
795
|
windowEnd: value.window_end,
|
|
762
796
|
correlationId,
|
|
763
797
|
};
|
|
@@ -777,8 +811,14 @@ function mcpStructuredValue(response: McpResult, maxTextBytes = 8 * 1024): unkno
|
|
|
777
811
|
}
|
|
778
812
|
}
|
|
779
813
|
|
|
780
|
-
async function readContextBudget(
|
|
814
|
+
export async function readContextBudget(
|
|
815
|
+
api: HookApi,
|
|
816
|
+
server: string,
|
|
817
|
+
now: MonotonicClock = monotonicNow,
|
|
818
|
+
): Promise<ContextBudgetRead> {
|
|
781
819
|
try {
|
|
820
|
+
const requestStartedAt = now();
|
|
821
|
+
if (requestStartedAt === undefined) return { ok: false, kind: 'provider_unavailable' };
|
|
782
822
|
const call = api.mcp.call(server, 'brain_context_budget', { schema_version: 1 });
|
|
783
823
|
const hostTimers = globalThis as unknown as {
|
|
784
824
|
setTimeout?: (callback: () => void, delay: number) => unknown;
|
|
@@ -792,8 +832,12 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
792
832
|
resolve(undefined);
|
|
793
833
|
}, BUDGET_PREFLIGHT_TIMEOUT_MS);
|
|
794
834
|
});
|
|
795
|
-
|
|
796
|
-
|
|
835
|
+
let response: McpResult | undefined;
|
|
836
|
+
try {
|
|
837
|
+
response = await Promise.race([call, timeout]);
|
|
838
|
+
} finally {
|
|
839
|
+
if (timer !== undefined) hostTimers.clearTimeout?.(timer);
|
|
840
|
+
}
|
|
797
841
|
if (response === undefined)
|
|
798
842
|
return { ok: false, kind: timedOut ? 'timeout' : 'invalid_response' };
|
|
799
843
|
if (response.isError === true) {
|
|
@@ -804,7 +848,15 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
804
848
|
error,
|
|
805
849
|
};
|
|
806
850
|
}
|
|
807
|
-
const
|
|
851
|
+
const responseReceivedAt = now();
|
|
852
|
+
if (responseReceivedAt === undefined) return { ok: false, kind: 'provider_unavailable' };
|
|
853
|
+
if (responseReceivedAt - requestStartedAt > BUDGET_PREFLIGHT_TIMEOUT_MS)
|
|
854
|
+
return { ok: false, kind: 'timeout' };
|
|
855
|
+
const snapshot = parseContextBudget(
|
|
856
|
+
mcpStructuredValue(response),
|
|
857
|
+
requestStartedAt,
|
|
858
|
+
responseReceivedAt,
|
|
859
|
+
);
|
|
808
860
|
return snapshot === undefined
|
|
809
861
|
? { ok: false, kind: 'invalid_response' }
|
|
810
862
|
: { ok: true, snapshot };
|
|
@@ -823,6 +875,25 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
823
875
|
}
|
|
824
876
|
}
|
|
825
877
|
|
|
878
|
+
export function contextBudgetIsFresh(
|
|
879
|
+
snapshot: ContextBudgetSnapshot,
|
|
880
|
+
now = monotonicNow(),
|
|
881
|
+
): boolean {
|
|
882
|
+
const elapsedMs = now === undefined ? Number.NaN : now - snapshot.requestStartedAtMonotonic;
|
|
883
|
+
return (
|
|
884
|
+
now !== undefined &&
|
|
885
|
+
Number.isFinite(now) &&
|
|
886
|
+
Number.isFinite(snapshot.requestStartedAtMonotonic) &&
|
|
887
|
+
Number.isSafeInteger(snapshot.lifetimeMs) &&
|
|
888
|
+
snapshot.requestStartedAtMonotonic >= 0 &&
|
|
889
|
+
snapshot.lifetimeMs > 0 &&
|
|
890
|
+
snapshot.lifetimeMs <= BUDGET_SNAPSHOT_MAX_AGE_MS &&
|
|
891
|
+
Number.isFinite(elapsedMs) &&
|
|
892
|
+
elapsedMs >= 0 &&
|
|
893
|
+
elapsedMs < snapshot.lifetimeMs
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
826
897
|
function safeRpcStatus(error: unknown): number | undefined {
|
|
827
898
|
const record = isRecord(error) ? error : {};
|
|
828
899
|
const candidate = record.rpc_status ?? record.status ?? record.code;
|
|
@@ -930,6 +1001,7 @@ const CHECKPOINT_ERRORS: readonly CheckpointError[] = [
|
|
|
930
1001
|
type CompactErrorCode =
|
|
931
1002
|
| 'state_budget_exceeded'
|
|
932
1003
|
| 'shared_admission_exceeded'
|
|
1004
|
+
| 'budget_response_invalid'
|
|
933
1005
|
| 'provider_budget_exceeded'
|
|
934
1006
|
| 'provider_unavailable'
|
|
935
1007
|
| 'provider_auth_denied'
|
|
@@ -944,6 +1016,7 @@ type CompactErrorCode =
|
|
|
944
1016
|
const COMPACT_ERROR_CODES: readonly CompactErrorCode[] = [
|
|
945
1017
|
'state_budget_exceeded',
|
|
946
1018
|
'shared_admission_exceeded',
|
|
1019
|
+
'budget_response_invalid',
|
|
947
1020
|
'provider_budget_exceeded',
|
|
948
1021
|
'provider_unavailable',
|
|
949
1022
|
'provider_auth_denied',
|
|
@@ -5586,7 +5659,11 @@ async function runShadowPreview(
|
|
|
5586
5659
|
);
|
|
5587
5660
|
}
|
|
5588
5661
|
|
|
5589
|
-
|
|
5662
|
+
const registerWithClock = (
|
|
5663
|
+
on: Parameters<Register>[0],
|
|
5664
|
+
options: HookOptions,
|
|
5665
|
+
monotonicClock: MonotonicClock,
|
|
5666
|
+
): void => {
|
|
5590
5667
|
const enabled = options.enabled === true;
|
|
5591
5668
|
const disclosed = options.disclosed === true;
|
|
5592
5669
|
const preservationMode: PreservationMode =
|
|
@@ -6175,7 +6252,7 @@ export const register: Register = (on, options) => {
|
|
|
6175
6252
|
markDiagnostic('budget', 'provider_not_attempted', {
|
|
6176
6253
|
item_count: built.pairs.filter((pair) => !pair.protectedPair).length,
|
|
6177
6254
|
});
|
|
6178
|
-
const budgetRead = await readContextBudget(api, brainServer);
|
|
6255
|
+
const budgetRead = await readContextBudget(api, brainServer, monotonicClock);
|
|
6179
6256
|
if (!budgetRead.ok) {
|
|
6180
6257
|
const admission = budgetRead.kind === 'admission_denied';
|
|
6181
6258
|
const category: ObservationRecord['category'] = admission
|
|
@@ -6190,8 +6267,13 @@ export const register: Register = (on, options) => {
|
|
|
6190
6267
|
const errorMetadata: Partial<ContextErrorMetadata> = isRecord(budgetRead.error)
|
|
6191
6268
|
? (budgetRead.error as ContextErrorMetadata)
|
|
6192
6269
|
: {};
|
|
6270
|
+
const budgetErrorCode = admission
|
|
6271
|
+
? 'shared_admission_exceeded'
|
|
6272
|
+
: budgetRead.kind === 'invalid_response'
|
|
6273
|
+
? 'budget_response_invalid'
|
|
6274
|
+
: 'provider_unavailable';
|
|
6193
6275
|
markDiagnostic('budget', category, {
|
|
6194
|
-
compact_error_code:
|
|
6276
|
+
compact_error_code: budgetErrorCode,
|
|
6195
6277
|
...(typeof errorMetadata.admissionDetail === 'string'
|
|
6196
6278
|
? { admission_detail: errorMetadata.admissionDetail }
|
|
6197
6279
|
: {}),
|
|
@@ -6206,7 +6288,7 @@ export const register: Register = (on, options) => {
|
|
|
6206
6288
|
: {}),
|
|
6207
6289
|
});
|
|
6208
6290
|
markDiagnostic('fallback', category, {
|
|
6209
|
-
compact_error_code:
|
|
6291
|
+
compact_error_code: budgetErrorCode,
|
|
6210
6292
|
});
|
|
6211
6293
|
logFallback(
|
|
6212
6294
|
api,
|
|
@@ -6374,18 +6456,42 @@ export const register: Register = (on, options) => {
|
|
|
6374
6456
|
projection: providerProjection,
|
|
6375
6457
|
...(policyVersion === 2 ? { schema_version: 2, options: wireOptions } : {}),
|
|
6376
6458
|
};
|
|
6459
|
+
await persistDiagnostics();
|
|
6460
|
+
if (!contextBudgetIsFresh(budget, monotonicClock())) {
|
|
6461
|
+
markDiagnostic('budget', 'invalid_response', {
|
|
6462
|
+
compact_error_code: 'budget_response_invalid',
|
|
6463
|
+
});
|
|
6464
|
+
markDiagnostic('fallback', 'invalid_response', {
|
|
6465
|
+
compact_error_code: 'budget_response_invalid',
|
|
6466
|
+
});
|
|
6467
|
+
logFallback(api, 'budget_unavailable', undefined, policyVersion === 2);
|
|
6468
|
+
getTelemetry(api, brainServer, options, telemetryState).fallback(
|
|
6469
|
+
operationId,
|
|
6470
|
+
'budget_unavailable',
|
|
6471
|
+
);
|
|
6472
|
+
return continueWithTelemetryFlush({
|
|
6473
|
+
operationId,
|
|
6474
|
+
sessionRef,
|
|
6475
|
+
baselineHash: rereadBaselineHash,
|
|
6476
|
+
baselineCount: reread.length,
|
|
6477
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6478
|
+
activeOffset: baselineActiveOffset,
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6377
6481
|
stage = 'mcp_call';
|
|
6378
6482
|
markDiagnostic('provider_request', 'request_attempted', {
|
|
6379
6483
|
item_count: providerPairs.filter((pair) => !pair.protectedPair).length,
|
|
6380
6484
|
byte_count: byteLength(JSON.stringify(input)),
|
|
6381
6485
|
});
|
|
6382
|
-
await persistDiagnostics();
|
|
6383
6486
|
if (diagnosticMode)
|
|
6384
6487
|
api.ui.log(
|
|
6385
6488
|
`AI Brain context diagnostic ${JSON.stringify({ hook: 'session.compact', phase: 'mcp_started' })}`,
|
|
6386
6489
|
);
|
|
6387
6490
|
const call = api.mcp.call(brainServer, 'brain_context_compact', input);
|
|
6388
6491
|
pendingMcp = call;
|
|
6492
|
+
// The request has started, so persist its attempt without delaying the
|
|
6493
|
+
// budget freshness check or extending the provider deadline.
|
|
6494
|
+
void persistDiagnostics();
|
|
6389
6495
|
let timedOut = false;
|
|
6390
6496
|
const hostTimers = globalThis as unknown as {
|
|
6391
6497
|
setTimeout?: (callback: () => void, delay: number) => unknown;
|
|
@@ -6812,3 +6918,13 @@ export const register: Register = (on, options) => {
|
|
|
6812
6918
|
});
|
|
6813
6919
|
});
|
|
6814
6920
|
};
|
|
6921
|
+
|
|
6922
|
+
export const register: Register = (on, options) => registerWithClock(on, options, monotonicNow);
|
|
6923
|
+
|
|
6924
|
+
export function registerForTesting(
|
|
6925
|
+
on: Parameters<Register>[0],
|
|
6926
|
+
options: HookOptions,
|
|
6927
|
+
monotonicClock: MonotonicClock,
|
|
6928
|
+
): void {
|
|
6929
|
+
registerWithClock(on, options, monotonicClock);
|
|
6930
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { expect, test } from 'claude-code/testing';
|
|
2
2
|
|
|
3
3
|
const contractTest: Parameters<typeof test>[2] = async ($, on) => {
|
|
4
|
+
const monotonicStart = globalThis.performance?.now?.();
|
|
5
|
+
expect(Number.isFinite(monotonicStart)).toBe(true);
|
|
4
6
|
on('prompt.submit', () => ({ text: 'native-host-contract-ok' }));
|
|
5
7
|
on('tool.call', () => ({ result: { content: 'native-host-tool-ok' } }));
|
|
6
8
|
on('session.messages', () => ({ value: [] }));
|
|
@@ -34,6 +36,13 @@ const contractTest: Parameters<typeof test>[2] = async ($, on) => {
|
|
|
34
36
|
expect(tool.result.content).toBe('native-host-tool-ok');
|
|
35
37
|
expect(usageCalls).toBe(1);
|
|
36
38
|
expect(compactCalls).toBe(1);
|
|
39
|
+
const monotonicEnd = globalThis.performance?.now?.();
|
|
40
|
+
expect(Number.isFinite(monotonicEnd)).toBe(true);
|
|
41
|
+
expect(
|
|
42
|
+
typeof monotonicStart === 'number' &&
|
|
43
|
+
typeof monotonicEnd === 'number' &&
|
|
44
|
+
monotonicEnd >= monotonicStart,
|
|
45
|
+
).toBe(true);
|
|
37
46
|
};
|
|
38
47
|
|
|
39
48
|
test(
|