@brunobracaioli/aibrain 0.3.0-beta.15 → 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
@@ -2,9 +2,16 @@
2
2
 
3
3
  Assisted AI Brain context beta client for Linux/WSL with Node.js 22 or newer.
4
4
 
5
+ Codex capture also requires the `flock` utility from `util-linux`, normally
6
+ included in Linux/WSL. The launcher and doctor check availability before a
7
+ monitored Codex session starts. Capture coordination is released by the kernel
8
+ after a process exits unexpectedly. An unidentifiable empty lock left by an
9
+ older client requires a verified support migration; do not delete checkpoints,
10
+ notes or lock files to work around a reported storage error.
11
+
5
12
  The reviewed native compatibility set is Claude Code 2.1.294, 2.1.295, 2.1.296,
6
13
  2.1.299, 2.1.300, 2.1.301, 2.1.302 or 2.1.303, and Codex CLI 0.161.0,
7
- 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
8
15
  after its locally generated experimental protocol schemas and transport flags
9
16
  match the bundled adapter contract; prereleases, older releases and new majors
10
17
  remain rejected. Starting with beta14, newer stable Claude 2.1 patches can
@@ -15,6 +22,11 @@ equivalence with a tested native release. Unknown older patches, prereleases
15
22
  and other major/minor families remain rejected. No credentials, conversations,
16
23
  model requests or JEV calls are used by these compatibility probes.
17
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
+
18
30
  The Codex terminal bridge permits the native session picker's separate read-only
19
31
  connection while keeping the primary conversation and monitor intact. A picker
20
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.13';
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',
@@ -974,6 +1047,7 @@ type ObservationRecord = {
974
1047
  | 'compact_entry'
975
1048
  | 'compact_exit'
976
1049
  | 'native_summary_proof'
1050
+ | 'native_fallback_proof'
977
1051
  | 'native_error';
978
1052
  readonly category:
979
1053
  | 'observed'
@@ -1790,6 +1864,37 @@ async function flushProofDiagnostic(
1790
1864
  ]);
1791
1865
  }
1792
1866
 
