@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 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.163.0. A newer stable Codex 0.x release may continue only
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.16';
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 parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnapshot | undefined {
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
- if (expiresAt <= now || expiresAt - now > BUDGET_SNAPSHOT_MAX_AGE_MS || windowEnd < expiresAt)
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
- !nonnegative(value.provider_batches) ||
746
- value.provider_batches > 4 ||
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(api: HookApi, server: string): Promise<ContextBudgetRead> {
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
- const response = await Promise.race([call, timeout]);
796
- if (timer !== undefined) hostTimers.clearTimeout?.(timer);
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 snapshot = parseContextBudget(mcpStructuredValue(response));
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
- export const register: Register = (on, options) => {
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: admission ? 'shared_admission_exceeded' : 'provider_unavailable',
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: admission ? 'shared_admission_exceeded' : 'provider_unavailable',
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(