@brunobracaioli/aibrain 0.3.0-beta.7 → 0.4.0-beta.2
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 +80 -15
- package/dist/assets/claude/.claude-plugin/plugin.json +1 -1
- package/dist/assets/claude/hooks/context-compaction.ts +1100 -77
- package/dist/assets/claude/hooks/native-host-contract-runtime.ts +18 -0
- package/dist/assets/claude/hooks/native-host-contract.test.ts +51 -0
- package/dist/assets/codex-context-compaction.mjs +2179 -194
- package/dist/assets/jev-paid-pilot-terminal.mjs +334 -35
- package/dist/claude-compatibility.mjs +506 -0
- package/dist/claude-observation.mjs +117 -3
- package/dist/cli.mjs +105 -28
- package/dist/codex-compatibility.mjs +481 -0
- package/dist/codex-diagnostics.mjs +39 -3
- package/dist/codex-protocol-fingerprint.json +131 -0
- package/dist/engineering-pack-public-key.json +10 -0
- package/dist/engineering-pack.mjs +722 -0
- package/dist/engineering.mjs +194 -0
- package/dist/launch.mjs +71 -14
- package/dist/mcp.mjs +8 -8
- package/dist/package.json +1 -1
- package/dist/platform.mjs +47 -3
- package/package.json +3 -2
|
@@ -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.
|
|
154
|
+
const CLAUDE_HOOK_VERSION = '0.4.0-beta.1';
|
|
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;
|
|
@@ -176,6 +177,7 @@ const NATIVE_SUMMARY_PENDING_KEY_PREFIX = 'context-compaction/native-summary-pen
|
|
|
176
177
|
const NATIVE_SUMMARY_PENDING_INDEX_KEY = 'context-compaction/native-summary-pending-index';
|
|
177
178
|
const MAX_NATIVE_SUMMARY_PENDING = 4;
|
|
178
179
|
const MAX_NATIVE_NOTE_BYTES = 96 * 1024 - 256;
|
|
180
|
+
const MAX_NATIVE_SUMMARY_MESSAGE_HASHES = 256;
|
|
179
181
|
const MAX_CHECKPOINTS = 4;
|
|
180
182
|
const MAX_CHECKPOINT_KEYS = 128;
|
|
181
183
|
const CHECKPOINT_KEY_PREFIX = 'context-compaction/checkpoint/';
|
|
@@ -215,6 +217,74 @@ function canonicalJson(value: unknown): string {
|
|
|
215
217
|
.join(',')}}`;
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
export type PlanBindingFailureReason =
|
|
221
|
+
| 'plan_session_mismatch'
|
|
222
|
+
| 'plan_revision_mismatch'
|
|
223
|
+
| 'plan_projection_mismatch'
|
|
224
|
+
| 'plan_options_mismatch'
|
|
225
|
+
| 'plan_expired'
|
|
226
|
+
| 'plan_parse_envelope_invalid'
|
|
227
|
+
| 'plan_parse_metadata_invalid'
|
|
228
|
+
| 'plan_parse_decisions_invalid'
|
|
229
|
+
| 'plan_parse_fitting_invalid'
|
|
230
|
+
| 'plan_parse_stats_invalid'
|
|
231
|
+
| 'plan_parse_response_too_large'
|
|
232
|
+
| 'plan_parse_failed'
|
|
233
|
+
| 'session_changed_during_planning'
|
|
234
|
+
| 'snapshot_changed_during_planning'
|
|
235
|
+
| 'historical_notes_changed_during_planning';
|
|
236
|
+
|
|
237
|
+
const PLAN_BINDING_FAILURE_REASONS: ReadonlySet<string> = new Set([
|
|
238
|
+
'plan_session_mismatch',
|
|
239
|
+
'plan_revision_mismatch',
|
|
240
|
+
'plan_projection_mismatch',
|
|
241
|
+
'plan_options_mismatch',
|
|
242
|
+
'plan_expired',
|
|
243
|
+
'plan_parse_envelope_invalid',
|
|
244
|
+
'plan_parse_metadata_invalid',
|
|
245
|
+
'plan_parse_decisions_invalid',
|
|
246
|
+
'plan_parse_fitting_invalid',
|
|
247
|
+
'plan_parse_stats_invalid',
|
|
248
|
+
'plan_parse_response_too_large',
|
|
249
|
+
'plan_parse_failed',
|
|
250
|
+
'session_changed_during_planning',
|
|
251
|
+
'snapshot_changed_during_planning',
|
|
252
|
+
'historical_notes_changed_during_planning',
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
function isPlanBindingFailureReason(value: unknown): value is PlanBindingFailureReason {
|
|
256
|
+
return typeof value === 'string' && PLAN_BINDING_FAILURE_REASONS.has(value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function planParseFailureReason(error: unknown): PlanBindingFailureReason {
|
|
260
|
+
const message = error instanceof Error ? error.message : '';
|
|
261
|
+
if (message === 'AI Brain response is too large') return 'plan_parse_response_too_large';
|
|
262
|
+
if (message.includes('decision') || message.includes('probability'))
|
|
263
|
+
return 'plan_parse_decisions_invalid';
|
|
264
|
+
if (message.includes('fitting')) return 'plan_parse_fitting_invalid';
|
|
265
|
+
if (message.includes('statistic') || message.includes('reduction'))
|
|
266
|
+
return 'plan_parse_stats_invalid';
|
|
267
|
+
if (message.includes('metadata')) return 'plan_parse_metadata_invalid';
|
|
268
|
+
if (
|
|
269
|
+
message.includes('structured plan') ||
|
|
270
|
+
message.includes('plan envelope') ||
|
|
271
|
+
message.includes('unknown fields')
|
|
272
|
+
)
|
|
273
|
+
return 'plan_parse_envelope_invalid';
|
|
274
|
+
return 'plan_parse_failed';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function planBindingFailure(
|
|
278
|
+
reason: PlanBindingFailureReason,
|
|
279
|
+
message: string,
|
|
280
|
+
): Error & { readonly planBindingFailureReason: PlanBindingFailureReason } {
|
|
281
|
+
const error = new Error(message) as Error & {
|
|
282
|
+
planBindingFailureReason: PlanBindingFailureReason;
|
|
283
|
+
};
|
|
284
|
+
error.planBindingFailureReason = reason;
|
|
285
|
+
return error;
|
|
286
|
+
}
|
|
287
|
+
|
|
218
288
|
async function sha256(value: string): Promise<string> {
|
|
219
289
|
if (globalThis.crypto?.subtle === undefined) throw new Error('host crypto is unavailable');
|
|
220
290
|
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
@@ -224,6 +294,58 @@ async function sha256(value: string): Promise<string> {
|
|
|
224
294
|
function byteLength(value: string): number {
|
|
225
295
|
return new TextEncoder().encode(value).byteLength;
|
|
226
296
|
}
|
|
297
|
+
|
|
298
|
+
function providerResponseMetadata(
|
|
299
|
+
response: McpResult,
|
|
300
|
+
): Pick<ObservationRecord, 'provider_response_format' | 'byte_count' | 'item_count'> {
|
|
301
|
+
const hasStructured = response.structuredContent !== undefined;
|
|
302
|
+
const textBlocks =
|
|
303
|
+
response.content?.filter(
|
|
304
|
+
(block): block is { readonly type: string; readonly text: string } =>
|
|
305
|
+
block.type === 'text' && typeof block.text === 'string',
|
|
306
|
+
) ?? [];
|
|
307
|
+
const format = hasStructured
|
|
308
|
+
? textBlocks.length > 0
|
|
309
|
+
? 'both'
|
|
310
|
+
: 'structured'
|
|
311
|
+
: textBlocks.length > 0
|
|
312
|
+
? 'text'
|
|
313
|
+
: 'missing';
|
|
314
|
+
let byteCount = 0;
|
|
315
|
+
if (hasStructured) {
|
|
316
|
+
const serialized = JSON.stringify(response.structuredContent);
|
|
317
|
+
if (serialized !== undefined) byteCount += byteLength(serialized);
|
|
318
|
+
}
|
|
319
|
+
for (const block of textBlocks) byteCount += byteLength(block.text);
|
|
320
|
+
return {
|
|
321
|
+
provider_response_format: format,
|
|
322
|
+
byte_count: Math.min(byteCount, MAX_DIAGNOSTIC_BYTE_COUNT),
|
|
323
|
+
item_count: Math.min(response.content?.length ?? 0, 10_000_000),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function planBindingFailureReason(
|
|
328
|
+
plan: Readonly<Record<string, unknown>>,
|
|
329
|
+
expected: Readonly<{
|
|
330
|
+
sessionRef: string;
|
|
331
|
+
revision: string;
|
|
332
|
+
projectionHash: string;
|
|
333
|
+
optionsHash?: string | null;
|
|
334
|
+
now?: number;
|
|
335
|
+
}>,
|
|
336
|
+
): PlanBindingFailureReason | undefined {
|
|
337
|
+
if (plan.sessionRef !== expected.sessionRef) return 'plan_session_mismatch';
|
|
338
|
+
if (plan.revision !== expected.revision) return 'plan_revision_mismatch';
|
|
339
|
+
if (plan.projectionHash !== expected.projectionHash) return 'plan_projection_mismatch';
|
|
340
|
+
if (expected.optionsHash === null) return 'plan_options_mismatch';
|
|
341
|
+
if (expected.optionsHash !== undefined && plan.optionsHash !== expected.optionsHash)
|
|
342
|
+
return 'plan_options_mismatch';
|
|
343
|
+
const expiresAt = Date.parse(String(plan.expiresAt));
|
|
344
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= (expected.now ?? Date.now()))
|
|
345
|
+
return 'plan_expired';
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
227
349
|
function unique(values: readonly string[]): string[] {
|
|
228
350
|
return [...new Set(values)];
|
|
229
351
|
}
|
|
@@ -565,6 +687,8 @@ type ContextBudgetSnapshot = {
|
|
|
565
687
|
readonly accountInFlight: boolean;
|
|
566
688
|
readonly retryAfterMs: number;
|
|
567
689
|
readonly expiresAt: string;
|
|
690
|
+
readonly requestStartedAtMonotonic: number;
|
|
691
|
+
readonly lifetimeMs: number;
|
|
568
692
|
readonly windowEnd: string;
|
|
569
693
|
readonly correlationId: string;
|
|
570
694
|
};
|
|
@@ -578,7 +702,25 @@ type ContextBudgetRead =
|
|
|
578
702
|
readonly error?: Error | undefined;
|
|
579
703
|
};
|
|
580
704
|
|
|
581
|
-
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 {
|
|
582
724
|
if (!isRecord(value)) return undefined;
|
|
583
725
|
const allowed = [
|
|
584
726
|
'schema_version',
|
|
@@ -606,9 +748,22 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
606
748
|
!Number.isFinite(Date.parse(value.window_end))
|
|
607
749
|
)
|
|
608
750
|
return undefined;
|
|
751
|
+
const serverTime = Date.parse(value.server_time);
|
|
609
752
|
const expiresAt = Date.parse(value.expires_at);
|
|
610
753
|
const windowEnd = Date.parse(value.window_end);
|
|
611
|
-
|
|
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
|
+
)
|
|
612
767
|
return undefined;
|
|
613
768
|
const nonnegative = (candidate: unknown): candidate is number =>
|
|
614
769
|
typeof candidate === 'number' && Number.isSafeInteger(candidate) && candidate >= 0;
|
|
@@ -621,10 +776,8 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
621
776
|
!nonnegative(value.retry_after_ms) ||
|
|
622
777
|
value.retry_after_ms > 3_600_000 ||
|
|
623
778
|
typeof value.account_in_flight !== 'boolean' ||
|
|
624
|
-
|
|
625
|
-
value.
|
|
626
|
-
(value.provider_model !== null &&
|
|
627
|
-
(typeof value.provider_model !== 'string' || value.provider_model.length > 128))
|
|
779
|
+
value.provider_batches !== 0 ||
|
|
780
|
+
value.provider_model !== null
|
|
628
781
|
)
|
|
629
782
|
return undefined;
|
|
630
783
|
const correlationId = boundedSafeIdentifier(value.correlation_id);
|
|
@@ -637,18 +790,20 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
637
790
|
accountInFlight: value.account_in_flight,
|
|
638
791
|
retryAfterMs: value.retry_after_ms,
|
|
639
792
|
expiresAt: value.expires_at,
|
|
793
|
+
requestStartedAtMonotonic: requestStartedAt,
|
|
794
|
+
lifetimeMs: snapshotLifetimeMs,
|
|
640
795
|
windowEnd: value.window_end,
|
|
641
796
|
correlationId,
|
|
642
797
|
};
|
|
643
798
|
}
|
|
644
799
|
|
|
645
|
-
function mcpStructuredValue(response: McpResult): unknown {
|
|
800
|
+
function mcpStructuredValue(response: McpResult, maxTextBytes = 8 * 1024): unknown {
|
|
646
801
|
if (response.structuredContent !== undefined) return response.structuredContent;
|
|
647
802
|
const text = response.content?.find(
|
|
648
803
|
(block): block is { readonly type: string; readonly text: string } =>
|
|
649
804
|
block.type === 'text' && typeof block.text === 'string',
|
|
650
805
|
)?.text;
|
|
651
|
-
if (text === undefined || byteLength(text) >
|
|
806
|
+
if (text === undefined || byteLength(text) > maxTextBytes) return undefined;
|
|
652
807
|
try {
|
|
653
808
|
return JSON.parse(text);
|
|
654
809
|
} catch {
|
|
@@ -656,8 +811,14 @@ function mcpStructuredValue(response: McpResult): unknown {
|
|
|
656
811
|
}
|
|
657
812
|
}
|
|
658
813
|
|
|
659
|
-
async function readContextBudget(
|
|
814
|
+
export async function readContextBudget(
|
|
815
|
+
api: HookApi,
|
|
816
|
+
server: string,
|
|
817
|
+
now: MonotonicClock = monotonicNow,
|
|
818
|
+
): Promise<ContextBudgetRead> {
|
|
660
819
|
try {
|
|
820
|
+
const requestStartedAt = now();
|
|
821
|
+
if (requestStartedAt === undefined) return { ok: false, kind: 'provider_unavailable' };
|
|
661
822
|
const call = api.mcp.call(server, 'brain_context_budget', { schema_version: 1 });
|
|
662
823
|
const hostTimers = globalThis as unknown as {
|
|
663
824
|
setTimeout?: (callback: () => void, delay: number) => unknown;
|
|
@@ -671,8 +832,12 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
671
832
|
resolve(undefined);
|
|
672
833
|
}, BUDGET_PREFLIGHT_TIMEOUT_MS);
|
|
673
834
|
});
|
|
674
|
-
|
|
675
|
-
|
|
835
|
+
let response: McpResult | undefined;
|
|
836
|
+
try {
|
|
837
|
+
response = await Promise.race([call, timeout]);
|
|
838
|
+
} finally {
|
|
839
|
+
if (timer !== undefined) hostTimers.clearTimeout?.(timer);
|
|
840
|
+
}
|
|
676
841
|
if (response === undefined)
|
|
677
842
|
return { ok: false, kind: timedOut ? 'timeout' : 'invalid_response' };
|
|
678
843
|
if (response.isError === true) {
|
|
@@ -683,7 +848,15 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
683
848
|
error,
|
|
684
849
|
};
|
|
685
850
|
}
|
|
686
|
-
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
|
+
);
|
|
687
860
|
return snapshot === undefined
|
|
688
861
|
? { ok: false, kind: 'invalid_response' }
|
|
689
862
|
: { ok: true, snapshot };
|
|
@@ -702,6 +875,25 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
702
875
|
}
|
|
703
876
|
}
|
|
704
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
|
+
|
|
705
897
|
function safeRpcStatus(error: unknown): number | undefined {
|
|
706
898
|
const record = isRecord(error) ? error : {};
|
|
707
899
|
const candidate = record.rpc_status ?? record.status ?? record.code;
|
|
@@ -809,6 +1001,7 @@ const CHECKPOINT_ERRORS: readonly CheckpointError[] = [
|
|
|
809
1001
|
type CompactErrorCode =
|
|
810
1002
|
| 'state_budget_exceeded'
|
|
811
1003
|
| 'shared_admission_exceeded'
|
|
1004
|
+
| 'budget_response_invalid'
|
|
812
1005
|
| 'provider_budget_exceeded'
|
|
813
1006
|
| 'provider_unavailable'
|
|
814
1007
|
| 'provider_auth_denied'
|
|
@@ -823,6 +1016,7 @@ type CompactErrorCode =
|
|
|
823
1016
|
const COMPACT_ERROR_CODES: readonly CompactErrorCode[] = [
|
|
824
1017
|
'state_budget_exceeded',
|
|
825
1018
|
'shared_admission_exceeded',
|
|
1019
|
+
'budget_response_invalid',
|
|
826
1020
|
'provider_budget_exceeded',
|
|
827
1021
|
'provider_unavailable',
|
|
828
1022
|
'provider_auth_denied',
|
|
@@ -852,6 +1046,8 @@ type ObservationRecord = {
|
|
|
852
1046
|
| 'fallback'
|
|
853
1047
|
| 'compact_entry'
|
|
854
1048
|
| 'compact_exit'
|
|
1049
|
+
| 'native_summary_proof'
|
|
1050
|
+
| 'native_fallback_proof'
|
|
855
1051
|
| 'native_error';
|
|
856
1052
|
readonly category:
|
|
857
1053
|
| 'observed'
|
|
@@ -886,11 +1082,21 @@ type ObservationRecord = {
|
|
|
886
1082
|
readonly selected_count?: number;
|
|
887
1083
|
readonly deferred_count?: number;
|
|
888
1084
|
readonly rpc_status?: number;
|
|
1085
|
+
readonly provider_response_format?: 'structured' | 'text' | 'both' | 'missing';
|
|
889
1086
|
readonly admission_detail?: string;
|
|
890
1087
|
readonly admission_reason?: string;
|
|
891
1088
|
readonly request_id?: string;
|
|
892
1089
|
readonly correlation_id?: string;
|
|
893
1090
|
readonly retry_after_ms?: number;
|
|
1091
|
+
readonly proof_expected_count?: number;
|
|
1092
|
+
readonly proof_actual_count?: number;
|
|
1093
|
+
readonly proof_match?: 'exact' | 'active_suffix' | 'mismatch' | 'ambiguous';
|
|
1094
|
+
readonly proof_match_offset?: number;
|
|
1095
|
+
readonly proof_view_source?: 'default' | 'api';
|
|
1096
|
+
readonly proof_api_view_match?: 'exact' | 'mismatch' | 'unavailable' | 'not_checked';
|
|
1097
|
+
readonly proof_api_count?: number;
|
|
1098
|
+
readonly proof_delivery_source?: 'atomic_result' | 'legacy_append';
|
|
1099
|
+
readonly proof_delivery_match_count?: number;
|
|
894
1100
|
readonly reason?:
|
|
895
1101
|
| 'all_recent'
|
|
896
1102
|
| 'goal_missing'
|
|
@@ -905,7 +1111,14 @@ type ObservationRecord = {
|
|
|
905
1111
|
| 'provider_not_attempted'
|
|
906
1112
|
| 'proof_expired'
|
|
907
1113
|
| 'proof_mismatch'
|
|
908
|
-
| 'native_fallback_unconfirmed'
|
|
1114
|
+
| 'native_fallback_unconfirmed'
|
|
1115
|
+
| 'native_summary_proof_expired'
|
|
1116
|
+
| 'native_summary_host_view_mismatch'
|
|
1117
|
+
| 'native_summary_host_view_ambiguous'
|
|
1118
|
+
| 'native_summary_host_view_unavailable'
|
|
1119
|
+
| 'native_summary_note_read_failed'
|
|
1120
|
+
| 'native_summary_attachment_uncertain'
|
|
1121
|
+
| PlanBindingFailureReason;
|
|
909
1122
|
};
|
|
910
1123
|
|
|
911
1124
|
function observationConfiguration(options: HookOptions): ObservationOptions {
|
|
@@ -1507,6 +1720,17 @@ function observationRecord(
|
|
|
1507
1720
|
| 'request_id'
|
|
1508
1721
|
| 'correlation_id'
|
|
1509
1722
|
| 'retry_after_ms'
|
|
1723
|
+
| 'provider_response_format'
|
|
1724
|
+
| 'proof_expected_count'
|
|
1725
|
+
| 'proof_actual_count'
|
|
1726
|
+
| 'proof_match'
|
|
1727
|
+
| 'proof_match_offset'
|
|
1728
|
+
| 'proof_view_source'
|
|
1729
|
+
| 'proof_api_view_match'
|
|
1730
|
+
| 'proof_api_count'
|
|
1731
|
+
| 'proof_delivery_source'
|
|
1732
|
+
| 'proof_delivery_match_count'
|
|
1733
|
+
| 'reason'
|
|
1510
1734
|
> = {},
|
|
1511
1735
|
reason?: ObservationRecord['reason'],
|
|
1512
1736
|
mode: ObservationRecord['mode'] = 'compact',
|
|
@@ -1550,6 +1774,33 @@ function observationRecord(
|
|
|
1550
1774
|
...(fields.request_id === undefined ? {} : { request_id: fields.request_id }),
|
|
1551
1775
|
...(fields.correlation_id === undefined ? {} : { correlation_id: fields.correlation_id }),
|
|
1552
1776
|
...(fields.retry_after_ms === undefined ? {} : { retry_after_ms: fields.retry_after_ms }),
|
|
1777
|
+
...(fields.provider_response_format === undefined
|
|
1778
|
+
? {}
|
|
1779
|
+
: { provider_response_format: fields.provider_response_format }),
|
|
1780
|
+
...(fields.proof_expected_count === undefined
|
|
1781
|
+
? {}
|
|
1782
|
+
: { proof_expected_count: fields.proof_expected_count }),
|
|
1783
|
+
...(fields.proof_actual_count === undefined
|
|
1784
|
+
? {}
|
|
1785
|
+
: { proof_actual_count: fields.proof_actual_count }),
|
|
1786
|
+
...(fields.proof_match === undefined ? {} : { proof_match: fields.proof_match }),
|
|
1787
|
+
...(fields.proof_match_offset === undefined
|
|
1788
|
+
? {}
|
|
1789
|
+
: { proof_match_offset: fields.proof_match_offset }),
|
|
1790
|
+
...(fields.proof_view_source === undefined
|
|
1791
|
+
? {}
|
|
1792
|
+
: { proof_view_source: fields.proof_view_source }),
|
|
1793
|
+
...(fields.proof_api_view_match === undefined
|
|
1794
|
+
? {}
|
|
1795
|
+
: { proof_api_view_match: fields.proof_api_view_match }),
|
|
1796
|
+
...(fields.proof_api_count === undefined ? {} : { proof_api_count: fields.proof_api_count }),
|
|
1797
|
+
...(fields.proof_delivery_source === undefined
|
|
1798
|
+
? {}
|
|
1799
|
+
: { proof_delivery_source: fields.proof_delivery_source }),
|
|
1800
|
+
...(fields.proof_delivery_match_count === undefined
|
|
1801
|
+
? {}
|
|
1802
|
+
: { proof_delivery_match_count: fields.proof_delivery_match_count }),
|
|
1803
|
+
...(fields.reason === undefined ? {} : { reason: fields.reason }),
|
|
1553
1804
|
...(reason === undefined ? {} : { reason }),
|
|
1554
1805
|
};
|
|
1555
1806
|
}
|
|
@@ -1585,7 +1836,19 @@ async function flushProofDiagnostic(
|
|
|
1585
1836
|
sessionId: string,
|
|
1586
1837
|
instanceId: string,
|
|
1587
1838
|
clientVersion: string,
|
|
1588
|
-
reason: '
|
|
1839
|
+
reason: NonNullable<ObservationRecord['reason']>,
|
|
1840
|
+
fields: Pick<
|
|
1841
|
+
ObservationRecord,
|
|
1842
|
+
| 'proof_expected_count'
|
|
1843
|
+
| 'proof_actual_count'
|
|
1844
|
+
| 'proof_match'
|
|
1845
|
+
| 'proof_match_offset'
|
|
1846
|
+
| 'proof_view_source'
|
|
1847
|
+
| 'proof_api_view_match'
|
|
1848
|
+
| 'proof_api_count'
|
|
1849
|
+
| 'proof_delivery_source'
|
|
1850
|
+
| 'proof_delivery_match_count'
|
|
1851
|
+
> = {},
|
|
1589
1852
|
): Promise<void> {
|
|
1590
1853
|
if (!options.diagnosticsEnabled) return;
|
|
1591
1854
|
await flushObservationDiagnostics(api, options, operationId, sessionId, instanceId, [
|
|
@@ -1595,12 +1858,76 @@ async function flushProofDiagnostic(
|
|
|
1595
1858
|
'native_error',
|
|
1596
1859
|
clientVersion,
|
|
1597
1860
|
undefined,
|
|
1598
|
-
|
|
1861
|
+
fields,
|
|
1599
1862
|
reason,
|
|
1600
1863
|
),
|
|
1601
1864
|
]);
|
|
1602
1865
|
}
|
|
1603
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
|
+
|
|
1898
|
+
async function flushNativeSummaryProofDiagnostic(
|
|
1899
|
+
api: HookApi,
|
|
1900
|
+
options: ObservationOptions,
|
|
1901
|
+
operationId: string,
|
|
1902
|
+
sessionId: string,
|
|
1903
|
+
instanceId: string,
|
|
1904
|
+
clientVersion: string,
|
|
1905
|
+
fields: Pick<
|
|
1906
|
+
ObservationRecord,
|
|
1907
|
+
| 'proof_expected_count'
|
|
1908
|
+
| 'proof_actual_count'
|
|
1909
|
+
| 'proof_match'
|
|
1910
|
+
| 'proof_match_offset'
|
|
1911
|
+
| 'proof_view_source'
|
|
1912
|
+
| 'proof_api_view_match'
|
|
1913
|
+
| 'proof_api_count'
|
|
1914
|
+
| 'proof_delivery_source'
|
|
1915
|
+
| 'proof_delivery_match_count'
|
|
1916
|
+
>,
|
|
1917
|
+
): Promise<void> {
|
|
1918
|
+
if (!options.diagnosticsEnabled) return;
|
|
1919
|
+
await flushObservationDiagnostics(api, options, operationId, sessionId, instanceId, [
|
|
1920
|
+
observationRecord(
|
|
1921
|
+
operationId,
|
|
1922
|
+
'native_summary_proof',
|
|
1923
|
+
'observed',
|
|
1924
|
+
clientVersion,
|
|
1925
|
+
undefined,
|
|
1926
|
+
fields,
|
|
1927
|
+
),
|
|
1928
|
+
]);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1604
1931
|
function logDiagnostic(
|
|
1605
1932
|
api: HookApi,
|
|
1606
1933
|
hook: 'turn.complete' | 'session.compact',
|
|
@@ -1681,11 +2008,11 @@ function visibleSnapshotsMatch(
|
|
|
1681
2008
|
rereadMessages: readonly ClaudeMessage[],
|
|
1682
2009
|
trigger: HookEvent['trigger'],
|
|
1683
2010
|
instructions: string | undefined,
|
|
1684
|
-
):
|
|
1685
|
-
if (eventMessages.length === 0) return rereadMessages.length === 0;
|
|
2011
|
+
): number | undefined {
|
|
2012
|
+
if (eventMessages.length === 0) return rereadMessages.length === 0 ? 0 : undefined;
|
|
1686
2013
|
const eventSignatures = visibleSignatures(eventMessages);
|
|
1687
2014
|
const rereadSignatures = visibleSignatures(rereadMessages);
|
|
1688
|
-
if (sameSequence(eventSignatures, rereadSignatures)) return
|
|
2015
|
+
if (sameSequence(eventSignatures, rereadSignatures)) return 0;
|
|
1689
2016
|
if (
|
|
1690
2017
|
trigger === 'manual' &&
|
|
1691
2018
|
instructions === undefined &&
|
|
@@ -1717,10 +2044,10 @@ function visibleSnapshotsMatch(
|
|
|
1717
2044
|
matches.length === 1 &&
|
|
1718
2045
|
matches[0] === precedingSignatures.length - eventSignatures.length
|
|
1719
2046
|
)
|
|
1720
|
-
return
|
|
2047
|
+
return matches[0];
|
|
1721
2048
|
}
|
|
1722
2049
|
}
|
|
1723
|
-
if (rereadMessages.length < eventMessages.length) return
|
|
2050
|
+
if (rereadMessages.length < eventMessages.length) return undefined;
|
|
1724
2051
|
const eventShape = eventSignatures;
|
|
1725
2052
|
const matches: number[] = [];
|
|
1726
2053
|
for (let index = 0; index <= rereadSignatures.length - eventSignatures.length; index++) {
|
|
@@ -1728,7 +2055,9 @@ function visibleSnapshotsMatch(
|
|
|
1728
2055
|
matches.push(index);
|
|
1729
2056
|
if (matches.length > 1) break;
|
|
1730
2057
|
}
|
|
1731
|
-
return matches.length === 1 && matches[0] === rereadMessages.length - eventMessages.length
|
|
2058
|
+
return matches.length === 1 && matches[0] === rereadMessages.length - eventMessages.length
|
|
2059
|
+
? matches[0]
|
|
2060
|
+
: undefined;
|
|
1732
2061
|
}
|
|
1733
2062
|
|
|
1734
2063
|
function diagnosticShape(messages: readonly ClaudeMessage[]): Record<string, unknown> {
|
|
@@ -2330,6 +2659,10 @@ type NativeSummaryPending = {
|
|
|
2330
2659
|
readonly noteSha256: string;
|
|
2331
2660
|
readonly expectedPrefixHash: string;
|
|
2332
2661
|
readonly expectedCount: number;
|
|
2662
|
+
readonly expectedMessageHashes?: readonly string[];
|
|
2663
|
+
readonly deliveryMode?: 'atomic_result' | 'legacy_append';
|
|
2664
|
+
readonly anchorCounts?: readonly number[];
|
|
2665
|
+
readonly anchorHashes?: readonly string[];
|
|
2333
2666
|
readonly expiresAt: string;
|
|
2334
2667
|
readonly appendAttempted: boolean;
|
|
2335
2668
|
readonly nativeCompletedReported: boolean;
|
|
@@ -2577,6 +2910,10 @@ function structurallyValidNativeSummaryPending(
|
|
|
2577
2910
|
'noteSha256',
|
|
2578
2911
|
'expectedPrefixHash',
|
|
2579
2912
|
'expectedCount',
|
|
2913
|
+
'expectedMessageHashes',
|
|
2914
|
+
'deliveryMode',
|
|
2915
|
+
'anchorCounts',
|
|
2916
|
+
'anchorHashes',
|
|
2580
2917
|
'expiresAt',
|
|
2581
2918
|
'appendAttempted',
|
|
2582
2919
|
'nativeCompletedReported',
|
|
@@ -2603,6 +2940,28 @@ function structurallyValidNativeSummaryPending(
|
|
|
2603
2940
|
Number.isSafeInteger(value.expectedCount) &&
|
|
2604
2941
|
(value.expectedCount as number) > 0 &&
|
|
2605
2942
|
(value.expectedCount as number) <= 10_000 &&
|
|
2943
|
+
(value.expectedMessageHashes === undefined ||
|
|
2944
|
+
(Array.isArray(value.expectedMessageHashes) &&
|
|
2945
|
+
value.expectedMessageHashes.length === value.expectedCount &&
|
|
2946
|
+
value.expectedMessageHashes.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES &&
|
|
2947
|
+
value.expectedMessageHashes.every(
|
|
2948
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{16}$/u.test(hash),
|
|
2949
|
+
))) &&
|
|
2950
|
+
(value.deliveryMode === undefined ||
|
|
2951
|
+
value.deliveryMode === 'atomic_result' ||
|
|
2952
|
+
value.deliveryMode === 'legacy_append') &&
|
|
2953
|
+
((value.anchorCounts === undefined && value.anchorHashes === undefined) ||
|
|
2954
|
+
(Array.isArray(value.anchorCounts) &&
|
|
2955
|
+
Array.isArray(value.anchorHashes) &&
|
|
2956
|
+
value.anchorCounts.length === value.anchorHashes.length &&
|
|
2957
|
+
value.anchorCounts.length >= 1 &&
|
|
2958
|
+
value.anchorCounts.length <= 3 &&
|
|
2959
|
+
value.anchorCounts.every(
|
|
2960
|
+
(count): count is number => Number.isSafeInteger(count) && count >= 0 && count <= 10_000,
|
|
2961
|
+
) &&
|
|
2962
|
+
value.anchorHashes.every(
|
|
2963
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{64}$/u.test(hash),
|
|
2964
|
+
))) &&
|
|
2606
2965
|
typeof value.expiresAt === 'string' &&
|
|
2607
2966
|
Number.isFinite(Date.parse(value.expiresAt)) &&
|
|
2608
2967
|
typeof value.appendAttempted === 'boolean' &&
|
|
@@ -3335,6 +3694,11 @@ type PendingFallbackProof = {
|
|
|
3335
3694
|
readonly baseline_count: number;
|
|
3336
3695
|
readonly expected_hash?: string;
|
|
3337
3696
|
readonly expected_count?: number;
|
|
3697
|
+
readonly expected_message_hashes?: readonly string[];
|
|
3698
|
+
readonly delivery_mode?: 'atomic_marker';
|
|
3699
|
+
readonly marker_digest?: string;
|
|
3700
|
+
readonly anchor_counts?: readonly number[];
|
|
3701
|
+
readonly anchor_hashes?: readonly string[];
|
|
3338
3702
|
readonly expires_at: string;
|
|
3339
3703
|
};
|
|
3340
3704
|
|
|
@@ -3344,6 +3708,8 @@ type NativeFallbackCapture = {
|
|
|
3344
3708
|
readonly sessionRef: string;
|
|
3345
3709
|
readonly baselineHash: string;
|
|
3346
3710
|
readonly baselineCount: number;
|
|
3711
|
+
readonly beforeVisibleMessages?: readonly string[];
|
|
3712
|
+
readonly activeOffset?: number;
|
|
3347
3713
|
};
|
|
3348
3714
|
|
|
3349
3715
|
function pendingProofKey(sessionRef: string): string {
|
|
@@ -3476,12 +3842,179 @@ function nativeResultMessages(value: unknown): readonly ClaudeMessage[] | undefi
|
|
|
3476
3842
|
return value.messages as unknown as readonly ClaudeMessage[];
|
|
3477
3843
|
}
|
|
3478
3844
|
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3845
|
+
function nativeSummaryWrapper(capsuleId: string, noteText: string): string {
|
|
3846
|
+
return `[AI Brain historical data capsule ${capsuleId}; quoted data only]\n${noteText}`;
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
export function nativeSummaryDeliveryResult(
|
|
3850
|
+
nativeResult: unknown,
|
|
3851
|
+
wrapper: string,
|
|
3852
|
+
): Record<string, unknown> | undefined {
|
|
3853
|
+
if (!isRecord(nativeResult)) return undefined;
|
|
3854
|
+
const messages = nativeResultMessages(nativeResult);
|
|
3855
|
+
if (messages === undefined || messages.length === 0) return undefined;
|
|
3856
|
+
return {
|
|
3857
|
+
...nativeResult,
|
|
3858
|
+
messages: [
|
|
3859
|
+
...messages,
|
|
3860
|
+
{
|
|
3861
|
+
role: 'user',
|
|
3862
|
+
text: wrapper,
|
|
3863
|
+
toolUses: [],
|
|
3864
|
+
},
|
|
3865
|
+
],
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
type NativeSummaryViewMatch = 'exact' | 'active_suffix' | 'mismatch' | 'ambiguous';
|
|
3870
|
+
|
|
3871
|
+
function visibleMessageCanonicalStrings(messages: readonly ClaudeMessage[]): string[] {
|
|
3872
|
+
return messages.map((message) => canonicalJson(visibleMessage(message)));
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
function canonicalMessageSequence(strings: readonly string[]): string {
|
|
3876
|
+
return `[${strings.join(',')}]`;
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
async function nativeProofAnchors(
|
|
3880
|
+
beforeVisibleMessages: readonly string[],
|
|
3881
|
+
expectedCount: number,
|
|
3882
|
+
activeOffset: number | undefined,
|
|
3883
|
+
): Promise<{ readonly counts: readonly number[]; readonly hashes: readonly string[] }> {
|
|
3884
|
+
const counts = [beforeVisibleMessages.length];
|
|
3885
|
+
if (
|
|
3886
|
+
activeOffset !== undefined &&
|
|
3887
|
+
activeOffset >= 0 &&
|
|
3888
|
+
activeOffset < beforeVisibleMessages.length
|
|
3889
|
+
)
|
|
3890
|
+
counts.push(activeOffset);
|
|
3891
|
+
const replaceTailCount = beforeVisibleMessages.length - expectedCount;
|
|
3892
|
+
if (replaceTailCount > 0 && replaceTailCount < beforeVisibleMessages.length)
|
|
3893
|
+
counts.push(replaceTailCount);
|
|
3894
|
+
const uniqueCounts = [...new Set(counts)].filter((count) => count <= 10_000);
|
|
3895
|
+
const hashes = await Promise.all(
|
|
3896
|
+
uniqueCounts.map((count) =>
|
|
3897
|
+
sha256(canonicalMessageSequence(beforeVisibleMessages.slice(0, count))),
|
|
3898
|
+
),
|
|
3899
|
+
);
|
|
3900
|
+
return { counts: uniqueCounts, hashes };
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
/**
|
|
3904
|
+
* Claude keeps the retired transcript prefix in session.messages() after a
|
|
3905
|
+
* native compaction, while the hook result describes the active summary
|
|
3906
|
+
* segment. Accept only the exact prefix or one uniquely matching active
|
|
3907
|
+
* suffix. A repeated match remains ambiguous and fails closed.
|
|
3908
|
+
*/
|
|
3909
|
+
export async function nativeSummaryViewMatch(
|
|
3910
|
+
actualMessages: readonly ClaudeMessage[],
|
|
3911
|
+
expectedCount: number,
|
|
3912
|
+
expectedHash: string,
|
|
3913
|
+
expectedMessageHashes: readonly string[] | undefined,
|
|
3914
|
+
anchorCounts: readonly number[] | undefined,
|
|
3915
|
+
anchorHashes: readonly string[] | undefined,
|
|
3916
|
+
): Promise<NativeSummaryViewMatch> {
|
|
3917
|
+
if (expectedCount === 0 || actualMessages.length < expectedCount) return 'mismatch';
|
|
3918
|
+
const matchingStarts: number[] = [];
|
|
3919
|
+
if (expectedMessageHashes === undefined) {
|
|
3920
|
+
const prefixHash = await sha256(
|
|
3921
|
+
canonicalJson(visibleMessages(actualMessages.slice(0, expectedCount))),
|
|
3922
|
+
);
|
|
3923
|
+
return prefixHash === expectedHash ? 'exact' : 'mismatch';
|
|
3924
|
+
}
|
|
3925
|
+
if (expectedMessageHashes.length !== expectedCount) return 'mismatch';
|
|
3926
|
+
const actualMessageHashes: string[] = [];
|
|
3927
|
+
for (const message of actualMessages)
|
|
3928
|
+
actualMessageHashes.push((await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16));
|
|
3929
|
+
for (let index = 0; index <= actualMessages.length - expectedCount; index += 1) {
|
|
3930
|
+
if (
|
|
3931
|
+
expectedMessageHashes.every((hash, offset) => hash === actualMessageHashes[index + offset])
|
|
3932
|
+
) {
|
|
3933
|
+
const candidateHash = await sha256(
|
|
3934
|
+
canonicalJson(visibleMessages(actualMessages.slice(index, index + expectedCount))),
|
|
3935
|
+
);
|
|
3936
|
+
if (candidateHash === expectedHash) matchingStarts.push(index);
|
|
3937
|
+
}
|
|
3938
|
+
if (matchingStarts.length > 1) return 'ambiguous';
|
|
3939
|
+
}
|
|
3940
|
+
if (matchingStarts.length !== 1) return 'mismatch';
|
|
3941
|
+
const start = matchingStarts[0] ?? -1;
|
|
3942
|
+
if (start === 0 && actualMessages.length === expectedCount) return 'exact';
|
|
3943
|
+
if (start === 0) return 'exact';
|
|
3944
|
+
if (await pendingAnchorMatches(actualMessages, start, anchorCounts, anchorHashes))
|
|
3945
|
+
return 'active_suffix';
|
|
3946
|
+
return 'mismatch';
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
async function pendingAnchorMatches(
|
|
3950
|
+
actualMessages: readonly ClaudeMessage[],
|
|
3951
|
+
actualOffset: number,
|
|
3952
|
+
anchorCounts: readonly number[] | undefined,
|
|
3953
|
+
anchorHashes: readonly string[] | undefined,
|
|
3954
|
+
): Promise<boolean> {
|
|
3955
|
+
if (
|
|
3956
|
+
anchorCounts === undefined ||
|
|
3957
|
+
anchorHashes === undefined ||
|
|
3958
|
+
anchorCounts.length !== anchorHashes.length
|
|
3959
|
+
)
|
|
3960
|
+
return false;
|
|
3961
|
+
const anchorIndex = anchorCounts.indexOf(actualOffset);
|
|
3962
|
+
if (anchorIndex < 0) return false;
|
|
3963
|
+
return (
|
|
3964
|
+
(await sha256(canonicalJson(visibleMessages(actualMessages.slice(0, actualOffset))))) ===
|
|
3965
|
+
anchorHashes[anchorIndex]
|
|
3966
|
+
);
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
async function nativeSummaryMatchedOffset(
|
|
3970
|
+
actualMessages: readonly ClaudeMessage[],
|
|
3971
|
+
expectedCount: number,
|
|
3972
|
+
expectedHash: string,
|
|
3973
|
+
expectedMessageHashes: readonly string[] | undefined,
|
|
3974
|
+
anchorCounts: readonly number[] | undefined,
|
|
3975
|
+
anchorHashes: readonly string[] | undefined,
|
|
3976
|
+
): Promise<number | undefined> {
|
|
3977
|
+
if (
|
|
3978
|
+
expectedMessageHashes === undefined ||
|
|
3979
|
+
anchorCounts === undefined ||
|
|
3980
|
+
anchorHashes === undefined
|
|
3981
|
+
)
|
|
3982
|
+
return undefined;
|
|
3983
|
+
const actualMessageHashes = await Promise.all(
|
|
3984
|
+
actualMessages.map(async (message) =>
|
|
3985
|
+
(await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16),
|
|
3986
|
+
),
|
|
3987
|
+
);
|
|
3988
|
+
const matches: number[] = [];
|
|
3989
|
+
for (const offset of anchorCounts) {
|
|
3990
|
+
if (offset + expectedCount > actualMessages.length) continue;
|
|
3991
|
+
if (!expectedMessageHashes.every((hash, index) => hash === actualMessageHashes[offset + index]))
|
|
3992
|
+
continue;
|
|
3993
|
+
if (
|
|
3994
|
+
(await sha256(
|
|
3995
|
+
canonicalJson(visibleMessages(actualMessages.slice(offset, offset + expectedCount))),
|
|
3996
|
+
)) !== expectedHash
|
|
3997
|
+
)
|
|
3998
|
+
continue;
|
|
3999
|
+
if (!(await pendingAnchorMatches(actualMessages, offset, anchorCounts, anchorHashes))) continue;
|
|
4000
|
+
matches.push(offset);
|
|
4001
|
+
}
|
|
4002
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
function exactTextCount(
|
|
4006
|
+
messages: readonly { readonly role: 'user' | 'assistant'; readonly text: string }[],
|
|
4007
|
+
wrapper: string,
|
|
4008
|
+
): number {
|
|
4009
|
+
return messages.filter(
|
|
3482
4010
|
(message) =>
|
|
3483
4011
|
message.role === 'user' && (message.text === wrapper || message.text === `${wrapper}\n`),
|
|
3484
|
-
);
|
|
4012
|
+
).length;
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
async function apiViewExactTextCount(api: HookApi, wrapper: string): Promise<number> {
|
|
4016
|
+
const messages = await boundedApiMessages(api);
|
|
4017
|
+
return exactTextCount(messages, wrapper);
|
|
3485
4018
|
}
|
|
3486
4019
|
|
|
3487
4020
|
async function boundedApiMessages(
|
|
@@ -3704,6 +4237,11 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
|
|
|
3704
4237
|
'baseline_count',
|
|
3705
4238
|
'expected_hash',
|
|
3706
4239
|
'expected_count',
|
|
4240
|
+
'expected_message_hashes',
|
|
4241
|
+
'delivery_mode',
|
|
4242
|
+
'marker_digest',
|
|
4243
|
+
'anchor_counts',
|
|
4244
|
+
'anchor_hashes',
|
|
3707
4245
|
'expires_at',
|
|
3708
4246
|
]);
|
|
3709
4247
|
if (Object.keys(value).some((key) => !allowedRootKeys.has(key))) return false;
|
|
@@ -3724,7 +4262,31 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
|
|
|
3724
4262
|
(value.baseline_count as number) >= 0 &&
|
|
3725
4263
|
(value.expected_hash === undefined || /^[a-f0-9]{64}$/u.test(String(value.expected_hash))) &&
|
|
3726
4264
|
(value.expected_count === undefined ||
|
|
3727
|
-
(Number.isSafeInteger(value.expected_count) && (value.expected_count as number) >= 0))
|
|
4265
|
+
(Number.isSafeInteger(value.expected_count) && (value.expected_count as number) >= 0)) &&
|
|
4266
|
+
(value.expected_message_hashes === undefined ||
|
|
4267
|
+
(Array.isArray(value.expected_message_hashes) &&
|
|
4268
|
+
value.expected_message_hashes.length === value.expected_count &&
|
|
4269
|
+
value.expected_message_hashes.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES &&
|
|
4270
|
+
value.expected_message_hashes.every(
|
|
4271
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{16}$/u.test(hash),
|
|
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')) &&
|
|
4277
|
+
((value.anchor_counts === undefined && value.anchor_hashes === undefined) ||
|
|
4278
|
+
(Array.isArray(value.anchor_counts) &&
|
|
4279
|
+
Array.isArray(value.anchor_hashes) &&
|
|
4280
|
+
value.anchor_counts.length === value.anchor_hashes.length &&
|
|
4281
|
+
value.anchor_counts.length >= 1 &&
|
|
4282
|
+
value.anchor_counts.length <= 3 &&
|
|
4283
|
+
value.anchor_counts.every(
|
|
4284
|
+
(count): count is number =>
|
|
4285
|
+
Number.isSafeInteger(count) && count >= 0 && count <= 10_000,
|
|
4286
|
+
) &&
|
|
4287
|
+
value.anchor_hashes.every(
|
|
4288
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{64}$/u.test(hash),
|
|
4289
|
+
)))
|
|
3728
4290
|
);
|
|
3729
4291
|
if (
|
|
3730
4292
|
value.kind !== 'custom' ||
|
|
@@ -4354,6 +4916,26 @@ function getTelemetry(
|
|
|
4354
4916
|
return state.current;
|
|
4355
4917
|
}
|
|
4356
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
|
+
|
|
4357
4939
|
async function queueNativeFallbackProof(
|
|
4358
4940
|
api: HookApi,
|
|
4359
4941
|
runtime: ProofRuntimeState,
|
|
@@ -4362,12 +4944,30 @@ async function queueNativeFallbackProof(
|
|
|
4362
4944
|
baselineHash: string,
|
|
4363
4945
|
baselineCount: number,
|
|
4364
4946
|
nativeResult: unknown,
|
|
4365
|
-
|
|
4947
|
+
beforeVisibleMessages: readonly string[] | undefined,
|
|
4948
|
+
activeOffset: number | undefined,
|
|
4949
|
+
): Promise<unknown> {
|
|
4366
4950
|
const expectedMessages = nativeResultMessages(nativeResult);
|
|
4367
4951
|
const expectedHash =
|
|
4368
4952
|
expectedMessages === undefined
|
|
4369
4953
|
? undefined
|
|
4370
4954
|
: await sha256(canonicalJson(visibleMessages(expectedMessages)));
|
|
4955
|
+
const expectedMessageHashes =
|
|
4956
|
+
expectedMessages !== undefined && expectedMessages.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES
|
|
4957
|
+
? await Promise.all(
|
|
4958
|
+
expectedMessages.map(async (message) =>
|
|
4959
|
+
(await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16),
|
|
4960
|
+
),
|
|
4961
|
+
)
|
|
4962
|
+
: undefined;
|
|
4963
|
+
const anchors =
|
|
4964
|
+
beforeVisibleMessages === undefined || expectedMessages === undefined
|
|
4965
|
+
? undefined
|
|
4966
|
+
: await nativeProofAnchors(beforeVisibleMessages, expectedMessages.length, activeOffset);
|
|
4967
|
+
const marker =
|
|
4968
|
+
expectedMessages === undefined
|
|
4969
|
+
? undefined
|
|
4970
|
+
: await nativeFallbackMarker(operationId, sessionRef, baselineHash);
|
|
4371
4971
|
const proof: PendingFallbackProof = {
|
|
4372
4972
|
schema_version: 1,
|
|
4373
4973
|
kind: 'native_fallback',
|
|
@@ -4378,10 +4978,25 @@ async function queueNativeFallbackProof(
|
|
|
4378
4978
|
...(expectedHash === undefined
|
|
4379
4979
|
? {}
|
|
4380
4980
|
: { expected_hash: expectedHash, expected_count: expectedMessages?.length ?? 0 }),
|
|
4381
|
-
|
|
4981
|
+
...(expectedMessageHashes === undefined
|
|
4982
|
+
? {}
|
|
4983
|
+
: { expected_message_hashes: expectedMessageHashes }),
|
|
4984
|
+
...(marker === undefined
|
|
4985
|
+
? {}
|
|
4986
|
+
: { delivery_mode: 'atomic_marker' as const, marker_digest: marker.digest }),
|
|
4987
|
+
...(anchors === undefined
|
|
4988
|
+
? {}
|
|
4989
|
+
: { anchor_counts: anchors.counts, anchor_hashes: anchors.hashes }),
|
|
4990
|
+
// Native fallback confirmation may occur after a long host compaction.
|
|
4991
|
+
// Keep its proof lifetime aligned with the durable proof bound instead of
|
|
4992
|
+
// expiring it after two minutes while Claude is still processing.
|
|
4993
|
+
expires_at: new Date(Date.now() + PENDING_PROOF_TTL_MS).toISOString(),
|
|
4382
4994
|
};
|
|
4383
4995
|
runtime.pendingProofMemory = proof;
|
|
4384
4996
|
runtime.pendingProofPersistenceFailed = !(await savePendingProof(api, proof));
|
|
4997
|
+
return runtime.pendingProofPersistenceFailed || marker === undefined
|
|
4998
|
+
? nativeResult
|
|
4999
|
+
: nativeFallbackDeliveryResult(nativeResult, marker.text);
|
|
4385
5000
|
}
|
|
4386
5001
|
|
|
4387
5002
|
async function confirmPendingProof(
|
|
@@ -4447,21 +5062,101 @@ async function confirmPendingProof(
|
|
|
4447
5062
|
return;
|
|
4448
5063
|
}
|
|
4449
5064
|
let verified = proof.kind === 'custom' ? await verifyCustomProof(proof, messages) : false;
|
|
5065
|
+
let fallbackMatch: NativeSummaryViewMatch | undefined;
|
|
5066
|
+
let fallbackProofFields: Pick<
|
|
5067
|
+
ObservationRecord,
|
|
5068
|
+
| 'proof_expected_count'
|
|
5069
|
+
| 'proof_actual_count'
|
|
5070
|
+
| 'proof_match'
|
|
5071
|
+
| 'proof_match_offset'
|
|
5072
|
+
| 'proof_view_source'
|
|
5073
|
+
| 'proof_api_view_match'
|
|
5074
|
+
| 'proof_api_count'
|
|
5075
|
+
> = {};
|
|
4450
5076
|
if (proof.kind === 'native_fallback') {
|
|
4451
5077
|
const expectedCount = proof.expected_count;
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
5078
|
+
fallbackProofFields = {
|
|
5079
|
+
...(expectedCount === undefined ? {} : { proof_expected_count: expectedCount }),
|
|
5080
|
+
proof_actual_count: messages.length,
|
|
5081
|
+
};
|
|
5082
|
+
if (proof.expected_hash !== undefined && expectedCount !== undefined) {
|
|
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
|
+
}
|
|
5147
|
+
}
|
|
4460
5148
|
}
|
|
4461
5149
|
if (runtime.pendingProofPersistenceFailed) verified = false;
|
|
4462
5150
|
const telemetry = getTelemetry(api, runtime.brainServer, runtime.options, runtime.telemetryState);
|
|
4463
5151
|
if (!verified) {
|
|
4464
|
-
|
|
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;
|
|
4465
5160
|
runtime.pendingProofMemory = undefined;
|
|
4466
5161
|
runtime.pendingProofPersistenceFailed = false;
|
|
4467
5162
|
await clearPendingProof(api, sessionRef);
|
|
@@ -4474,6 +5169,7 @@ async function confirmPendingProof(
|
|
|
4474
5169
|
runtime.observationInstanceId,
|
|
4475
5170
|
nativeClientVersion(runtime.options),
|
|
4476
5171
|
reason,
|
|
5172
|
+
fallbackProofFields,
|
|
4477
5173
|
);
|
|
4478
5174
|
if (runtime.diagnosticMode)
|
|
4479
5175
|
api.ui.log(
|
|
@@ -4485,6 +5181,16 @@ async function confirmPendingProof(
|
|
|
4485
5181
|
runtime.pendingProofMemory = undefined;
|
|
4486
5182
|
runtime.pendingProofPersistenceFailed = false;
|
|
4487
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
|
+
);
|
|
4488
5194
|
if (proof.kind === 'custom') {
|
|
4489
5195
|
const dropped = proof.pairs.filter((pair) => pair.action === 'drop_pair').length;
|
|
4490
5196
|
const reduced = proof.pairs.filter((pair) => pair.action === 'truncate_result').length;
|
|
@@ -4517,9 +5223,43 @@ async function recoverNativeSummaryNote(
|
|
|
4517
5223
|
}
|
|
4518
5224
|
const pendingOperationId = pending.operationId;
|
|
4519
5225
|
const telemetry = getTelemetry(api, runtime.brainServer, runtime.options, runtime.telemetryState);
|
|
5226
|
+
let proofFields: Pick<
|
|
5227
|
+
ObservationRecord,
|
|
5228
|
+
| 'proof_expected_count'
|
|
5229
|
+
| 'proof_actual_count'
|
|
5230
|
+
| 'proof_match'
|
|
5231
|
+
| 'proof_match_offset'
|
|
5232
|
+
| 'proof_view_source'
|
|
5233
|
+
| 'proof_api_view_match'
|
|
5234
|
+
| 'proof_api_count'
|
|
5235
|
+
| 'proof_delivery_source'
|
|
5236
|
+
| 'proof_delivery_match_count'
|
|
5237
|
+
> = {
|
|
5238
|
+
proof_expected_count: pending.deliveryMode === 'atomic_result' ? 1 : pending.expectedCount,
|
|
5239
|
+
};
|
|
5240
|
+
proofFields = {
|
|
5241
|
+
...proofFields,
|
|
5242
|
+
proof_delivery_source: pending.deliveryMode ?? 'legacy_append',
|
|
5243
|
+
proof_view_source: pending.deliveryMode === 'atomic_result' ? 'api' : 'default',
|
|
5244
|
+
proof_api_view_match: 'not_checked',
|
|
5245
|
+
};
|
|
4520
5246
|
const uncertain = async (reason: string): Promise<void> => {
|
|
4521
5247
|
runtime.pendingNativeSummary = undefined;
|
|
4522
5248
|
await clearNativeSummaryPending(api, sessionRef).catch(() => undefined);
|
|
5249
|
+
const diagnosticReason: ObservationRecord['reason'] =
|
|
5250
|
+
reason === 'pending_expired'
|
|
5251
|
+
? 'native_summary_proof_expired'
|
|
5252
|
+
: reason === 'native_view_ambiguous'
|
|
5253
|
+
? 'native_summary_host_view_ambiguous'
|
|
5254
|
+
: reason === 'native_prefix_mismatch'
|
|
5255
|
+
? 'native_summary_host_view_mismatch'
|
|
5256
|
+
: reason === 'messages_unavailable' ||
|
|
5257
|
+
reason === 'api_view_unavailable' ||
|
|
5258
|
+
reason === 'api_view_reread_failed'
|
|
5259
|
+
? 'native_summary_host_view_unavailable'
|
|
5260
|
+
: reason === 'note_read_failed'
|
|
5261
|
+
? 'native_summary_note_read_failed'
|
|
5262
|
+
: 'native_summary_attachment_uncertain';
|
|
4523
5263
|
await flushProofDiagnostic(
|
|
4524
5264
|
api,
|
|
4525
5265
|
runtime.observation,
|
|
@@ -4527,7 +5267,8 @@ async function recoverNativeSummaryNote(
|
|
|
4527
5267
|
sessionRef,
|
|
4528
5268
|
runtime.observationInstanceId,
|
|
4529
5269
|
nativeClientVersion(runtime.options),
|
|
4530
|
-
|
|
5270
|
+
diagnosticReason,
|
|
5271
|
+
proofFields,
|
|
4531
5272
|
);
|
|
4532
5273
|
if (runtime.diagnosticMode)
|
|
4533
5274
|
api.ui.log(
|
|
@@ -4539,6 +5280,71 @@ async function recoverNativeSummaryNote(
|
|
|
4539
5280
|
await uncertain('pending_expired');
|
|
4540
5281
|
return;
|
|
4541
5282
|
}
|
|
5283
|
+
if (pending.deliveryMode === 'atomic_result') {
|
|
5284
|
+
let noteText: string;
|
|
5285
|
+
try {
|
|
5286
|
+
noteText = await readNativeSummaryNote(
|
|
5287
|
+
api,
|
|
5288
|
+
runtime.options,
|
|
5289
|
+
pending.operationId,
|
|
5290
|
+
sessionRef,
|
|
5291
|
+
pending.noteSha256,
|
|
5292
|
+
);
|
|
5293
|
+
} catch {
|
|
5294
|
+
await uncertain('note_read_failed');
|
|
5295
|
+
return;
|
|
5296
|
+
}
|
|
5297
|
+
const wrapper = nativeSummaryWrapper(pending.capsuleId, noteText);
|
|
5298
|
+
let apiCount: number;
|
|
5299
|
+
let matchCount: number;
|
|
5300
|
+
try {
|
|
5301
|
+
const apiView = await boundedApiMessages(api);
|
|
5302
|
+
apiCount = apiView.length;
|
|
5303
|
+
matchCount = exactTextCount(apiView, wrapper);
|
|
5304
|
+
} catch {
|
|
5305
|
+
proofFields = { ...proofFields, proof_api_view_match: 'unavailable' };
|
|
5306
|
+
if (!allowAppend) return;
|
|
5307
|
+
await uncertain('api_view_unavailable');
|
|
5308
|
+
return;
|
|
5309
|
+
}
|
|
5310
|
+
proofFields = {
|
|
5311
|
+
...proofFields,
|
|
5312
|
+
proof_api_count: apiCount,
|
|
5313
|
+
proof_delivery_match_count: matchCount,
|
|
5314
|
+
proof_api_view_match: matchCount === 1 ? 'exact' : 'mismatch',
|
|
5315
|
+
proof_match: matchCount === 1 ? 'exact' : 'mismatch',
|
|
5316
|
+
};
|
|
5317
|
+
if (matchCount !== 1) {
|
|
5318
|
+
if (matchCount === 0 && !allowAppend) return;
|
|
5319
|
+
await uncertain('native_summary_attachment_uncertain');
|
|
5320
|
+
return;
|
|
5321
|
+
}
|
|
5322
|
+
await flushNativeSummaryProofDiagnostic(
|
|
5323
|
+
api,
|
|
5324
|
+
runtime.observation,
|
|
5325
|
+
pendingOperationId,
|
|
5326
|
+
sessionRef,
|
|
5327
|
+
runtime.observationInstanceId,
|
|
5328
|
+
nativeClientVersion(runtime.options),
|
|
5329
|
+
proofFields,
|
|
5330
|
+
);
|
|
5331
|
+
if (!pending.nativeCompletedReported) {
|
|
5332
|
+
const marked: NativeSummaryPending = { ...pending, nativeCompletedReported: true };
|
|
5333
|
+
try {
|
|
5334
|
+
await saveNativeSummaryPending(api, marked);
|
|
5335
|
+
pending = marked;
|
|
5336
|
+
runtime.pendingNativeSummary = marked;
|
|
5337
|
+
} catch {
|
|
5338
|
+
runtime.pendingNativeSummary = marked;
|
|
5339
|
+
pending = marked;
|
|
5340
|
+
}
|
|
5341
|
+
telemetry.nativeSummaryCompleted(pending.operationId);
|
|
5342
|
+
}
|
|
5343
|
+
runtime.pendingNativeSummary = undefined;
|
|
5344
|
+
await clearNativeSummaryPending(api, sessionRef);
|
|
5345
|
+
telemetry.recoveryAttached(pending.operationId);
|
|
5346
|
+
return;
|
|
5347
|
+
}
|
|
4542
5348
|
let messages: ClaudeMessage[];
|
|
4543
5349
|
try {
|
|
4544
5350
|
messages = await api.session.messages();
|
|
@@ -4546,10 +5352,60 @@ async function recoverNativeSummaryNote(
|
|
|
4546
5352
|
await uncertain('messages_unavailable');
|
|
4547
5353
|
return;
|
|
4548
5354
|
}
|
|
4549
|
-
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
|
|
5355
|
+
proofFields = { ...proofFields, proof_actual_count: messages.length };
|
|
5356
|
+
const viewMatch = await nativeSummaryViewMatch(
|
|
5357
|
+
messages,
|
|
5358
|
+
pending.expectedCount,
|
|
5359
|
+
pending.expectedPrefixHash,
|
|
5360
|
+
pending.expectedMessageHashes,
|
|
5361
|
+
pending.anchorCounts,
|
|
5362
|
+
pending.anchorHashes,
|
|
5363
|
+
);
|
|
5364
|
+
const viewOffset =
|
|
5365
|
+
viewMatch === 'active_suffix'
|
|
5366
|
+
? await nativeSummaryMatchedOffset(
|
|
5367
|
+
messages,
|
|
5368
|
+
pending.expectedCount,
|
|
5369
|
+
pending.expectedPrefixHash,
|
|
5370
|
+
pending.expectedMessageHashes,
|
|
5371
|
+
pending.anchorCounts,
|
|
5372
|
+
pending.anchorHashes,
|
|
5373
|
+
)
|
|
5374
|
+
: undefined;
|
|
5375
|
+
proofFields = {
|
|
5376
|
+
...proofFields,
|
|
5377
|
+
proof_match: viewMatch,
|
|
5378
|
+
...(viewMatch === 'exact' ? { proof_match_offset: 0 } : {}),
|
|
5379
|
+
...(viewOffset === undefined ? {} : { proof_match_offset: viewOffset }),
|
|
5380
|
+
};
|
|
5381
|
+
await flushNativeSummaryProofDiagnostic(
|
|
5382
|
+
api,
|
|
5383
|
+
runtime.observation,
|
|
5384
|
+
pendingOperationId,
|
|
5385
|
+
sessionRef,
|
|
5386
|
+
runtime.observationInstanceId,
|
|
5387
|
+
nativeClientVersion(runtime.options),
|
|
5388
|
+
proofFields,
|
|
5389
|
+
);
|
|
5390
|
+
if (runtime.diagnosticMode) {
|
|
5391
|
+
const apiView = await boundedApiMessages(api).catch(() => undefined);
|
|
5392
|
+
api.ui.log(
|
|
5393
|
+
`AI Brain context diagnostic ${JSON.stringify({
|
|
5394
|
+
hook: 'prompt.submit',
|
|
5395
|
+
phase: 'native_summary_view_probe',
|
|
5396
|
+
expected_count: pending.expectedCount,
|
|
5397
|
+
expected_hash: pending.expectedPrefixHash,
|
|
5398
|
+
default_count: messages.length,
|
|
5399
|
+
default_hash: await sha256(canonicalJson(visibleMessages(messages))),
|
|
5400
|
+
api_count: apiView?.length ?? null,
|
|
5401
|
+
api_hash: apiView === undefined ? null : await sha256(canonicalJson(apiView)),
|
|
5402
|
+
anchor_counts: pending.anchorCounts ?? null,
|
|
5403
|
+
view_match: viewMatch,
|
|
5404
|
+
})}`,
|
|
5405
|
+
);
|
|
5406
|
+
}
|
|
5407
|
+
if (viewMatch === 'mismatch' || viewMatch === 'ambiguous') {
|
|
5408
|
+
await uncertain(viewMatch === 'ambiguous' ? 'native_view_ambiguous' : 'native_prefix_mismatch');
|
|
4553
5409
|
return;
|
|
4554
5410
|
}
|
|
4555
5411
|
// The native result is now proven at the current session prefix. The note
|
|
@@ -4579,10 +5435,12 @@ async function recoverNativeSummaryNote(
|
|
|
4579
5435
|
await uncertain('note_read_failed');
|
|
4580
5436
|
return;
|
|
4581
5437
|
}
|
|
4582
|
-
const wrapper =
|
|
5438
|
+
const wrapper = nativeSummaryWrapper(pending.capsuleId, noteText);
|
|
4583
5439
|
let hasNote: boolean;
|
|
4584
5440
|
try {
|
|
4585
|
-
|
|
5441
|
+
const matchCount = await apiViewExactTextCount(api, wrapper);
|
|
5442
|
+
proofFields = { ...proofFields, proof_delivery_match_count: matchCount };
|
|
5443
|
+
hasNote = matchCount === 1;
|
|
4586
5444
|
} catch {
|
|
4587
5445
|
if (!allowAppend && pending.appendAttempted) await uncertain('api_view_unavailable');
|
|
4588
5446
|
return;
|
|
@@ -4611,7 +5469,9 @@ async function recoverNativeSummaryNote(
|
|
|
4611
5469
|
}
|
|
4612
5470
|
let apiHasNoteAfter: boolean;
|
|
4613
5471
|
try {
|
|
4614
|
-
|
|
5472
|
+
const matchCount = await apiViewExactTextCount(api, wrapper);
|
|
5473
|
+
proofFields = { ...proofFields, proof_delivery_match_count: matchCount };
|
|
5474
|
+
apiHasNoteAfter = matchCount === 1;
|
|
4615
5475
|
} catch {
|
|
4616
5476
|
if (!allowAppend)
|
|
4617
5477
|
await uncertain(appendError ? 'append_ack_ambiguous' : 'api_view_reread_failed');
|
|
@@ -4625,6 +5485,15 @@ async function recoverNativeSummaryNote(
|
|
|
4625
5485
|
}
|
|
4626
5486
|
runtime.pendingNativeSummary = undefined;
|
|
4627
5487
|
await clearNativeSummaryPending(api, sessionRef);
|
|
5488
|
+
await flushNativeSummaryProofDiagnostic(
|
|
5489
|
+
api,
|
|
5490
|
+
runtime.observation,
|
|
5491
|
+
pending.operationId,
|
|
5492
|
+
sessionRef,
|
|
5493
|
+
runtime.observationInstanceId,
|
|
5494
|
+
nativeClientVersion(runtime.options),
|
|
5495
|
+
proofFields,
|
|
5496
|
+
);
|
|
4628
5497
|
telemetry.recoveryAttached(pending.operationId);
|
|
4629
5498
|
}
|
|
4630
5499
|
|
|
@@ -4790,7 +5659,11 @@ async function runShadowPreview(
|
|
|
4790
5659
|
);
|
|
4791
5660
|
}
|
|
4792
5661
|
|
|
4793
|
-
|
|
5662
|
+
const registerWithClock = (
|
|
5663
|
+
on: Parameters<Register>[0],
|
|
5664
|
+
options: HookOptions,
|
|
5665
|
+
monotonicClock: MonotonicClock,
|
|
5666
|
+
): void => {
|
|
4794
5667
|
const enabled = options.enabled === true;
|
|
4795
5668
|
const disclosed = options.disclosed === true;
|
|
4796
5669
|
const preservationMode: PreservationMode =
|
|
@@ -4908,7 +5781,10 @@ export const register: Register = (on, options) => {
|
|
|
4908
5781
|
|
|
4909
5782
|
on('prompt.submit', async (api, rawEvent, next) => {
|
|
4910
5783
|
const event = rawEvent as unknown as PromptSubmitHookEvent;
|
|
4911
|
-
|
|
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);
|
|
4912
5788
|
if (
|
|
4913
5789
|
proofRuntime.compactionStrategy === 'native_summary_notes' &&
|
|
4914
5790
|
enabled &&
|
|
@@ -4983,7 +5859,10 @@ export const register: Register = (on, options) => {
|
|
|
4983
5859
|
});
|
|
4984
5860
|
|
|
4985
5861
|
on('turn.complete', async (api, event, next) => {
|
|
4986
|
-
|
|
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);
|
|
4987
5866
|
if (
|
|
4988
5867
|
proofRuntime.compactionStrategy === 'native_summary_notes' &&
|
|
4989
5868
|
enabled &&
|
|
@@ -5155,7 +6034,7 @@ export const register: Register = (on, options) => {
|
|
|
5155
6034
|
nativeContinuationStarted = true;
|
|
5156
6035
|
const nativeResult = await next(event);
|
|
5157
6036
|
if (fallbackCapture !== undefined)
|
|
5158
|
-
|
|
6037
|
+
return queueNativeFallbackProof(
|
|
5159
6038
|
api,
|
|
5160
6039
|
proofRuntime,
|
|
5161
6040
|
fallbackCapture.operationId,
|
|
@@ -5163,6 +6042,8 @@ export const register: Register = (on, options) => {
|
|
|
5163
6042
|
fallbackCapture.baselineHash,
|
|
5164
6043
|
fallbackCapture.baselineCount,
|
|
5165
6044
|
nativeResult,
|
|
6045
|
+
fallbackCapture.beforeVisibleMessages,
|
|
6046
|
+
fallbackCapture.activeOffset,
|
|
5166
6047
|
);
|
|
5167
6048
|
return nativeResult;
|
|
5168
6049
|
};
|
|
@@ -5225,6 +6106,8 @@ export const register: Register = (on, options) => {
|
|
|
5225
6106
|
| 'request_id'
|
|
5226
6107
|
| 'correlation_id'
|
|
5227
6108
|
| 'retry_after_ms'
|
|
6109
|
+
| 'provider_response_format'
|
|
6110
|
+
| 'reason'
|
|
5228
6111
|
> = {},
|
|
5229
6112
|
): void => {
|
|
5230
6113
|
if (!observation.diagnosticsEnabled || diagnosticRecords.length >= 64) return;
|
|
@@ -5285,6 +6168,8 @@ export const register: Register = (on, options) => {
|
|
|
5285
6168
|
let proofSessionRef: string | undefined;
|
|
5286
6169
|
let proofBaselineHash: string | undefined;
|
|
5287
6170
|
let proofBaselineCount = 0;
|
|
6171
|
+
let baselineActiveOffset: number | undefined;
|
|
6172
|
+
let proofBaselineVisibleMessages: readonly string[] | undefined;
|
|
5288
6173
|
let historicalNotes: readonly VerifiedHistoricalNote[] = [];
|
|
5289
6174
|
try {
|
|
5290
6175
|
const current = event.messages;
|
|
@@ -5303,14 +6188,22 @@ export const register: Register = (on, options) => {
|
|
|
5303
6188
|
});
|
|
5304
6189
|
const rereadBaselineHash = await sha256(canonicalJson(reread));
|
|
5305
6190
|
proofBaselineHash = rereadBaselineHash;
|
|
5306
|
-
proofBaselineCount =
|
|
5307
|
-
|
|
6191
|
+
proofBaselineCount = reread.length;
|
|
6192
|
+
const rereadActiveOffset = visibleSnapshotsMatch(
|
|
6193
|
+
current,
|
|
6194
|
+
reread,
|
|
6195
|
+
event.trigger,
|
|
6196
|
+
event.instructions,
|
|
6197
|
+
);
|
|
6198
|
+
if (rereadActiveOffset === undefined) {
|
|
5308
6199
|
await diagnosticMismatch(api, diagnosticMode, 'reread_before', current, reread);
|
|
5309
6200
|
markDiagnostic('snapshot', 'invalid_response');
|
|
5310
6201
|
markDiagnostic('fallback', 'invalid_response');
|
|
5311
6202
|
logFallback(api, 'reread_before', undefined, policyVersion === 2);
|
|
5312
6203
|
return continueWithTelemetryFlush();
|
|
5313
6204
|
}
|
|
6205
|
+
baselineActiveOffset = rereadActiveOffset;
|
|
6206
|
+
proofBaselineVisibleMessages = visibleMessageCanonicalStrings(reread);
|
|
5314
6207
|
stage = 'projection';
|
|
5315
6208
|
const built = buildProjection(
|
|
5316
6209
|
current,
|
|
@@ -5359,7 +6252,7 @@ export const register: Register = (on, options) => {
|
|
|
5359
6252
|
markDiagnostic('budget', 'provider_not_attempted', {
|
|
5360
6253
|
item_count: built.pairs.filter((pair) => !pair.protectedPair).length,
|
|
5361
6254
|
});
|
|
5362
|
-
const budgetRead = await readContextBudget(api, brainServer);
|
|
6255
|
+
const budgetRead = await readContextBudget(api, brainServer, monotonicClock);
|
|
5363
6256
|
if (!budgetRead.ok) {
|
|
5364
6257
|
const admission = budgetRead.kind === 'admission_denied';
|
|
5365
6258
|
const category: ObservationRecord['category'] = admission
|
|
@@ -5374,8 +6267,13 @@ export const register: Register = (on, options) => {
|
|
|
5374
6267
|
const errorMetadata: Partial<ContextErrorMetadata> = isRecord(budgetRead.error)
|
|
5375
6268
|
? (budgetRead.error as ContextErrorMetadata)
|
|
5376
6269
|
: {};
|
|
6270
|
+
const budgetErrorCode = admission
|
|
6271
|
+
? 'shared_admission_exceeded'
|
|
6272
|
+
: budgetRead.kind === 'invalid_response'
|
|
6273
|
+
? 'budget_response_invalid'
|
|
6274
|
+
: 'provider_unavailable';
|
|
5377
6275
|
markDiagnostic('budget', category, {
|
|
5378
|
-
compact_error_code:
|
|
6276
|
+
compact_error_code: budgetErrorCode,
|
|
5379
6277
|
...(typeof errorMetadata.admissionDetail === 'string'
|
|
5380
6278
|
? { admission_detail: errorMetadata.admissionDetail }
|
|
5381
6279
|
: {}),
|
|
@@ -5390,7 +6288,7 @@ export const register: Register = (on, options) => {
|
|
|
5390
6288
|
: {}),
|
|
5391
6289
|
});
|
|
5392
6290
|
markDiagnostic('fallback', category, {
|
|
5393
|
-
compact_error_code:
|
|
6291
|
+
compact_error_code: budgetErrorCode,
|
|
5394
6292
|
});
|
|
5395
6293
|
logFallback(
|
|
5396
6294
|
api,
|
|
@@ -5406,7 +6304,9 @@ export const register: Register = (on, options) => {
|
|
|
5406
6304
|
operationId,
|
|
5407
6305
|
sessionRef,
|
|
5408
6306
|
baselineHash: rereadBaselineHash,
|
|
5409
|
-
baselineCount:
|
|
6307
|
+
baselineCount: reread.length,
|
|
6308
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6309
|
+
activeOffset: baselineActiveOffset,
|
|
5410
6310
|
});
|
|
5411
6311
|
}
|
|
5412
6312
|
const budget = budgetRead.snapshot;
|
|
@@ -5431,7 +6331,9 @@ export const register: Register = (on, options) => {
|
|
|
5431
6331
|
operationId,
|
|
5432
6332
|
sessionRef,
|
|
5433
6333
|
baselineHash: rereadBaselineHash,
|
|
5434
|
-
baselineCount:
|
|
6334
|
+
baselineCount: reread.length,
|
|
6335
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6336
|
+
activeOffset: baselineActiveOffset,
|
|
5435
6337
|
});
|
|
5436
6338
|
}
|
|
5437
6339
|
let providerProjection = built.projection;
|
|
@@ -5506,7 +6408,9 @@ export const register: Register = (on, options) => {
|
|
|
5506
6408
|
operationId,
|
|
5507
6409
|
sessionRef,
|
|
5508
6410
|
baselineHash: rereadBaselineHash,
|
|
5509
|
-
baselineCount:
|
|
6411
|
+
baselineCount: reread.length,
|
|
6412
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6413
|
+
activeOffset: baselineActiveOffset,
|
|
5510
6414
|
});
|
|
5511
6415
|
}
|
|
5512
6416
|
providerProjection = window.projection;
|
|
@@ -5552,18 +6456,42 @@ export const register: Register = (on, options) => {
|
|
|
5552
6456
|
projection: providerProjection,
|
|
5553
6457
|
...(policyVersion === 2 ? { schema_version: 2, options: wireOptions } : {}),
|
|
5554
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
|
+
}
|
|
5555
6481
|
stage = 'mcp_call';
|
|
5556
6482
|
markDiagnostic('provider_request', 'request_attempted', {
|
|
5557
6483
|
item_count: providerPairs.filter((pair) => !pair.protectedPair).length,
|
|
5558
6484
|
byte_count: byteLength(JSON.stringify(input)),
|
|
5559
6485
|
});
|
|
5560
|
-
await persistDiagnostics();
|
|
5561
6486
|
if (diagnosticMode)
|
|
5562
6487
|
api.ui.log(
|
|
5563
6488
|
`AI Brain context diagnostic ${JSON.stringify({ hook: 'session.compact', phase: 'mcp_started' })}`,
|
|
5564
6489
|
);
|
|
5565
6490
|
const call = api.mcp.call(brainServer, 'brain_context_compact', input);
|
|
5566
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();
|
|
5567
6495
|
let timedOut = false;
|
|
5568
6496
|
const hostTimers = globalThis as unknown as {
|
|
5569
6497
|
setTimeout?: (callback: () => void, delay: number) => unknown;
|
|
@@ -5610,13 +6538,41 @@ export const register: Register = (on, options) => {
|
|
|
5610
6538
|
markDiagnostic(
|
|
5611
6539
|
'provider_response',
|
|
5612
6540
|
response.isError === true ? 'native_error' : 'response_obtained',
|
|
6541
|
+
providerResponseMetadata(response),
|
|
5613
6542
|
);
|
|
5614
6543
|
if (response.isError === true) throw providerResponseError(response);
|
|
5615
6544
|
stage = 'parse_plan';
|
|
5616
|
-
const structured = mcpStructuredValue(response);
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
6545
|
+
const structured = mcpStructuredValue(response, MAX_RESULT_BYTES);
|
|
6546
|
+
let plan: Record<string, unknown>;
|
|
6547
|
+
try {
|
|
6548
|
+
const serialized = JSON.stringify(structured);
|
|
6549
|
+
const responseText = response.content?.find(
|
|
6550
|
+
(block): block is { readonly type: string; readonly text: string } =>
|
|
6551
|
+
block.type === 'text' && typeof block.text === 'string',
|
|
6552
|
+
)?.text;
|
|
6553
|
+
if (
|
|
6554
|
+
(serialized === undefined &&
|
|
6555
|
+
response.structuredContent === undefined &&
|
|
6556
|
+
responseText === undefined) ||
|
|
6557
|
+
(response.structuredContent === undefined &&
|
|
6558
|
+
responseText !== undefined &&
|
|
6559
|
+
byteLength(responseText) > MAX_RESULT_BYTES) ||
|
|
6560
|
+
(serialized !== undefined && byteLength(serialized) > MAX_RESULT_BYTES)
|
|
6561
|
+
)
|
|
6562
|
+
throw new Error(
|
|
6563
|
+
serialized === undefined &&
|
|
6564
|
+
response.structuredContent === undefined &&
|
|
6565
|
+
responseText === undefined
|
|
6566
|
+
? 'AI Brain plan envelope is missing'
|
|
6567
|
+
: 'AI Brain response is too large',
|
|
6568
|
+
);
|
|
6569
|
+
plan = parsePlan(structured, policyVersion, v2Options);
|
|
6570
|
+
} catch (error) {
|
|
6571
|
+
throw planBindingFailure(
|
|
6572
|
+
planParseFailureReason(error),
|
|
6573
|
+
'AI Brain plan failed validation',
|
|
6574
|
+
);
|
|
6575
|
+
}
|
|
5620
6576
|
const planDecisions = plan.decisions;
|
|
5621
6577
|
const planStats = plan.stats;
|
|
5622
6578
|
markDiagnostic('plan_validation', 'provider_decision', {
|
|
@@ -5627,22 +6583,30 @@ export const register: Register = (on, options) => {
|
|
|
5627
6583
|
`AI Brain context diagnostic ${JSON.stringify({ hook: 'session.compact', phase: 'plan_ready', status: plan.status, eligible_count: isRecord(planStats) && typeof planStats.eligible_count === 'number' ? planStats.eligible_count : 0, batch_count: isRecord(planStats) && typeof planStats.batch_count === 'number' ? planStats.batch_count : 0 })}`,
|
|
5628
6584
|
);
|
|
5629
6585
|
stage = 'match_plan';
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
(policyVersion === 2
|
|
5635
|
-
|
|
5636
|
-
)
|
|
5637
|
-
throw
|
|
6586
|
+
const bindingReason = planBindingFailureReason(plan, {
|
|
6587
|
+
sessionRef,
|
|
6588
|
+
revision,
|
|
6589
|
+
projectionHash: await sha256(canonicalJson(providerProjection)),
|
|
6590
|
+
...(policyVersion === 2 ? { optionsHash: optionsHash ?? null } : {}),
|
|
6591
|
+
});
|
|
6592
|
+
if (policyVersion === 2 && optionsHash === undefined)
|
|
6593
|
+
throw planBindingFailure('plan_options_mismatch', 'AI Brain plan options are missing');
|
|
6594
|
+
if (bindingReason !== undefined)
|
|
6595
|
+
throw planBindingFailure(bindingReason, 'AI Brain plan binding was rejected');
|
|
5638
6596
|
if ((await api.session.id()) !== sessionRef)
|
|
5639
|
-
throw
|
|
6597
|
+
throw planBindingFailure(
|
|
6598
|
+
'session_changed_during_planning',
|
|
6599
|
+
'Claude session changed during planning',
|
|
6600
|
+
);
|
|
5640
6601
|
stage = 'reread_after';
|
|
5641
6602
|
const latestVisible = await api.session.messages();
|
|
5642
6603
|
if ((await sha256(canonicalJson(latestVisible))) !== rereadBaselineHash) {
|
|
5643
6604
|
await diagnosticMismatch(api, diagnosticMode, 'reread_after', current, latestVisible);
|
|
5644
6605
|
logFallback(api, 'reread_after', undefined, policyVersion === 2);
|
|
5645
|
-
throw
|
|
6606
|
+
throw planBindingFailure(
|
|
6607
|
+
'snapshot_changed_during_planning',
|
|
6608
|
+
'Claude transcript changed during planning',
|
|
6609
|
+
);
|
|
5646
6610
|
}
|
|
5647
6611
|
if (compactionStrategy === 'native_summary_notes') {
|
|
5648
6612
|
const latestHistoricalNotes = await collectVerifiedHistoricalNotes(
|
|
@@ -5655,7 +6619,10 @@ export const register: Register = (on, options) => {
|
|
|
5655
6619
|
latestHistoricalNotes.map((note) => [note.capsuleId, note.noteSha256]),
|
|
5656
6620
|
) !== canonicalJson(historicalNotes.map((note) => [note.capsuleId, note.noteSha256]))
|
|
5657
6621
|
)
|
|
5658
|
-
throw
|
|
6622
|
+
throw planBindingFailure(
|
|
6623
|
+
'historical_notes_changed_during_planning',
|
|
6624
|
+
'Claude historical notes changed during planning',
|
|
6625
|
+
);
|
|
5659
6626
|
}
|
|
5660
6627
|
if (plan.status === 'noop') {
|
|
5661
6628
|
markDiagnostic('candidate_preparation', 'skipped');
|
|
@@ -5729,6 +6696,7 @@ export const register: Register = (on, options) => {
|
|
|
5729
6696
|
transformed,
|
|
5730
6697
|
);
|
|
5731
6698
|
getTelemetry(api, brainServer, options, telemetryState).notesWritten(operationId);
|
|
6699
|
+
const beforeVisibleMessages = visibleMessageCanonicalStrings(reread);
|
|
5732
6700
|
nativeContinuationStarted = true;
|
|
5733
6701
|
const nativeInput = appendHistoricalNotes(transformed, historicalNotes);
|
|
5734
6702
|
const nativeResult = await next({ ...event, messages: nativeInput });
|
|
@@ -5738,6 +6706,29 @@ export const register: Register = (on, options) => {
|
|
|
5738
6706
|
throw new Error('native_summary_result_missing');
|
|
5739
6707
|
const configuration = fileCheckpointConfiguration(options);
|
|
5740
6708
|
if (configuration.mode !== 'helper') throw new Error('native_note_helper_unavailable');
|
|
6709
|
+
const expectedMessageHashes =
|
|
6710
|
+
expectedMessages.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES
|
|
6711
|
+
? await Promise.all(
|
|
6712
|
+
expectedMessages.map(async (message) =>
|
|
6713
|
+
(await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16),
|
|
6714
|
+
),
|
|
6715
|
+
)
|
|
6716
|
+
: undefined;
|
|
6717
|
+
const wrapper = nativeSummaryWrapper(note.capsuleId, note.noteText);
|
|
6718
|
+
const deliveredResult = nativeSummaryDeliveryResult(nativeResult, wrapper);
|
|
6719
|
+
if (deliveredResult === undefined) {
|
|
6720
|
+
getTelemetry(api, brainServer, options, telemetryState).uncertain(
|
|
6721
|
+
operationId,
|
|
6722
|
+
'native_summary_recovery',
|
|
6723
|
+
);
|
|
6724
|
+
return nativeResult;
|
|
6725
|
+
}
|
|
6726
|
+
const deliveryMode: NativeSummaryPending['deliveryMode'] = 'atomic_result';
|
|
6727
|
+
const anchors = await nativeProofAnchors(
|
|
6728
|
+
beforeVisibleMessages,
|
|
6729
|
+
expectedMessages.length,
|
|
6730
|
+
baselineActiveOffset,
|
|
6731
|
+
);
|
|
5741
6732
|
const pendingNativeSummary: NativeSummaryPending = {
|
|
5742
6733
|
schemaVersion: 1,
|
|
5743
6734
|
operationId,
|
|
@@ -5747,13 +6738,19 @@ export const register: Register = (on, options) => {
|
|
|
5747
6738
|
noteSha256: note.noteSha256,
|
|
5748
6739
|
expectedPrefixHash: await sha256(canonicalJson(visibleMessages(expectedMessages))),
|
|
5749
6740
|
expectedCount: expectedMessages.length,
|
|
6741
|
+
...(expectedMessageHashes === undefined ? {} : { expectedMessageHashes }),
|
|
6742
|
+
deliveryMode,
|
|
6743
|
+
anchorCounts: anchors.counts,
|
|
6744
|
+
anchorHashes: anchors.hashes,
|
|
5750
6745
|
expiresAt: new Date(Date.now() + PENDING_PROOF_TTL_MS).toISOString(),
|
|
5751
6746
|
appendAttempted: false,
|
|
5752
6747
|
nativeCompletedReported: false,
|
|
5753
6748
|
};
|
|
6749
|
+
let deliveryIntentSaved = false;
|
|
5754
6750
|
try {
|
|
5755
6751
|
await saveNativeSummaryPending(api, pendingNativeSummary);
|
|
5756
6752
|
proofRuntime.pendingNativeSummary = pendingNativeSummary;
|
|
6753
|
+
deliveryIntentSaved = true;
|
|
5757
6754
|
} catch {
|
|
5758
6755
|
if (nativeSessionRef !== undefined)
|
|
5759
6756
|
await flushProofDiagnostic(
|
|
@@ -5763,7 +6760,7 @@ export const register: Register = (on, options) => {
|
|
|
5763
6760
|
nativeSessionRef,
|
|
5764
6761
|
observationInstanceId,
|
|
5765
6762
|
nativeClientVersion(options),
|
|
5766
|
-
'
|
|
6763
|
+
'native_summary_attachment_uncertain',
|
|
5767
6764
|
);
|
|
5768
6765
|
getTelemetry(api, brainServer, options, telemetryState).uncertain(
|
|
5769
6766
|
operationId,
|
|
@@ -5772,7 +6769,7 @@ export const register: Register = (on, options) => {
|
|
|
5772
6769
|
}
|
|
5773
6770
|
if (options.telemetryEnabled === true && options.telemetryDisclosed === true)
|
|
5774
6771
|
await getTelemetry(api, brainServer, options, telemetryState).flush();
|
|
5775
|
-
return nativeResult;
|
|
6772
|
+
return deliveryIntentSaved ? (deliveredResult ?? nativeResult) : nativeResult;
|
|
5776
6773
|
}
|
|
5777
6774
|
return { messages: transformed };
|
|
5778
6775
|
} catch (error) {
|
|
@@ -5791,7 +6788,7 @@ export const register: Register = (on, options) => {
|
|
|
5791
6788
|
nativeSessionRef,
|
|
5792
6789
|
observationInstanceId,
|
|
5793
6790
|
nativeClientVersion(options),
|
|
5794
|
-
'
|
|
6791
|
+
'native_summary_attachment_uncertain',
|
|
5795
6792
|
);
|
|
5796
6793
|
}
|
|
5797
6794
|
throw error;
|
|
@@ -5817,6 +6814,14 @@ export const register: Register = (on, options) => {
|
|
|
5817
6814
|
const rpcStatus = safeRpcStatus(error);
|
|
5818
6815
|
const checkpoint = failureStage === 'checkpoint' ? checkpointFailure(error) : undefined;
|
|
5819
6816
|
const errorMetadata = isRecord(error) ? error : {};
|
|
6817
|
+
const planReason =
|
|
6818
|
+
failureStage === 'parse_plan' ||
|
|
6819
|
+
failureStage === 'match_plan' ||
|
|
6820
|
+
failureStage === 'reread_after'
|
|
6821
|
+
? isPlanBindingFailureReason(errorMetadata.planBindingFailureReason)
|
|
6822
|
+
? errorMetadata.planBindingFailureReason
|
|
6823
|
+
: undefined
|
|
6824
|
+
: undefined;
|
|
5820
6825
|
const admissionFallback = isAdmissionFallback(error);
|
|
5821
6826
|
const compactCode =
|
|
5822
6827
|
isRecord(error) &&
|
|
@@ -5855,6 +6860,7 @@ export const register: Register = (on, options) => {
|
|
|
5855
6860
|
...(typeof errorMetadata.retryAfterMs === 'number'
|
|
5856
6861
|
? { retry_after_ms: errorMetadata.retryAfterMs }
|
|
5857
6862
|
: {}),
|
|
6863
|
+
...(planReason === undefined ? {} : { reason: planReason }),
|
|
5858
6864
|
...(checkpoint === undefined
|
|
5859
6865
|
? {}
|
|
5860
6866
|
: { checkpoint_phase: checkpoint.phase, checkpoint_error: checkpoint.code }),
|
|
@@ -5867,6 +6873,7 @@ export const register: Register = (on, options) => {
|
|
|
5867
6873
|
markDiagnostic('fallback', failureCategory, {
|
|
5868
6874
|
...(rpcStatus === undefined ? {} : { rpc_status: rpcStatus }),
|
|
5869
6875
|
...(compactCode === undefined ? {} : { compact_error_code: compactCode }),
|
|
6876
|
+
...(planReason === undefined ? {} : { reason: planReason }),
|
|
5870
6877
|
});
|
|
5871
6878
|
const telemetry = getTelemetry(api, brainServer, options, telemetryState);
|
|
5872
6879
|
telemetry.fallback(operationId, admissionFallback ? 'admission_denied' : stage);
|
|
@@ -5878,6 +6885,12 @@ export const register: Register = (on, options) => {
|
|
|
5878
6885
|
sessionRef: proofSessionRef,
|
|
5879
6886
|
baselineHash: proofBaselineHash,
|
|
5880
6887
|
baselineCount: proofBaselineCount,
|
|
6888
|
+
...(proofBaselineVisibleMessages === undefined
|
|
6889
|
+
? {}
|
|
6890
|
+
: { beforeVisibleMessages: proofBaselineVisibleMessages }),
|
|
6891
|
+
...(baselineActiveOffset === undefined
|
|
6892
|
+
? {}
|
|
6893
|
+
: { activeOffset: baselineActiveOffset }),
|
|
5881
6894
|
}
|
|
5882
6895
|
: undefined,
|
|
5883
6896
|
);
|
|
@@ -5905,3 +6918,13 @@ export const register: Register = (on, options) => {
|
|
|
5905
6918
|
});
|
|
5906
6919
|
});
|
|
5907
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
|
+
}
|