1867
+ async function flushNativeFallbackProofDiagnostic(
1868
+ api: HookApi,
1869
+ options: ObservationOptions,
1870
+ operationId: string,
1871
+ sessionId: string,
1872
+ instanceId: string,
1873
+ clientVersion: string,
1874
+ fields: Pick<
1875
+ ObservationRecord,
1876
+ | 'proof_expected_count'
1877
+ | 'proof_actual_count'
1878
+ | 'proof_match'
1879
+ | 'proof_match_offset'
1880
+ | 'proof_view_source'
1881
+ | 'proof_api_view_match'
1882
+ | 'proof_api_count'
1883
+ >,
1884
+ ): Promise<void> {
1885
+ if (!options.diagnosticsEnabled) return;
1886
+ await flushObservationDiagnostics(api, options, operationId, sessionId, instanceId, [
1887
+ observationRecord(
1888
+ operationId,
1889
+ 'native_fallback_proof',
1890
+ 'observed',
1891
+ clientVersion,
1892
+ undefined,
1893
+ fields,
1894
+ ),
1895
+ ]);
1896
+ }
1897
+
1793
1898
  async function flushNativeSummaryProofDiagnostic(
1794
1899
  api: HookApi,
1795
1900
  options: ObservationOptions,
@@ -3590,6 +3695,8 @@ type PendingFallbackProof = {
3590
3695
  readonly expected_hash?: string;
3591
3696
  readonly expected_count?: number;
3592
3697
  readonly expected_message_hashes?: readonly string[];
3698
+ readonly delivery_mode?: 'atomic_marker';
3699
+ readonly marker_digest?: string;
3593
3700
  readonly anchor_counts?: readonly number[];
3594
3701
  readonly anchor_hashes?: readonly string[];
3595
3702
  readonly expires_at: string;
@@ -4131,6 +4238,8 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
4131
4238
  'expected_hash',
4132
4239
  'expected_count',
4133
4240
  'expected_message_hashes',
4241
+ 'delivery_mode',
4242
+ 'marker_digest',
4134
4243
  'anchor_counts',
4135
4244
  'anchor_hashes',
4136
4245
  'expires_at',
@@ -4161,6 +4270,10 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
4161
4270
  value.expected_message_hashes.every(
4162
4271
  (hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{16}$/u.test(hash),
4163
4272
  ))) &&
4273
+ (value.delivery_mode === undefined || value.delivery_mode === 'atomic_marker') &&
4274
+ (value.marker_digest === undefined || /^[a-f0-9]{64}$/u.test(String(value.marker_digest))) &&
4275
+ ((value.delivery_mode === undefined && value.marker_digest === undefined) ||
4276
+ (value.delivery_mode === 'atomic_marker' && typeof value.marker_digest === 'string')) &&
4164
4277
  ((value.anchor_counts === undefined && value.anchor_hashes === undefined) ||
4165
4278
  (Array.isArray(value.anchor_counts) &&
4166
4279
  Array.isArray(value.anchor_hashes) &&
@@ -4803,6 +4916,26 @@ function getTelemetry(
4803
4916
  return state.current;
4804
4917
  }
4805
4918
 
4919
+ async function nativeFallbackMarker(
4920
+ operationId: string,
4921
+ sessionRef: string,
4922
+ baselineHash: string,
4923
+ ): Promise<{ readonly text: string; readonly digest: string }> {
4924
+ const digest = await sha256(
4925
+ `aibrain-native-fallback-v1|${operationId}|${sessionRef}|${baselineHash}`,
4926
+ );
4927
+ return { digest, text: `[AI Brain native fallback receipt v1:${digest}]` };
4928
+ }
4929
+
4930
+ function nativeFallbackDeliveryResult(nativeResult: unknown, marker: string): unknown {
4931
+ const messages = nativeResultMessages(nativeResult);
4932
+ if (messages === undefined || messages.length === 0) return nativeResult;
4933
+ return {
4934
+ ...(nativeResult as Record<string, unknown>),
4935
+ messages: [...messages, { role: 'user', text: marker, toolUses: [] }],
4936
+ };
4937
+ }
4938
+
4806
4939
  async function queueNativeFallbackProof(
4807
4940
  api: HookApi,
4808
4941
  runtime: ProofRuntimeState,
@@ -4813,7 +4946,7 @@ async function queueNativeFallbackProof(
4813
4946
  nativeResult: unknown,
4814
4947
  beforeVisibleMessages: readonly string[] | undefined,
4815
4948
  activeOffset: number | undefined,
4816
- ): Promise<void> {
4949
+ ): Promise<unknown> {
4817
4950
  const expectedMessages = nativeResultMessages(nativeResult);
4818
4951
  const expectedHash =
4819
4952
  expectedMessages === undefined
@@ -4831,6 +4964,10 @@ async function queueNativeFallbackProof(
4831
4964
  beforeVisibleMessages === undefined || expectedMessages === undefined
4832
4965
  ? undefined
4833
4966
  : await nativeProofAnchors(beforeVisibleMessages, expectedMessages.length, activeOffset);
4967
+ const marker =
4968
+ expectedMessages === undefined
4969
+ ? undefined
4970
+ : await nativeFallbackMarker(operationId, sessionRef, baselineHash);
4834
4971
  const proof: PendingFallbackProof = {
4835
4972
  schema_version: 1,
4836
4973
  kind: 'native_fallback',
@@ -4844,6 +4981,9 @@ async function queueNativeFallbackProof(
4844
4981
  ...(expectedMessageHashes === undefined
4845
4982
  ? {}
4846
4983
  : { expected_message_hashes: expectedMessageHashes }),
4984
+ ...(marker === undefined
4985
+ ? {}
4986
+ : { delivery_mode: 'atomic_marker' as const, marker_digest: marker.digest }),
4847
4987
  ...(anchors === undefined
4848
4988
  ? {}
4849
4989
  : { anchor_counts: anchors.counts, anchor_hashes: anchors.hashes }),
@@ -4854,6 +4994,9 @@ async function queueNativeFallbackProof(
4854
4994
  };
4855
4995
  runtime.pendingProofMemory = proof;
4856
4996
  runtime.pendingProofPersistenceFailed = !(await savePendingProof(api, proof));
4997
+ return runtime.pendingProofPersistenceFailed || marker === undefined
4998
+ ? nativeResult
4999
+ : nativeFallbackDeliveryResult(nativeResult, marker.text);
4857
5000
  }
4858
5001
 
4859
5002
  async function confirmPendingProof(
@@ -4937,38 +5080,83 @@ async function confirmPendingProof(
4937
5080
  proof_actual_count: messages.length,
4938
5081
  };
4939
5082
  if (proof.expected_hash !== undefined && expectedCount !== undefined) {
4940
- fallbackMatch = await nativeSummaryViewMatch(
4941
- messages,
4942
- expectedCount,
4943
- proof.expected_hash,
4944
- proof.expected_message_hashes,
4945
- proof.anchor_counts,
4946
- proof.anchor_hashes,
4947
- );
4948
- const fallbackOffset =
4949
- fallbackMatch === 'active_suffix'
4950
- ? await nativeSummaryMatchedOffset(
4951
- messages,
4952
- expectedCount,
4953
- proof.expected_hash,
4954
- proof.expected_message_hashes,
4955
- proof.anchor_counts,
4956
- proof.anchor_hashes,
4957
- )
4958
- : undefined;
4959
- fallbackProofFields = {
4960
- ...fallbackProofFields,
4961
- proof_match: fallbackMatch,
4962
- ...(fallbackMatch === 'exact' ? { proof_match_offset: 0 } : {}),
4963
- ...(fallbackOffset === undefined ? {} : { proof_match_offset: fallbackOffset }),
4964
- };
4965
- verified = fallbackMatch === 'exact' || fallbackMatch === 'active_suffix';
5083
+ if (proof.delivery_mode === 'atomic_marker' && proof.marker_digest !== undefined) {
5084
+ try {
5085
+ const apiView = await boundedApiMessages(api);
5086
+ const marker = await nativeFallbackMarker(
5087
+ proof.operation_id,
5088
+ sessionRef,
5089
+ proof.baseline_hash,
5090
+ );
5091
+ const markerValid = marker.digest === proof.marker_digest;
5092
+ const markerCount = exactTextCount(apiView, marker.text);
5093
+ fallbackProofFields = {
5094
+ ...fallbackProofFields,
5095
+ proof_view_source: 'api',
5096
+ proof_api_view_match: markerValid && markerCount === 1 ? 'exact' : 'mismatch',
5097
+ proof_api_count: apiView.length,
5098
+ };
5099
+ if (markerValid && markerCount === 1) {
5100
+ fallbackMatch = 'exact';
5101
+ fallbackProofFields = {
5102
+ ...fallbackProofFields,
5103
+ proof_match: 'exact',
5104
+ proof_match_offset: 0,
5105
+ };
5106
+ verified = true;
5107
+ }
5108
+ } catch {
5109
+ fallbackProofFields = {
5110
+ ...fallbackProofFields,
5111
+ proof_view_source: 'api',
5112
+ proof_api_view_match: 'unavailable',
5113
+ };
5114
+ }
5115
+ } else {
5116
+ // Compatibility with proofs written by beta15 and earlier. These
5117
+ // proofs predate the API-view fingerprint and retain the old strict
5118
+ // default-view check.
5119
+ fallbackMatch = await nativeSummaryViewMatch(
5120
+ messages,
5121
+ expectedCount,
5122
+ proof.expected_hash,
5123
+ proof.expected_message_hashes,
5124
+ proof.anchor_counts,
5125
+ proof.anchor_hashes,
5126
+ );
5127
+ const fallbackOffset =
5128
+ fallbackMatch === 'active_suffix'
5129
+ ? await nativeSummaryMatchedOffset(
5130
+ messages,
5131
+ expectedCount,
5132
+ proof.expected_hash,
5133
+ proof.expected_message_hashes,
5134
+ proof.anchor_counts,
5135
+ proof.anchor_hashes,
5136
+ )
5137
+ : undefined;
5138
+ fallbackProofFields = {
5139
+ ...fallbackProofFields,
5140
+ proof_view_source: 'default',
5141
+ proof_match: fallbackMatch,
5142
+ ...(fallbackMatch === 'exact' ? { proof_match_offset: 0 } : {}),
5143
+ ...(fallbackOffset === undefined ? {} : { proof_match_offset: fallbackOffset }),
5144
+ };
5145
+ verified = fallbackMatch === 'exact' || fallbackMatch === 'active_suffix';
5146
+ }
4966
5147
  }
4967
5148
  }
4968
5149
  if (runtime.pendingProofPersistenceFailed) verified = false;
4969
5150
  const telemetry = getTelemetry(api, runtime.brainServer, runtime.options, runtime.telemetryState);
4970
5151
  if (!verified) {
4971
- if (deferMismatch) return;
5152
+ // Only the atomic native fallback marker may wait for the host's active
5153
+ // API view. A prepared JEV capsule remains strict at every callback.
5154
+ if (
5155
+ deferMismatch &&
5156
+ proof.kind === 'native_fallback' &&
5157
+ proof.delivery_mode === 'atomic_marker'
5158
+ )
5159
+ return;
4972
5160
  runtime.pendingProofMemory = undefined;
4973
5161
  runtime.pendingProofPersistenceFailed = false;
4974
5162
  await clearPendingProof(api, sessionRef);
@@ -4993,6 +5181,16 @@ async function confirmPendingProof(
4993
5181
  runtime.pendingProofMemory = undefined;
4994
5182
  runtime.pendingProofPersistenceFailed = false;
4995
5183
  await clearPendingProof(api, sessionRef);
5184
+ if (proof.kind === 'native_fallback')
5185
+ await flushNativeFallbackProofDiagnostic(
5186
+ api,
5187
+ runtime.observation,
5188
+ proof.operation_id,
5189
+ sessionRef,
5190
+ runtime.observationInstanceId,
5191
+ nativeClientVersion(runtime.options),
5192
+ fallbackProofFields,
5193
+ );
4996
5194
  if (proof.kind === 'custom') {
4997
5195
  const dropped = proof.pairs.filter((pair) => pair.action === 'drop_pair').length;
4998
5196
  const reduced = proof.pairs.filter((pair) => pair.action === 'truncate_result').length;
@@ -5461,7 +5659,11 @@ async function runShadowPreview(
5461
5659
  );
5462
5660
  }
5463
5661
 
5464
- export const register: Register = (on, options) => {
5662
+ const registerWithClock = (
5663
+ on: Parameters<Register>[0],
5664
+ options: HookOptions,
5665
+ monotonicClock: MonotonicClock,
5666
+ ): void => {
5465
5667
  const enabled = options.enabled === true;
5466
5668
  const disclosed = options.disclosed === true;
5467
5669
  const preservationMode: PreservationMode =
@@ -5579,7 +5781,10 @@ export const register: Register = (on, options) => {
5579
5781
 
5580
5782
  on('prompt.submit', async (api, rawEvent, next) => {
5581
5783
  const event = rawEvent as unknown as PromptSubmitHookEvent;
5582
- await confirmPendingProof(api, proofRuntime, true);
5784
+ // The host has completed the prior native turn before accepting a new
5785
+ // prompt. This is the final proof opportunity for a fallback whose first
5786
+ // turn.complete callback observed the retired/default view.
5787
+ await confirmPendingProof(api, proofRuntime);
5583
5788
  if (
5584
5789
  proofRuntime.compactionStrategy === 'native_summary_notes' &&
5585
5790
  enabled &&
@@ -5654,7 +5859,10 @@ export const register: Register = (on, options) => {
5654
5859
  });
5655
5860
 
5656
5861
  on('turn.complete', async (api, event, next) => {
5657
- await confirmPendingProof(api, proofRuntime);
5862
+ // The native host can publish the result before its active API view catches
5863
+ // up. Keep only the atomic fallback marker pending through this lifecycle
5864
+ // edge; prompt.submit performs the strict confirmation on the next turn.
5865
+ await confirmPendingProof(api, proofRuntime, true);
5658
5866
  if (
5659
5867
  proofRuntime.compactionStrategy === 'native_summary_notes' &&
5660
5868
  enabled &&
@@ -5826,7 +6034,7 @@ export const register: Register = (on, options) => {
5826
6034
  nativeContinuationStarted = true;
5827
6035
  const nativeResult = await next(event);
5828
6036
  if (fallbackCapture !== undefined)
5829
- await queueNativeFallbackProof(
6037
+ return queueNativeFallbackProof(
5830
6038
  api,
5831
6039
  proofRuntime,
5832
6040
  fallbackCapture.operationId,
@@ -6044,7 +6252,7 @@ export const register: Register = (on, options) => {
6044
6252
  markDiagnostic('budget', 'provider_not_attempted', {
6045
6253
  item_count: built.pairs.filter((pair) => !pair.protectedPair).length,
6046
6254
  });
6047
- const budgetRead = await readContextBudget(api, brainServer);
6255
+ const budgetRead = await readContextBudget(api, brainServer, monotonicClock);
6048
6256
  if (!budgetRead.ok) {
6049
6257
  const admission = budgetRead.kind === 'admission_denied';
6050
6258
  const category: ObservationRecord['category'] = admission
@@ -6059,8 +6267,13 @@ export const register: Register = (on, options) => {
6059
6267
  const errorMetadata: Partial<ContextErrorMetadata> = isRecord(budgetRead.error)
6060
6268
  ? (budgetRead.error as ContextErrorMetadata)
6061
6269
  : {};
6270
+ const budgetErrorCode = admission
6271
+ ? 'shared_admission_exceeded'
6272
+ : budgetRead.kind === 'invalid_response'
6273
+ ? 'budget_response_invalid'
6274
+ : 'provider_unavailable';
6062
6275
  markDiagnostic('budget', category, {
6063
- compact_error_code: admission ? 'shared_admission_exceeded' : 'provider_unavailable',
6276
+ compact_error_code: budgetErrorCode,
6064
6277
  ...(typeof errorMetadata.admissionDetail === 'string'
6065
6278
  ? { admission_detail: errorMetadata.admissionDetail }
6066
6279
  : {}),
@@ -6075,7 +6288,7 @@ export const register: Register = (on, options) => {
6075
6288
  : {}),
6076
6289
  });
6077
6290
  markDiagnostic('fallback', category, {
6078
- compact_error_code: admission ? 'shared_admission_exceeded' : 'provider_unavailable',
6291
+ compact_error_code: budgetErrorCode,
6079
6292
  });
6080
6293
  logFallback(
6081
6294
  api,
@@ -6243,18 +6456,42 @@ export const register: Register = (on, options) => {
6243
6456
  projection: providerProjection,
6244
6457
  ...(policyVersion === 2 ? { schema_version: 2, options: wireOptions } : {}),
6245
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
+ }
6246
6481
  stage = 'mcp_call';
6247
6482
  markDiagnostic('provider_request', 'request_attempted', {
6248
6483
  item_count: providerPairs.filter((pair) => !pair.protectedPair).length,
6249
6484
  byte_count: byteLength(JSON.stringify(input)),
6250
6485
  });
6251
- await persistDiagnostics();
6252
6486
  if (diagnosticMode)
6253
6487
  api.ui.log(
6254
6488
  `AI Brain context diagnostic ${JSON.stringify({ hook: 'session.compact', phase: 'mcp_started' })}`,
6255
6489
  );
6256
6490
  const call = api.mcp.call(brainServer, 'brain_context_compact', input);
6257
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();
6258
6495
  let timedOut = false;
6259
6496
  const hostTimers = globalThis as unknown as {
6260
6497
  setTimeout?: (callback: () => void, delay: number) => unknown;
@@ -6681,3 +6918,13 @@ export const register: Register = (on, options) => {
6681
6918
  });
6682
6919
  });
6683
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(