@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 +13 -1
- package/dist/assets/claude/hooks/context-compaction.ts +294 -47
- package/dist/assets/claude/hooks/native-host-contract.test.ts +9 -0
- package/dist/assets/codex-context-compaction.mjs +780 -105
- package/dist/claude-observation.mjs +3 -1
- package/dist/cli.mjs +13 -2
- package/dist/codex-compatibility.mjs +106 -92
- package/dist/codex-diagnostics.mjs +4 -0
- package/dist/codex-protocol-fingerprint.json +10 -10
- package/dist/launch.mjs +3 -1
- package/dist/package.json +1 -1
- package/dist/platform.mjs +15 -2
- package/package.json +1 -1
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.
|
|
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.
|
|
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',
|
|
@@ -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<
|
|
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
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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(
|