@brunobracaioli/aibrain 0.3.0-beta.7 → 0.4.0-beta.10
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 +117 -16
- package/dist/assets/claude/.claude-plugin/plugin.json +1 -1
- package/dist/assets/claude/hooks/context-compaction.ts +1104 -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 +2273 -191
- 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 +141 -30
- package/dist/codex-compatibility.mjs +531 -0
- package/dist/codex-diagnostics.mjs +39 -3
- package/dist/codex-engineering.mjs +165 -0
- package/dist/codex-protocol-fingerprint.json +131 -0
- package/dist/engineering-pack-public-key.json +10 -0
- package/dist/engineering-pack.mjs +845 -0
- package/dist/engineering.mjs +194 -0
- package/dist/extensions-catalog.mjs +310 -0
- package/dist/extensions-cli.mjs +486 -0
- package/dist/extensions-preferences.mjs +151 -0
- package/dist/guardrails/continuity.mjs +239 -0
- package/dist/guardrails/format.mjs +111 -0
- package/dist/guardrails/guards.mjs +397 -0
- package/dist/guardrails/run.mjs +211 -0
- package/dist/guardrails/shell-words.mjs +206 -0
- package/dist/guardrails-plugin.mjs +149 -0
- package/dist/launch.mjs +170 -17
- package/dist/mcp-servers.mjs +192 -0
- package/dist/mcp.mjs +8 -8
- package/dist/monitor.mjs +33 -0
- package/dist/package.json +1 -1
- package/dist/platform.mjs +81 -7
- 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.7';
|
|
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
|
}
|
|
@@ -341,6 +463,10 @@ function v2WireOptions(options: V2Options): Record<string, unknown> {
|
|
|
341
463
|
|
|
342
464
|
function safeErrorCategory(error: unknown): string {
|
|
343
465
|
const record = isRecord(error) ? error : {};
|
|
466
|
+
// A coded error names its own reason; the keyword guess below would read
|
|
467
|
+
// "AI Brain plan does not meet the minimum estimated reduction" as invalid_plan.
|
|
468
|
+
if (COMPACT_ERROR_CODES.includes(record.compactErrorCode as CompactErrorCode))
|
|
469
|
+
return record.compactErrorCode as CompactErrorCode;
|
|
344
470
|
const code = typeof record.code === 'string' ? record.code.toLowerCase() : '';
|
|
345
471
|
const name = typeof record.name === 'string' ? record.name.toLowerCase() : '';
|
|
346
472
|
const message = typeof record.message === 'string' ? record.message.toLowerCase() : '';
|
|
@@ -565,6 +691,8 @@ type ContextBudgetSnapshot = {
|
|
|
565
691
|
readonly accountInFlight: boolean;
|
|
566
692
|
readonly retryAfterMs: number;
|
|
567
693
|
readonly expiresAt: string;
|
|
694
|
+
readonly requestStartedAtMonotonic: number;
|
|
695
|
+
readonly lifetimeMs: number;
|
|
568
696
|
readonly windowEnd: string;
|
|
569
697
|
readonly correlationId: string;
|
|
570
698
|
};
|
|
@@ -578,7 +706,25 @@ type ContextBudgetRead =
|
|
|
578
706
|
readonly error?: Error | undefined;
|
|
579
707
|
};
|
|
580
708
|
|
|
581
|
-
function
|
|
709
|
+
function monotonicNow(): number | undefined {
|
|
710
|
+
const host = globalThis as unknown as {
|
|
711
|
+
readonly performance?: { readonly now?: () => number };
|
|
712
|
+
};
|
|
713
|
+
const now = host.performance?.now;
|
|
714
|
+
if (typeof now !== 'function') return undefined;
|
|
715
|
+
try {
|
|
716
|
+
const value = now.call(host.performance);
|
|
717
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
718
|
+
} catch {
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function parseContextBudget(
|
|
724
|
+
value: unknown,
|
|
725
|
+
requestStartedAt: number,
|
|
726
|
+
responseReceivedAt: number,
|
|
727
|
+
): ContextBudgetSnapshot | undefined {
|
|
582
728
|
if (!isRecord(value)) return undefined;
|
|
583
729
|
const allowed = [
|
|
584
730
|
'schema_version',
|
|
@@ -606,9 +752,22 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
606
752
|
!Number.isFinite(Date.parse(value.window_end))
|
|
607
753
|
)
|
|
608
754
|
return undefined;
|
|
755
|
+
const serverTime = Date.parse(value.server_time);
|
|
609
756
|
const expiresAt = Date.parse(value.expires_at);
|
|
610
757
|
const windowEnd = Date.parse(value.window_end);
|
|
611
|
-
|
|
758
|
+
const snapshotLifetimeMs = expiresAt - serverTime;
|
|
759
|
+
const requestElapsedMs = responseReceivedAt - requestStartedAt;
|
|
760
|
+
if (
|
|
761
|
+
!Number.isFinite(requestStartedAt) ||
|
|
762
|
+
!Number.isFinite(responseReceivedAt) ||
|
|
763
|
+
requestStartedAt < 0 ||
|
|
764
|
+
requestElapsedMs < 0 ||
|
|
765
|
+
!Number.isSafeInteger(snapshotLifetimeMs) ||
|
|
766
|
+
snapshotLifetimeMs <= 0 ||
|
|
767
|
+
snapshotLifetimeMs > BUDGET_SNAPSHOT_MAX_AGE_MS ||
|
|
768
|
+
requestElapsedMs >= snapshotLifetimeMs ||
|
|
769
|
+
windowEnd < expiresAt
|
|
770
|
+
)
|
|
612
771
|
return undefined;
|
|
613
772
|
const nonnegative = (candidate: unknown): candidate is number =>
|
|
614
773
|
typeof candidate === 'number' && Number.isSafeInteger(candidate) && candidate >= 0;
|
|
@@ -621,10 +780,8 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
621
780
|
!nonnegative(value.retry_after_ms) ||
|
|
622
781
|
value.retry_after_ms > 3_600_000 ||
|
|
623
782
|
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))
|
|
783
|
+
value.provider_batches !== 0 ||
|
|
784
|
+
value.provider_model !== null
|
|
628
785
|
)
|
|
629
786
|
return undefined;
|
|
630
787
|
const correlationId = boundedSafeIdentifier(value.correlation_id);
|
|
@@ -637,18 +794,20 @@ function parseContextBudget(value: unknown, now = Date.now()): ContextBudgetSnap
|
|
|
637
794
|
accountInFlight: value.account_in_flight,
|
|
638
795
|
retryAfterMs: value.retry_after_ms,
|
|
639
796
|
expiresAt: value.expires_at,
|
|
797
|
+
requestStartedAtMonotonic: requestStartedAt,
|
|
798
|
+
lifetimeMs: snapshotLifetimeMs,
|
|
640
799
|
windowEnd: value.window_end,
|
|
641
800
|
correlationId,
|
|
642
801
|
};
|
|
643
802
|
}
|
|
644
803
|
|
|
645
|
-
function mcpStructuredValue(response: McpResult): unknown {
|
|
804
|
+
function mcpStructuredValue(response: McpResult, maxTextBytes = 8 * 1024): unknown {
|
|
646
805
|
if (response.structuredContent !== undefined) return response.structuredContent;
|
|
647
806
|
const text = response.content?.find(
|
|
648
807
|
(block): block is { readonly type: string; readonly text: string } =>
|
|
649
808
|
block.type === 'text' && typeof block.text === 'string',
|
|
650
809
|
)?.text;
|
|
651
|
-
if (text === undefined || byteLength(text) >
|
|
810
|
+
if (text === undefined || byteLength(text) > maxTextBytes) return undefined;
|
|
652
811
|
try {
|
|
653
812
|
return JSON.parse(text);
|
|
654
813
|
} catch {
|
|
@@ -656,8 +815,14 @@ function mcpStructuredValue(response: McpResult): unknown {
|
|
|
656
815
|
}
|
|
657
816
|
}
|
|
658
817
|
|
|
659
|
-
async function readContextBudget(
|
|
818
|
+
export async function readContextBudget(
|
|
819
|
+
api: HookApi,
|
|
820
|
+
server: string,
|
|
821
|
+
now: MonotonicClock = monotonicNow,
|
|
822
|
+
): Promise<ContextBudgetRead> {
|
|
660
823
|
try {
|
|
824
|
+
const requestStartedAt = now();
|
|
825
|
+
if (requestStartedAt === undefined) return { ok: false, kind: 'provider_unavailable' };
|
|
661
826
|
const call = api.mcp.call(server, 'brain_context_budget', { schema_version: 1 });
|
|
662
827
|
const hostTimers = globalThis as unknown as {
|
|
663
828
|
setTimeout?: (callback: () => void, delay: number) => unknown;
|
|
@@ -671,8 +836,12 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
671
836
|
resolve(undefined);
|
|
672
837
|
}, BUDGET_PREFLIGHT_TIMEOUT_MS);
|
|
673
838
|
});
|
|
674
|
-
|
|
675
|
-
|
|
839
|
+
let response: McpResult | undefined;
|
|
840
|
+
try {
|
|
841
|
+
response = await Promise.race([call, timeout]);
|
|
842
|
+
} finally {
|
|
843
|
+
if (timer !== undefined) hostTimers.clearTimeout?.(timer);
|
|
844
|
+
}
|
|
676
845
|
if (response === undefined)
|
|
677
846
|
return { ok: false, kind: timedOut ? 'timeout' : 'invalid_response' };
|
|
678
847
|
if (response.isError === true) {
|
|
@@ -683,7 +852,15 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
683
852
|
error,
|
|
684
853
|
};
|
|
685
854
|
}
|
|
686
|
-
const
|
|
855
|
+
const responseReceivedAt = now();
|
|
856
|
+
if (responseReceivedAt === undefined) return { ok: false, kind: 'provider_unavailable' };
|
|
857
|
+
if (responseReceivedAt - requestStartedAt > BUDGET_PREFLIGHT_TIMEOUT_MS)
|
|
858
|
+
return { ok: false, kind: 'timeout' };
|
|
859
|
+
const snapshot = parseContextBudget(
|
|
860
|
+
mcpStructuredValue(response),
|
|
861
|
+
requestStartedAt,
|
|
862
|
+
responseReceivedAt,
|
|
863
|
+
);
|
|
687
864
|
return snapshot === undefined
|
|
688
865
|
? { ok: false, kind: 'invalid_response' }
|
|
689
866
|
: { ok: true, snapshot };
|
|
@@ -702,6 +879,25 @@ async function readContextBudget(api: HookApi, server: string): Promise<ContextB
|
|
|
702
879
|
}
|
|
703
880
|
}
|
|
704
881
|
|
|
882
|
+
export function contextBudgetIsFresh(
|
|
883
|
+
snapshot: ContextBudgetSnapshot,
|
|
884
|
+
now = monotonicNow(),
|
|
885
|
+
): boolean {
|
|
886
|
+
const elapsedMs = now === undefined ? Number.NaN : now - snapshot.requestStartedAtMonotonic;
|
|
887
|
+
return (
|
|
888
|
+
now !== undefined &&
|
|
889
|
+
Number.isFinite(now) &&
|
|
890
|
+
Number.isFinite(snapshot.requestStartedAtMonotonic) &&
|
|
891
|
+
Number.isSafeInteger(snapshot.lifetimeMs) &&
|
|
892
|
+
snapshot.requestStartedAtMonotonic >= 0 &&
|
|
893
|
+
snapshot.lifetimeMs > 0 &&
|
|
894
|
+
snapshot.lifetimeMs <= BUDGET_SNAPSHOT_MAX_AGE_MS &&
|
|
895
|
+
Number.isFinite(elapsedMs) &&
|
|
896
|
+
elapsedMs >= 0 &&
|
|
897
|
+
elapsedMs < snapshot.lifetimeMs
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
705
901
|
function safeRpcStatus(error: unknown): number | undefined {
|
|
706
902
|
const record = isRecord(error) ? error : {};
|
|
707
903
|
const candidate = record.rpc_status ?? record.status ?? record.code;
|
|
@@ -809,6 +1005,7 @@ const CHECKPOINT_ERRORS: readonly CheckpointError[] = [
|
|
|
809
1005
|
type CompactErrorCode =
|
|
810
1006
|
| 'state_budget_exceeded'
|
|
811
1007
|
| 'shared_admission_exceeded'
|
|
1008
|
+
| 'budget_response_invalid'
|
|
812
1009
|
| 'provider_budget_exceeded'
|
|
813
1010
|
| 'provider_unavailable'
|
|
814
1011
|
| 'provider_auth_denied'
|
|
@@ -823,6 +1020,7 @@ type CompactErrorCode =
|
|
|
823
1020
|
const COMPACT_ERROR_CODES: readonly CompactErrorCode[] = [
|
|
824
1021
|
'state_budget_exceeded',
|
|
825
1022
|
'shared_admission_exceeded',
|
|
1023
|
+
'budget_response_invalid',
|
|
826
1024
|
'provider_budget_exceeded',
|
|
827
1025
|
'provider_unavailable',
|
|
828
1026
|
'provider_auth_denied',
|
|
@@ -852,6 +1050,8 @@ type ObservationRecord = {
|
|
|
852
1050
|
| 'fallback'
|
|
853
1051
|
| 'compact_entry'
|
|
854
1052
|
| 'compact_exit'
|
|
1053
|
+
| 'native_summary_proof'
|
|
1054
|
+
| 'native_fallback_proof'
|
|
855
1055
|
| 'native_error';
|
|
856
1056
|
readonly category:
|
|
857
1057
|
| 'observed'
|
|
@@ -886,11 +1086,21 @@ type ObservationRecord = {
|
|
|
886
1086
|
readonly selected_count?: number;
|
|
887
1087
|
readonly deferred_count?: number;
|
|
888
1088
|
readonly rpc_status?: number;
|
|
1089
|
+
readonly provider_response_format?: 'structured' | 'text' | 'both' | 'missing';
|
|
889
1090
|
readonly admission_detail?: string;
|
|
890
1091
|
readonly admission_reason?: string;
|
|
891
1092
|
readonly request_id?: string;
|
|
892
1093
|
readonly correlation_id?: string;
|
|
893
1094
|
readonly retry_after_ms?: number;
|
|
1095
|
+
readonly proof_expected_count?: number;
|
|
1096
|
+
readonly proof_actual_count?: number;
|
|
1097
|
+
readonly proof_match?: 'exact' | 'active_suffix' | 'mismatch' | 'ambiguous';
|
|
1098
|
+
readonly proof_match_offset?: number;
|
|
1099
|
+
readonly proof_view_source?: 'default' | 'api';
|
|
1100
|
+
readonly proof_api_view_match?: 'exact' | 'mismatch' | 'unavailable' | 'not_checked';
|
|
1101
|
+
readonly proof_api_count?: number;
|
|
1102
|
+
readonly proof_delivery_source?: 'atomic_result' | 'legacy_append';
|
|
1103
|
+
readonly proof_delivery_match_count?: number;
|
|
894
1104
|
readonly reason?:
|
|
895
1105
|
| 'all_recent'
|
|
896
1106
|
| 'goal_missing'
|
|
@@ -905,7 +1115,14 @@ type ObservationRecord = {
|
|
|
905
1115
|
| 'provider_not_attempted'
|
|
906
1116
|
| 'proof_expired'
|
|
907
1117
|
| 'proof_mismatch'
|
|
908
|
-
| 'native_fallback_unconfirmed'
|
|
1118
|
+
| 'native_fallback_unconfirmed'
|
|
1119
|
+
| 'native_summary_proof_expired'
|
|
1120
|
+
| 'native_summary_host_view_mismatch'
|
|
1121
|
+
| 'native_summary_host_view_ambiguous'
|
|
1122
|
+
| 'native_summary_host_view_unavailable'
|
|
1123
|
+
| 'native_summary_note_read_failed'
|
|
1124
|
+
| 'native_summary_attachment_uncertain'
|
|
1125
|
+
| PlanBindingFailureReason;
|
|
909
1126
|
};
|
|
910
1127
|
|
|
911
1128
|
function observationConfiguration(options: HookOptions): ObservationOptions {
|
|
@@ -1507,6 +1724,17 @@ function observationRecord(
|
|
|
1507
1724
|
| 'request_id'
|
|
1508
1725
|
| 'correlation_id'
|
|
1509
1726
|
| 'retry_after_ms'
|
|
1727
|
+
| 'provider_response_format'
|
|
1728
|
+
| 'proof_expected_count'
|
|
1729
|
+
| 'proof_actual_count'
|
|
1730
|
+
| 'proof_match'
|
|
1731
|
+
| 'proof_match_offset'
|
|
1732
|
+
| 'proof_view_source'
|
|
1733
|
+
| 'proof_api_view_match'
|
|
1734
|
+
| 'proof_api_count'
|
|
1735
|
+
| 'proof_delivery_source'
|
|
1736
|
+
| 'proof_delivery_match_count'
|
|
1737
|
+
| 'reason'
|
|
1510
1738
|
> = {},
|
|
1511
1739
|
reason?: ObservationRecord['reason'],
|
|
1512
1740
|
mode: ObservationRecord['mode'] = 'compact',
|
|
@@ -1550,6 +1778,33 @@ function observationRecord(
|
|
|
1550
1778
|
...(fields.request_id === undefined ? {} : { request_id: fields.request_id }),
|
|
1551
1779
|
...(fields.correlation_id === undefined ? {} : { correlation_id: fields.correlation_id }),
|
|
1552
1780
|
...(fields.retry_after_ms === undefined ? {} : { retry_after_ms: fields.retry_after_ms }),
|
|
1781
|
+
...(fields.provider_response_format === undefined
|
|
1782
|
+
? {}
|
|
1783
|
+
: { provider_response_format: fields.provider_response_format }),
|
|
1784
|
+
...(fields.proof_expected_count === undefined
|
|
1785
|
+
? {}
|
|
1786
|
+
: { proof_expected_count: fields.proof_expected_count }),
|
|
1787
|
+
...(fields.proof_actual_count === undefined
|
|
1788
|
+
? {}
|
|
1789
|
+
: { proof_actual_count: fields.proof_actual_count }),
|
|
1790
|
+
...(fields.proof_match === undefined ? {} : { proof_match: fields.proof_match }),
|
|
1791
|
+
...(fields.proof_match_offset === undefined
|
|
1792
|
+
? {}
|
|
1793
|
+
: { proof_match_offset: fields.proof_match_offset }),
|
|
1794
|
+
...(fields.proof_view_source === undefined
|
|
1795
|
+
? {}
|
|
1796
|
+
: { proof_view_source: fields.proof_view_source }),
|
|
1797
|
+
...(fields.proof_api_view_match === undefined
|
|
1798
|
+
? {}
|
|
1799
|
+
: { proof_api_view_match: fields.proof_api_view_match }),
|
|
1800
|
+
...(fields.proof_api_count === undefined ? {} : { proof_api_count: fields.proof_api_count }),
|
|
1801
|
+
...(fields.proof_delivery_source === undefined
|
|
1802
|
+
? {}
|
|
1803
|
+
: { proof_delivery_source: fields.proof_delivery_source }),
|
|
1804
|
+
...(fields.proof_delivery_match_count === undefined
|
|
1805
|
+
? {}
|
|
1806
|
+
: { proof_delivery_match_count: fields.proof_delivery_match_count }),
|
|
1807
|
+
...(fields.reason === undefined ? {} : { reason: fields.reason }),
|
|
1553
1808
|
...(reason === undefined ? {} : { reason }),
|
|
1554
1809
|
};
|
|
1555
1810
|
}
|
|
@@ -1585,7 +1840,19 @@ async function flushProofDiagnostic(
|
|
|
1585
1840
|
sessionId: string,
|
|
1586
1841
|
instanceId: string,
|
|
1587
1842
|
clientVersion: string,
|
|
1588
|
-
reason: '
|
|
1843
|
+
reason: NonNullable<ObservationRecord['reason']>,
|
|
1844
|
+
fields: Pick<
|
|
1845
|
+
ObservationRecord,
|
|
1846
|
+
| 'proof_expected_count'
|
|
1847
|
+
| 'proof_actual_count'
|
|
1848
|
+
| 'proof_match'
|
|
1849
|
+
| 'proof_match_offset'
|
|
1850
|
+
| 'proof_view_source'
|
|
1851
|
+
| 'proof_api_view_match'
|
|
1852
|
+
| 'proof_api_count'
|
|
1853
|
+
| 'proof_delivery_source'
|
|
1854
|
+
| 'proof_delivery_match_count'
|
|
1855
|
+
> = {},
|
|
1589
1856
|
): Promise<void> {
|
|
1590
1857
|
if (!options.diagnosticsEnabled) return;
|
|
1591
1858
|
await flushObservationDiagnostics(api, options, operationId, sessionId, instanceId, [
|
|
@@ -1595,12 +1862,76 @@ async function flushProofDiagnostic(
|
|
|
1595
1862
|
'native_error',
|
|
1596
1863
|
clientVersion,
|
|
1597
1864
|
undefined,
|
|
1598
|
-
|
|
1865
|
+
fields,
|
|
1599
1866
|
reason,
|
|
1600
1867
|
),
|
|
1601
1868
|
]);
|
|
1602
1869
|
}
|
|
1603
1870
|
|
|
1871
|
+
async function flushNativeFallbackProofDiagnostic(
|
|
1872
|
+
api: HookApi,
|
|
1873
|
+
options: ObservationOptions,
|
|
1874
|
+
operationId: string,
|
|
1875
|
+
sessionId: string,
|
|
1876
|
+
instanceId: string,
|
|
1877
|
+
clientVersion: string,
|
|
1878
|
+
fields: Pick<
|
|
1879
|
+
ObservationRecord,
|
|
1880
|
+
| 'proof_expected_count'
|
|
1881
|
+
| 'proof_actual_count'
|
|
1882
|
+
| 'proof_match'
|
|
1883
|
+
| 'proof_match_offset'
|
|
1884
|
+
| 'proof_view_source'
|
|
1885
|
+
| 'proof_api_view_match'
|
|
1886
|
+
| 'proof_api_count'
|
|
1887
|
+
>,
|
|
1888
|
+
): Promise<void> {
|
|
1889
|
+
if (!options.diagnosticsEnabled) return;
|
|
1890
|
+
await flushObservationDiagnostics(api, options, operationId, sessionId, instanceId, [
|
|
1891
|
+
observationRecord(
|
|
1892
|
+
operationId,
|
|
1893
|
+
'native_fallback_proof',
|
|
1894
|
+
'observed',
|
|
1895
|
+
clientVersion,
|
|
1896
|
+
undefined,
|
|
1897
|
+
fields,
|
|
1898
|
+
),
|
|
1899
|
+
]);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
async function flushNativeSummaryProofDiagnostic(
|
|
1903
|
+
api: HookApi,
|
|
1904
|
+
options: ObservationOptions,
|
|
1905
|
+
operationId: string,
|
|
1906
|
+
sessionId: string,
|
|
1907
|
+
instanceId: string,
|
|
1908
|
+
clientVersion: string,
|
|
1909
|
+
fields: Pick<
|
|
1910
|
+
ObservationRecord,
|
|
1911
|
+
| 'proof_expected_count'
|
|
1912
|
+
| 'proof_actual_count'
|
|
1913
|
+
| 'proof_match'
|
|
1914
|
+
| 'proof_match_offset'
|
|
1915
|
+
| 'proof_view_source'
|
|
1916
|
+
| 'proof_api_view_match'
|
|
1917
|
+
| 'proof_api_count'
|
|
1918
|
+
| 'proof_delivery_source'
|
|
1919
|
+
| 'proof_delivery_match_count'
|
|
1920
|
+
>,
|
|
1921
|
+
): Promise<void> {
|
|
1922
|
+
if (!options.diagnosticsEnabled) return;
|
|
1923
|
+
await flushObservationDiagnostics(api, options, operationId, sessionId, instanceId, [
|
|
1924
|
+
observationRecord(
|
|
1925
|
+
operationId,
|
|
1926
|
+
'native_summary_proof',
|
|
1927
|
+
'observed',
|
|
1928
|
+
clientVersion,
|
|
1929
|
+
undefined,
|
|
1930
|
+
fields,
|
|
1931
|
+
),
|
|
1932
|
+
]);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1604
1935
|
function logDiagnostic(
|
|
1605
1936
|
api: HookApi,
|
|
1606
1937
|
hook: 'turn.complete' | 'session.compact',
|
|
@@ -1681,11 +2012,11 @@ function visibleSnapshotsMatch(
|
|
|
1681
2012
|
rereadMessages: readonly ClaudeMessage[],
|
|
1682
2013
|
trigger: HookEvent['trigger'],
|
|
1683
2014
|
instructions: string | undefined,
|
|
1684
|
-
):
|
|
1685
|
-
if (eventMessages.length === 0) return rereadMessages.length === 0;
|
|
2015
|
+
): number | undefined {
|
|
2016
|
+
if (eventMessages.length === 0) return rereadMessages.length === 0 ? 0 : undefined;
|
|
1686
2017
|
const eventSignatures = visibleSignatures(eventMessages);
|
|
1687
2018
|
const rereadSignatures = visibleSignatures(rereadMessages);
|
|
1688
|
-
if (sameSequence(eventSignatures, rereadSignatures)) return
|
|
2019
|
+
if (sameSequence(eventSignatures, rereadSignatures)) return 0;
|
|
1689
2020
|
if (
|
|
1690
2021
|
trigger === 'manual' &&
|
|
1691
2022
|
instructions === undefined &&
|
|
@@ -1717,10 +2048,10 @@ function visibleSnapshotsMatch(
|
|
|
1717
2048
|
matches.length === 1 &&
|
|
1718
2049
|
matches[0] === precedingSignatures.length - eventSignatures.length
|
|
1719
2050
|
)
|
|
1720
|
-
return
|
|
2051
|
+
return matches[0];
|
|
1721
2052
|
}
|
|
1722
2053
|
}
|
|
1723
|
-
if (rereadMessages.length < eventMessages.length) return
|
|
2054
|
+
if (rereadMessages.length < eventMessages.length) return undefined;
|
|
1724
2055
|
const eventShape = eventSignatures;
|
|
1725
2056
|
const matches: number[] = [];
|
|
1726
2057
|
for (let index = 0; index <= rereadSignatures.length - eventSignatures.length; index++) {
|
|
@@ -1728,7 +2059,9 @@ function visibleSnapshotsMatch(
|
|
|
1728
2059
|
matches.push(index);
|
|
1729
2060
|
if (matches.length > 1) break;
|
|
1730
2061
|
}
|
|
1731
|
-
return matches.length === 1 && matches[0] === rereadMessages.length - eventMessages.length
|
|
2062
|
+
return matches.length === 1 && matches[0] === rereadMessages.length - eventMessages.length
|
|
2063
|
+
? matches[0]
|
|
2064
|
+
: undefined;
|
|
1732
2065
|
}
|
|
1733
2066
|
|
|
1734
2067
|
function diagnosticShape(messages: readonly ClaudeMessage[]): Record<string, unknown> {
|
|
@@ -2330,6 +2663,10 @@ type NativeSummaryPending = {
|
|
|
2330
2663
|
readonly noteSha256: string;
|
|
2331
2664
|
readonly expectedPrefixHash: string;
|
|
2332
2665
|
readonly expectedCount: number;
|
|
2666
|
+
readonly expectedMessageHashes?: readonly string[];
|
|
2667
|
+
readonly deliveryMode?: 'atomic_result' | 'legacy_append';
|
|
2668
|
+
readonly anchorCounts?: readonly number[];
|
|
2669
|
+
readonly anchorHashes?: readonly string[];
|
|
2333
2670
|
readonly expiresAt: string;
|
|
2334
2671
|
readonly appendAttempted: boolean;
|
|
2335
2672
|
readonly nativeCompletedReported: boolean;
|
|
@@ -2577,6 +2914,10 @@ function structurallyValidNativeSummaryPending(
|
|
|
2577
2914
|
'noteSha256',
|
|
2578
2915
|
'expectedPrefixHash',
|
|
2579
2916
|
'expectedCount',
|
|
2917
|
+
'expectedMessageHashes',
|
|
2918
|
+
'deliveryMode',
|
|
2919
|
+
'anchorCounts',
|
|
2920
|
+
'anchorHashes',
|
|
2580
2921
|
'expiresAt',
|
|
2581
2922
|
'appendAttempted',
|
|
2582
2923
|
'nativeCompletedReported',
|
|
@@ -2603,6 +2944,28 @@ function structurallyValidNativeSummaryPending(
|
|
|
2603
2944
|
Number.isSafeInteger(value.expectedCount) &&
|
|
2604
2945
|
(value.expectedCount as number) > 0 &&
|
|
2605
2946
|
(value.expectedCount as number) <= 10_000 &&
|
|
2947
|
+
(value.expectedMessageHashes === undefined ||
|
|
2948
|
+
(Array.isArray(value.expectedMessageHashes) &&
|
|
2949
|
+
value.expectedMessageHashes.length === value.expectedCount &&
|
|
2950
|
+
value.expectedMessageHashes.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES &&
|
|
2951
|
+
value.expectedMessageHashes.every(
|
|
2952
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{16}$/u.test(hash),
|
|
2953
|
+
))) &&
|
|
2954
|
+
(value.deliveryMode === undefined ||
|
|
2955
|
+
value.deliveryMode === 'atomic_result' ||
|
|
2956
|
+
value.deliveryMode === 'legacy_append') &&
|
|
2957
|
+
((value.anchorCounts === undefined && value.anchorHashes === undefined) ||
|
|
2958
|
+
(Array.isArray(value.anchorCounts) &&
|
|
2959
|
+
Array.isArray(value.anchorHashes) &&
|
|
2960
|
+
value.anchorCounts.length === value.anchorHashes.length &&
|
|
2961
|
+
value.anchorCounts.length >= 1 &&
|
|
2962
|
+
value.anchorCounts.length <= 3 &&
|
|
2963
|
+
value.anchorCounts.every(
|
|
2964
|
+
(count): count is number => Number.isSafeInteger(count) && count >= 0 && count <= 10_000,
|
|
2965
|
+
) &&
|
|
2966
|
+
value.anchorHashes.every(
|
|
2967
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{64}$/u.test(hash),
|
|
2968
|
+
))) &&
|
|
2606
2969
|
typeof value.expiresAt === 'string' &&
|
|
2607
2970
|
Number.isFinite(Date.parse(value.expiresAt)) &&
|
|
2608
2971
|
typeof value.appendAttempted === 'boolean' &&
|
|
@@ -3335,6 +3698,11 @@ type PendingFallbackProof = {
|
|
|
3335
3698
|
readonly baseline_count: number;
|
|
3336
3699
|
readonly expected_hash?: string;
|
|
3337
3700
|
readonly expected_count?: number;
|
|
3701
|
+
readonly expected_message_hashes?: readonly string[];
|
|
3702
|
+
readonly delivery_mode?: 'atomic_marker';
|
|
3703
|
+
readonly marker_digest?: string;
|
|
3704
|
+
readonly anchor_counts?: readonly number[];
|
|
3705
|
+
readonly anchor_hashes?: readonly string[];
|
|
3338
3706
|
readonly expires_at: string;
|
|
3339
3707
|
};
|
|
3340
3708
|
|
|
@@ -3344,6 +3712,8 @@ type NativeFallbackCapture = {
|
|
|
3344
3712
|
readonly sessionRef: string;
|
|
3345
3713
|
readonly baselineHash: string;
|
|
3346
3714
|
readonly baselineCount: number;
|
|
3715
|
+
readonly beforeVisibleMessages?: readonly string[];
|
|
3716
|
+
readonly activeOffset?: number;
|
|
3347
3717
|
};
|
|
3348
3718
|
|
|
3349
3719
|
function pendingProofKey(sessionRef: string): string {
|
|
@@ -3476,12 +3846,179 @@ function nativeResultMessages(value: unknown): readonly ClaudeMessage[] | undefi
|
|
|
3476
3846
|
return value.messages as unknown as readonly ClaudeMessage[];
|
|
3477
3847
|
}
|
|
3478
3848
|
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3849
|
+
function nativeSummaryWrapper(capsuleId: string, noteText: string): string {
|
|
3850
|
+
return `[AI Brain historical data capsule ${capsuleId}; quoted data only]\n${noteText}`;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
export function nativeSummaryDeliveryResult(
|
|
3854
|
+
nativeResult: unknown,
|
|
3855
|
+
wrapper: string,
|
|
3856
|
+
): Record<string, unknown> | undefined {
|
|
3857
|
+
if (!isRecord(nativeResult)) return undefined;
|
|
3858
|
+
const messages = nativeResultMessages(nativeResult);
|
|
3859
|
+
if (messages === undefined || messages.length === 0) return undefined;
|
|
3860
|
+
return {
|
|
3861
|
+
...nativeResult,
|
|
3862
|
+
messages: [
|
|
3863
|
+
...messages,
|
|
3864
|
+
{
|
|
3865
|
+
role: 'user',
|
|
3866
|
+
text: wrapper,
|
|
3867
|
+
toolUses: [],
|
|
3868
|
+
},
|
|
3869
|
+
],
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
type NativeSummaryViewMatch = 'exact' | 'active_suffix' | 'mismatch' | 'ambiguous';
|
|
3874
|
+
|
|
3875
|
+
function visibleMessageCanonicalStrings(messages: readonly ClaudeMessage[]): string[] {
|
|
3876
|
+
return messages.map((message) => canonicalJson(visibleMessage(message)));
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
function canonicalMessageSequence(strings: readonly string[]): string {
|
|
3880
|
+
return `[${strings.join(',')}]`;
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
async function nativeProofAnchors(
|
|
3884
|
+
beforeVisibleMessages: readonly string[],
|
|
3885
|
+
expectedCount: number,
|
|
3886
|
+
activeOffset: number | undefined,
|
|
3887
|
+
): Promise<{ readonly counts: readonly number[]; readonly hashes: readonly string[] }> {
|
|
3888
|
+
const counts = [beforeVisibleMessages.length];
|
|
3889
|
+
if (
|
|
3890
|
+
activeOffset !== undefined &&
|
|
3891
|
+
activeOffset >= 0 &&
|
|
3892
|
+
activeOffset < beforeVisibleMessages.length
|
|
3893
|
+
)
|
|
3894
|
+
counts.push(activeOffset);
|
|
3895
|
+
const replaceTailCount = beforeVisibleMessages.length - expectedCount;
|
|
3896
|
+
if (replaceTailCount > 0 && replaceTailCount < beforeVisibleMessages.length)
|
|
3897
|
+
counts.push(replaceTailCount);
|
|
3898
|
+
const uniqueCounts = [...new Set(counts)].filter((count) => count <= 10_000);
|
|
3899
|
+
const hashes = await Promise.all(
|
|
3900
|
+
uniqueCounts.map((count) =>
|
|
3901
|
+
sha256(canonicalMessageSequence(beforeVisibleMessages.slice(0, count))),
|
|
3902
|
+
),
|
|
3903
|
+
);
|
|
3904
|
+
return { counts: uniqueCounts, hashes };
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
/**
|
|
3908
|
+
* Claude keeps the retired transcript prefix in session.messages() after a
|
|
3909
|
+
* native compaction, while the hook result describes the active summary
|
|
3910
|
+
* segment. Accept only the exact prefix or one uniquely matching active
|
|
3911
|
+
* suffix. A repeated match remains ambiguous and fails closed.
|
|
3912
|
+
*/
|
|
3913
|
+
export async function nativeSummaryViewMatch(
|
|
3914
|
+
actualMessages: readonly ClaudeMessage[],
|
|
3915
|
+
expectedCount: number,
|
|
3916
|
+
expectedHash: string,
|
|
3917
|
+
expectedMessageHashes: readonly string[] | undefined,
|
|
3918
|
+
anchorCounts: readonly number[] | undefined,
|
|
3919
|
+
anchorHashes: readonly string[] | undefined,
|
|
3920
|
+
): Promise<NativeSummaryViewMatch> {
|
|
3921
|
+
if (expectedCount === 0 || actualMessages.length < expectedCount) return 'mismatch';
|
|
3922
|
+
const matchingStarts: number[] = [];
|
|
3923
|
+
if (expectedMessageHashes === undefined) {
|
|
3924
|
+
const prefixHash = await sha256(
|
|
3925
|
+
canonicalJson(visibleMessages(actualMessages.slice(0, expectedCount))),
|
|
3926
|
+
);
|
|
3927
|
+
return prefixHash === expectedHash ? 'exact' : 'mismatch';
|
|
3928
|
+
}
|
|
3929
|
+
if (expectedMessageHashes.length !== expectedCount) return 'mismatch';
|
|
3930
|
+
const actualMessageHashes: string[] = [];
|
|
3931
|
+
for (const message of actualMessages)
|
|
3932
|
+
actualMessageHashes.push((await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16));
|
|
3933
|
+
for (let index = 0; index <= actualMessages.length - expectedCount; index += 1) {
|
|
3934
|
+
if (
|
|
3935
|
+
expectedMessageHashes.every((hash, offset) => hash === actualMessageHashes[index + offset])
|
|
3936
|
+
) {
|
|
3937
|
+
const candidateHash = await sha256(
|
|
3938
|
+
canonicalJson(visibleMessages(actualMessages.slice(index, index + expectedCount))),
|
|
3939
|
+
);
|
|
3940
|
+
if (candidateHash === expectedHash) matchingStarts.push(index);
|
|
3941
|
+
}
|
|
3942
|
+
if (matchingStarts.length > 1) return 'ambiguous';
|
|
3943
|
+
}
|
|
3944
|
+
if (matchingStarts.length !== 1) return 'mismatch';
|
|
3945
|
+
const start = matchingStarts[0] ?? -1;
|
|
3946
|
+
if (start === 0 && actualMessages.length === expectedCount) return 'exact';
|
|
3947
|
+
if (start === 0) return 'exact';
|
|
3948
|
+
if (await pendingAnchorMatches(actualMessages, start, anchorCounts, anchorHashes))
|
|
3949
|
+
return 'active_suffix';
|
|
3950
|
+
return 'mismatch';
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
async function pendingAnchorMatches(
|
|
3954
|
+
actualMessages: readonly ClaudeMessage[],
|
|
3955
|
+
actualOffset: number,
|
|
3956
|
+
anchorCounts: readonly number[] | undefined,
|
|
3957
|
+
anchorHashes: readonly string[] | undefined,
|
|
3958
|
+
): Promise<boolean> {
|
|
3959
|
+
if (
|
|
3960
|
+
anchorCounts === undefined ||
|
|
3961
|
+
anchorHashes === undefined ||
|
|
3962
|
+
anchorCounts.length !== anchorHashes.length
|
|
3963
|
+
)
|
|
3964
|
+
return false;
|
|
3965
|
+
const anchorIndex = anchorCounts.indexOf(actualOffset);
|
|
3966
|
+
if (anchorIndex < 0) return false;
|
|
3967
|
+
return (
|
|
3968
|
+
(await sha256(canonicalJson(visibleMessages(actualMessages.slice(0, actualOffset))))) ===
|
|
3969
|
+
anchorHashes[anchorIndex]
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
async function nativeSummaryMatchedOffset(
|
|
3974
|
+
actualMessages: readonly ClaudeMessage[],
|
|
3975
|
+
expectedCount: number,
|
|
3976
|
+
expectedHash: string,
|
|
3977
|
+
expectedMessageHashes: readonly string[] | undefined,
|
|
3978
|
+
anchorCounts: readonly number[] | undefined,
|
|
3979
|
+
anchorHashes: readonly string[] | undefined,
|
|
3980
|
+
): Promise<number | undefined> {
|
|
3981
|
+
if (
|
|
3982
|
+
expectedMessageHashes === undefined ||
|
|
3983
|
+
anchorCounts === undefined ||
|
|
3984
|
+
anchorHashes === undefined
|
|
3985
|
+
)
|
|
3986
|
+
return undefined;
|
|
3987
|
+
const actualMessageHashes = await Promise.all(
|
|
3988
|
+
actualMessages.map(async (message) =>
|
|
3989
|
+
(await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16),
|
|
3990
|
+
),
|
|
3991
|
+
);
|
|
3992
|
+
const matches: number[] = [];
|
|
3993
|
+
for (const offset of anchorCounts) {
|
|
3994
|
+
if (offset + expectedCount > actualMessages.length) continue;
|
|
3995
|
+
if (!expectedMessageHashes.every((hash, index) => hash === actualMessageHashes[offset + index]))
|
|
3996
|
+
continue;
|
|
3997
|
+
if (
|
|
3998
|
+
(await sha256(
|
|
3999
|
+
canonicalJson(visibleMessages(actualMessages.slice(offset, offset + expectedCount))),
|
|
4000
|
+
)) !== expectedHash
|
|
4001
|
+
)
|
|
4002
|
+
continue;
|
|
4003
|
+
if (!(await pendingAnchorMatches(actualMessages, offset, anchorCounts, anchorHashes))) continue;
|
|
4004
|
+
matches.push(offset);
|
|
4005
|
+
}
|
|
4006
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
function exactTextCount(
|
|
4010
|
+
messages: readonly { readonly role: 'user' | 'assistant'; readonly text: string }[],
|
|
4011
|
+
wrapper: string,
|
|
4012
|
+
): number {
|
|
4013
|
+
return messages.filter(
|
|
3482
4014
|
(message) =>
|
|
3483
4015
|
message.role === 'user' && (message.text === wrapper || message.text === `${wrapper}\n`),
|
|
3484
|
-
);
|
|
4016
|
+
).length;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
async function apiViewExactTextCount(api: HookApi, wrapper: string): Promise<number> {
|
|
4020
|
+
const messages = await boundedApiMessages(api);
|
|
4021
|
+
return exactTextCount(messages, wrapper);
|
|
3485
4022
|
}
|
|
3486
4023
|
|
|
3487
4024
|
async function boundedApiMessages(
|
|
@@ -3704,6 +4241,11 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
|
|
|
3704
4241
|
'baseline_count',
|
|
3705
4242
|
'expected_hash',
|
|
3706
4243
|
'expected_count',
|
|
4244
|
+
'expected_message_hashes',
|
|
4245
|
+
'delivery_mode',
|
|
4246
|
+
'marker_digest',
|
|
4247
|
+
'anchor_counts',
|
|
4248
|
+
'anchor_hashes',
|
|
3707
4249
|
'expires_at',
|
|
3708
4250
|
]);
|
|
3709
4251
|
if (Object.keys(value).some((key) => !allowedRootKeys.has(key))) return false;
|
|
@@ -3724,7 +4266,31 @@ function structurallyValidPendingProof(value: unknown): value is PendingProof {
|
|
|
3724
4266
|
(value.baseline_count as number) >= 0 &&
|
|
3725
4267
|
(value.expected_hash === undefined || /^[a-f0-9]{64}$/u.test(String(value.expected_hash))) &&
|
|
3726
4268
|
(value.expected_count === undefined ||
|
|
3727
|
-
(Number.isSafeInteger(value.expected_count) && (value.expected_count as number) >= 0))
|
|
4269
|
+
(Number.isSafeInteger(value.expected_count) && (value.expected_count as number) >= 0)) &&
|
|
4270
|
+
(value.expected_message_hashes === undefined ||
|
|
4271
|
+
(Array.isArray(value.expected_message_hashes) &&
|
|
4272
|
+
value.expected_message_hashes.length === value.expected_count &&
|
|
4273
|
+
value.expected_message_hashes.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES &&
|
|
4274
|
+
value.expected_message_hashes.every(
|
|
4275
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{16}$/u.test(hash),
|
|
4276
|
+
))) &&
|
|
4277
|
+
(value.delivery_mode === undefined || value.delivery_mode === 'atomic_marker') &&
|
|
4278
|
+
(value.marker_digest === undefined || /^[a-f0-9]{64}$/u.test(String(value.marker_digest))) &&
|
|
4279
|
+
((value.delivery_mode === undefined && value.marker_digest === undefined) ||
|
|
4280
|
+
(value.delivery_mode === 'atomic_marker' && typeof value.marker_digest === 'string')) &&
|
|
4281
|
+
((value.anchor_counts === undefined && value.anchor_hashes === undefined) ||
|
|
4282
|
+
(Array.isArray(value.anchor_counts) &&
|
|
4283
|
+
Array.isArray(value.anchor_hashes) &&
|
|
4284
|
+
value.anchor_counts.length === value.anchor_hashes.length &&
|
|
4285
|
+
value.anchor_counts.length >= 1 &&
|
|
4286
|
+
value.anchor_counts.length <= 3 &&
|
|
4287
|
+
value.anchor_counts.every(
|
|
4288
|
+
(count): count is number =>
|
|
4289
|
+
Number.isSafeInteger(count) && count >= 0 && count <= 10_000,
|
|
4290
|
+
) &&
|
|
4291
|
+
value.anchor_hashes.every(
|
|
4292
|
+
(hash): hash is string => typeof hash === 'string' && /^[a-f0-9]{64}$/u.test(hash),
|
|
4293
|
+
)))
|
|
3728
4294
|
);
|
|
3729
4295
|
if (
|
|
3730
4296
|
value.kind !== 'custom' ||
|
|
@@ -4354,6 +4920,26 @@ function getTelemetry(
|
|
|
4354
4920
|
return state.current;
|
|
4355
4921
|
}
|
|
4356
4922
|
|
|
4923
|
+
async function nativeFallbackMarker(
|
|
4924
|
+
operationId: string,
|
|
4925
|
+
sessionRef: string,
|
|
4926
|
+
baselineHash: string,
|
|
4927
|
+
): Promise<{ readonly text: string; readonly digest: string }> {
|
|
4928
|
+
const digest = await sha256(
|
|
4929
|
+
`aibrain-native-fallback-v1|${operationId}|${sessionRef}|${baselineHash}`,
|
|
4930
|
+
);
|
|
4931
|
+
return { digest, text: `[AI Brain native fallback receipt v1:${digest}]` };
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
function nativeFallbackDeliveryResult(nativeResult: unknown, marker: string): unknown {
|
|
4935
|
+
const messages = nativeResultMessages(nativeResult);
|
|
4936
|
+
if (messages === undefined || messages.length === 0) return nativeResult;
|
|
4937
|
+
return {
|
|
4938
|
+
...(nativeResult as Record<string, unknown>),
|
|
4939
|
+
messages: [...messages, { role: 'user', text: marker, toolUses: [] }],
|
|
4940
|
+
};
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4357
4943
|
async function queueNativeFallbackProof(
|
|
4358
4944
|
api: HookApi,
|
|
4359
4945
|
runtime: ProofRuntimeState,
|
|
@@ -4362,12 +4948,30 @@ async function queueNativeFallbackProof(
|
|
|
4362
4948
|
baselineHash: string,
|
|
4363
4949
|
baselineCount: number,
|
|
4364
4950
|
nativeResult: unknown,
|
|
4365
|
-
|
|
4951
|
+
beforeVisibleMessages: readonly string[] | undefined,
|
|
4952
|
+
activeOffset: number | undefined,
|
|
4953
|
+
): Promise<unknown> {
|
|
4366
4954
|
const expectedMessages = nativeResultMessages(nativeResult);
|
|
4367
4955
|
const expectedHash =
|
|
4368
4956
|
expectedMessages === undefined
|
|
4369
4957
|
? undefined
|
|
4370
4958
|
: await sha256(canonicalJson(visibleMessages(expectedMessages)));
|
|
4959
|
+
const expectedMessageHashes =
|
|
4960
|
+
expectedMessages !== undefined && expectedMessages.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES
|
|
4961
|
+
? await Promise.all(
|
|
4962
|
+
expectedMessages.map(async (message) =>
|
|
4963
|
+
(await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16),
|
|
4964
|
+
),
|
|
4965
|
+
)
|
|
4966
|
+
: undefined;
|
|
4967
|
+
const anchors =
|
|
4968
|
+
beforeVisibleMessages === undefined || expectedMessages === undefined
|
|
4969
|
+
? undefined
|
|
4970
|
+
: await nativeProofAnchors(beforeVisibleMessages, expectedMessages.length, activeOffset);
|
|
4971
|
+
const marker =
|
|
4972
|
+
expectedMessages === undefined
|
|
4973
|
+
? undefined
|
|
4974
|
+
: await nativeFallbackMarker(operationId, sessionRef, baselineHash);
|
|
4371
4975
|
const proof: PendingFallbackProof = {
|
|
4372
4976
|
schema_version: 1,
|
|
4373
4977
|
kind: 'native_fallback',
|
|
@@ -4378,10 +4982,25 @@ async function queueNativeFallbackProof(
|
|
|
4378
4982
|
...(expectedHash === undefined
|
|
4379
4983
|
? {}
|
|
4380
4984
|
: { expected_hash: expectedHash, expected_count: expectedMessages?.length ?? 0 }),
|
|
4381
|
-
|
|
4985
|
+
...(expectedMessageHashes === undefined
|
|
4986
|
+
? {}
|
|
4987
|
+
: { expected_message_hashes: expectedMessageHashes }),
|
|
4988
|
+
...(marker === undefined
|
|
4989
|
+
? {}
|
|
4990
|
+
: { delivery_mode: 'atomic_marker' as const, marker_digest: marker.digest }),
|
|
4991
|
+
...(anchors === undefined
|
|
4992
|
+
? {}
|
|
4993
|
+
: { anchor_counts: anchors.counts, anchor_hashes: anchors.hashes }),
|
|
4994
|
+
// Native fallback confirmation may occur after a long host compaction.
|
|
4995
|
+
// Keep its proof lifetime aligned with the durable proof bound instead of
|
|
4996
|
+
// expiring it after two minutes while Claude is still processing.
|
|
4997
|
+
expires_at: new Date(Date.now() + PENDING_PROOF_TTL_MS).toISOString(),
|
|
4382
4998
|
};
|
|
4383
4999
|
runtime.pendingProofMemory = proof;
|
|
4384
5000
|
runtime.pendingProofPersistenceFailed = !(await savePendingProof(api, proof));
|
|
5001
|
+
return runtime.pendingProofPersistenceFailed || marker === undefined
|
|
5002
|
+
? nativeResult
|
|
5003
|
+
: nativeFallbackDeliveryResult(nativeResult, marker.text);
|
|
4385
5004
|
}
|
|
4386
5005
|
|
|
4387
5006
|
async function confirmPendingProof(
|
|
@@ -4447,21 +5066,101 @@ async function confirmPendingProof(
|
|
|
4447
5066
|
return;
|
|
4448
5067
|
}
|
|
4449
5068
|
let verified = proof.kind === 'custom' ? await verifyCustomProof(proof, messages) : false;
|
|
5069
|
+
let fallbackMatch: NativeSummaryViewMatch | undefined;
|
|
5070
|
+
let fallbackProofFields: Pick<
|
|
5071
|
+
ObservationRecord,
|
|
5072
|
+
| 'proof_expected_count'
|
|
5073
|
+
| 'proof_actual_count'
|
|
5074
|
+
| 'proof_match'
|
|
5075
|
+
| 'proof_match_offset'
|
|
5076
|
+
| 'proof_view_source'
|
|
5077
|
+
| 'proof_api_view_match'
|
|
5078
|
+
| 'proof_api_count'
|
|
5079
|
+
> = {};
|
|
4450
5080
|
if (proof.kind === 'native_fallback') {
|
|
4451
5081
|
const expectedCount = proof.expected_count;
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
5082
|
+
fallbackProofFields = {
|
|
5083
|
+
...(expectedCount === undefined ? {} : { proof_expected_count: expectedCount }),
|
|
5084
|
+
proof_actual_count: messages.length,
|
|
5085
|
+
};
|
|
5086
|
+
if (proof.expected_hash !== undefined && expectedCount !== undefined) {
|
|
5087
|
+
if (proof.delivery_mode === 'atomic_marker' && proof.marker_digest !== undefined) {
|
|
5088
|
+
try {
|
|
5089
|
+
const apiView = await boundedApiMessages(api);
|
|
5090
|
+
const marker = await nativeFallbackMarker(
|
|
5091
|
+
proof.operation_id,
|
|
5092
|
+
sessionRef,
|
|
5093
|
+
proof.baseline_hash,
|
|
5094
|
+
);
|
|
5095
|
+
const markerValid = marker.digest === proof.marker_digest;
|
|
5096
|
+
const markerCount = exactTextCount(apiView, marker.text);
|
|
5097
|
+
fallbackProofFields = {
|
|
5098
|
+
...fallbackProofFields,
|
|
5099
|
+
proof_view_source: 'api',
|
|
5100
|
+
proof_api_view_match: markerValid && markerCount === 1 ? 'exact' : 'mismatch',
|
|
5101
|
+
proof_api_count: apiView.length,
|
|
5102
|
+
};
|
|
5103
|
+
if (markerValid && markerCount === 1) {
|
|
5104
|
+
fallbackMatch = 'exact';
|
|
5105
|
+
fallbackProofFields = {
|
|
5106
|
+
...fallbackProofFields,
|
|
5107
|
+
proof_match: 'exact',
|
|
5108
|
+
proof_match_offset: 0,
|
|
5109
|
+
};
|
|
5110
|
+
verified = true;
|
|
5111
|
+
}
|
|
5112
|
+
} catch {
|
|
5113
|
+
fallbackProofFields = {
|
|
5114
|
+
...fallbackProofFields,
|
|
5115
|
+
proof_view_source: 'api',
|
|
5116
|
+
proof_api_view_match: 'unavailable',
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
} else {
|
|
5120
|
+
// Compatibility with proofs written by beta15 and earlier. These
|
|
5121
|
+
// proofs predate the API-view fingerprint and retain the old strict
|
|
5122
|
+
// default-view check.
|
|
5123
|
+
fallbackMatch = await nativeSummaryViewMatch(
|
|
5124
|
+
messages,
|
|
5125
|
+
expectedCount,
|
|
5126
|
+
proof.expected_hash,
|
|
5127
|
+
proof.expected_message_hashes,
|
|
5128
|
+
proof.anchor_counts,
|
|
5129
|
+
proof.anchor_hashes,
|
|
5130
|
+
);
|
|
5131
|
+
const fallbackOffset =
|
|
5132
|
+
fallbackMatch === 'active_suffix'
|
|
5133
|
+
? await nativeSummaryMatchedOffset(
|
|
5134
|
+
messages,
|
|
5135
|
+
expectedCount,
|
|
5136
|
+
proof.expected_hash,
|
|
5137
|
+
proof.expected_message_hashes,
|
|
5138
|
+
proof.anchor_counts,
|
|
5139
|
+
proof.anchor_hashes,
|
|
5140
|
+
)
|
|
5141
|
+
: undefined;
|
|
5142
|
+
fallbackProofFields = {
|
|
5143
|
+
...fallbackProofFields,
|
|
5144
|
+
proof_view_source: 'default',
|
|
5145
|
+
proof_match: fallbackMatch,
|
|
5146
|
+
...(fallbackMatch === 'exact' ? { proof_match_offset: 0 } : {}),
|
|
5147
|
+
...(fallbackOffset === undefined ? {} : { proof_match_offset: fallbackOffset }),
|
|
5148
|
+
};
|
|
5149
|
+
verified = fallbackMatch === 'exact' || fallbackMatch === 'active_suffix';
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
4460
5152
|
}
|
|
4461
5153
|
if (runtime.pendingProofPersistenceFailed) verified = false;
|
|
4462
5154
|
const telemetry = getTelemetry(api, runtime.brainServer, runtime.options, runtime.telemetryState);
|
|
4463
5155
|
if (!verified) {
|
|
4464
|
-
|
|
5156
|
+
// Only the atomic native fallback marker may wait for the host's active
|
|
5157
|
+
// API view. A prepared JEV capsule remains strict at every callback.
|
|
5158
|
+
if (
|
|
5159
|
+
deferMismatch &&
|
|
5160
|
+
proof.kind === 'native_fallback' &&
|
|
5161
|
+
proof.delivery_mode === 'atomic_marker'
|
|
5162
|
+
)
|
|
5163
|
+
return;
|
|
4465
5164
|
runtime.pendingProofMemory = undefined;
|
|
4466
5165
|
runtime.pendingProofPersistenceFailed = false;
|
|
4467
5166
|
await clearPendingProof(api, sessionRef);
|
|
@@ -4474,6 +5173,7 @@ async function confirmPendingProof(
|
|
|
4474
5173
|
runtime.observationInstanceId,
|
|
4475
5174
|
nativeClientVersion(runtime.options),
|
|
4476
5175
|
reason,
|
|
5176
|
+
fallbackProofFields,
|
|
4477
5177
|
);
|
|
4478
5178
|
if (runtime.diagnosticMode)
|
|
4479
5179
|
api.ui.log(
|
|
@@ -4485,6 +5185,16 @@ async function confirmPendingProof(
|
|
|
4485
5185
|
runtime.pendingProofMemory = undefined;
|
|
4486
5186
|
runtime.pendingProofPersistenceFailed = false;
|
|
4487
5187
|
await clearPendingProof(api, sessionRef);
|
|
5188
|
+
if (proof.kind === 'native_fallback')
|
|
5189
|
+
await flushNativeFallbackProofDiagnostic(
|
|
5190
|
+
api,
|
|
5191
|
+
runtime.observation,
|
|
5192
|
+
proof.operation_id,
|
|
5193
|
+
sessionRef,
|
|
5194
|
+
runtime.observationInstanceId,
|
|
5195
|
+
nativeClientVersion(runtime.options),
|
|
5196
|
+
fallbackProofFields,
|
|
5197
|
+
);
|
|
4488
5198
|
if (proof.kind === 'custom') {
|
|
4489
5199
|
const dropped = proof.pairs.filter((pair) => pair.action === 'drop_pair').length;
|
|
4490
5200
|
const reduced = proof.pairs.filter((pair) => pair.action === 'truncate_result').length;
|
|
@@ -4517,9 +5227,43 @@ async function recoverNativeSummaryNote(
|
|
|
4517
5227
|
}
|
|
4518
5228
|
const pendingOperationId = pending.operationId;
|
|
4519
5229
|
const telemetry = getTelemetry(api, runtime.brainServer, runtime.options, runtime.telemetryState);
|
|
5230
|
+
let proofFields: Pick<
|
|
5231
|
+
ObservationRecord,
|
|
5232
|
+
| 'proof_expected_count'
|
|
5233
|
+
| 'proof_actual_count'
|
|
5234
|
+
| 'proof_match'
|
|
5235
|
+
| 'proof_match_offset'
|
|
5236
|
+
| 'proof_view_source'
|
|
5237
|
+
| 'proof_api_view_match'
|
|
5238
|
+
| 'proof_api_count'
|
|
5239
|
+
| 'proof_delivery_source'
|
|
5240
|
+
| 'proof_delivery_match_count'
|
|
5241
|
+
> = {
|
|
5242
|
+
proof_expected_count: pending.deliveryMode === 'atomic_result' ? 1 : pending.expectedCount,
|
|
5243
|
+
};
|
|
5244
|
+
proofFields = {
|
|
5245
|
+
...proofFields,
|
|
5246
|
+
proof_delivery_source: pending.deliveryMode ?? 'legacy_append',
|
|
5247
|
+
proof_view_source: pending.deliveryMode === 'atomic_result' ? 'api' : 'default',
|
|
5248
|
+
proof_api_view_match: 'not_checked',
|
|
5249
|
+
};
|
|
4520
5250
|
const uncertain = async (reason: string): Promise<void> => {
|
|
4521
5251
|
runtime.pendingNativeSummary = undefined;
|
|
4522
5252
|
await clearNativeSummaryPending(api, sessionRef).catch(() => undefined);
|
|
5253
|
+
const diagnosticReason: ObservationRecord['reason'] =
|
|
5254
|
+
reason === 'pending_expired'
|
|
5255
|
+
? 'native_summary_proof_expired'
|
|
5256
|
+
: reason === 'native_view_ambiguous'
|
|
5257
|
+
? 'native_summary_host_view_ambiguous'
|
|
5258
|
+
: reason === 'native_prefix_mismatch'
|
|
5259
|
+
? 'native_summary_host_view_mismatch'
|
|
5260
|
+
: reason === 'messages_unavailable' ||
|
|
5261
|
+
reason === 'api_view_unavailable' ||
|
|
5262
|
+
reason === 'api_view_reread_failed'
|
|
5263
|
+
? 'native_summary_host_view_unavailable'
|
|
5264
|
+
: reason === 'note_read_failed'
|
|
5265
|
+
? 'native_summary_note_read_failed'
|
|
5266
|
+
: 'native_summary_attachment_uncertain';
|
|
4523
5267
|
await flushProofDiagnostic(
|
|
4524
5268
|
api,
|
|
4525
5269
|
runtime.observation,
|
|
@@ -4527,7 +5271,8 @@ async function recoverNativeSummaryNote(
|
|
|
4527
5271
|
sessionRef,
|
|
4528
5272
|
runtime.observationInstanceId,
|
|
4529
5273
|
nativeClientVersion(runtime.options),
|
|
4530
|
-
|
|
5274
|
+
diagnosticReason,
|
|
5275
|
+
proofFields,
|
|
4531
5276
|
);
|
|
4532
5277
|
if (runtime.diagnosticMode)
|
|
4533
5278
|
api.ui.log(
|
|
@@ -4539,6 +5284,71 @@ async function recoverNativeSummaryNote(
|
|
|
4539
5284
|
await uncertain('pending_expired');
|
|
4540
5285
|
return;
|
|
4541
5286
|
}
|
|
5287
|
+
if (pending.deliveryMode === 'atomic_result') {
|
|
5288
|
+
let noteText: string;
|
|
5289
|
+
try {
|
|
5290
|
+
noteText = await readNativeSummaryNote(
|
|
5291
|
+
api,
|
|
5292
|
+
runtime.options,
|
|
5293
|
+
pending.operationId,
|
|
5294
|
+
sessionRef,
|
|
5295
|
+
pending.noteSha256,
|
|
5296
|
+
);
|
|
5297
|
+
} catch {
|
|
5298
|
+
await uncertain('note_read_failed');
|
|
5299
|
+
return;
|
|
5300
|
+
}
|
|
5301
|
+
const wrapper = nativeSummaryWrapper(pending.capsuleId, noteText);
|
|
5302
|
+
let apiCount: number;
|
|
5303
|
+
let matchCount: number;
|
|
5304
|
+
try {
|
|
5305
|
+
const apiView = await boundedApiMessages(api);
|
|
5306
|
+
apiCount = apiView.length;
|
|
5307
|
+
matchCount = exactTextCount(apiView, wrapper);
|
|
5308
|
+
} catch {
|
|
5309
|
+
proofFields = { ...proofFields, proof_api_view_match: 'unavailable' };
|
|
5310
|
+
if (!allowAppend) return;
|
|
5311
|
+
await uncertain('api_view_unavailable');
|
|
5312
|
+
return;
|
|
5313
|
+
}
|
|
5314
|
+
proofFields = {
|
|
5315
|
+
...proofFields,
|
|
5316
|
+
proof_api_count: apiCount,
|
|
5317
|
+
proof_delivery_match_count: matchCount,
|
|
5318
|
+
proof_api_view_match: matchCount === 1 ? 'exact' : 'mismatch',
|
|
5319
|
+
proof_match: matchCount === 1 ? 'exact' : 'mismatch',
|
|
5320
|
+
};
|
|
5321
|
+
if (matchCount !== 1) {
|
|
5322
|
+
if (matchCount === 0 && !allowAppend) return;
|
|
5323
|
+
await uncertain('native_summary_attachment_uncertain');
|
|
5324
|
+
return;
|
|
5325
|
+
}
|
|
5326
|
+
await flushNativeSummaryProofDiagnostic(
|
|
5327
|
+
api,
|
|
5328
|
+
runtime.observation,
|
|
5329
|
+
pendingOperationId,
|
|
5330
|
+
sessionRef,
|
|
5331
|
+
runtime.observationInstanceId,
|
|
5332
|
+
nativeClientVersion(runtime.options),
|
|
5333
|
+
proofFields,
|
|
5334
|
+
);
|
|
5335
|
+
if (!pending.nativeCompletedReported) {
|
|
5336
|
+
const marked: NativeSummaryPending = { ...pending, nativeCompletedReported: true };
|
|
5337
|
+
try {
|
|
5338
|
+
await saveNativeSummaryPending(api, marked);
|
|
5339
|
+
pending = marked;
|
|
5340
|
+
runtime.pendingNativeSummary = marked;
|
|
5341
|
+
} catch {
|
|
5342
|
+
runtime.pendingNativeSummary = marked;
|
|
5343
|
+
pending = marked;
|
|
5344
|
+
}
|
|
5345
|
+
telemetry.nativeSummaryCompleted(pending.operationId);
|
|
5346
|
+
}
|
|
5347
|
+
runtime.pendingNativeSummary = undefined;
|
|
5348
|
+
await clearNativeSummaryPending(api, sessionRef);
|
|
5349
|
+
telemetry.recoveryAttached(pending.operationId);
|
|
5350
|
+
return;
|
|
5351
|
+
}
|
|
4542
5352
|
let messages: ClaudeMessage[];
|
|
4543
5353
|
try {
|
|
4544
5354
|
messages = await api.session.messages();
|
|
@@ -4546,10 +5356,60 @@ async function recoverNativeSummaryNote(
|
|
|
4546
5356
|
await uncertain('messages_unavailable');
|
|
4547
5357
|
return;
|
|
4548
5358
|
}
|
|
4549
|
-
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
|
|
5359
|
+
proofFields = { ...proofFields, proof_actual_count: messages.length };
|
|
5360
|
+
const viewMatch = await nativeSummaryViewMatch(
|
|
5361
|
+
messages,
|
|
5362
|
+
pending.expectedCount,
|
|
5363
|
+
pending.expectedPrefixHash,
|
|
5364
|
+
pending.expectedMessageHashes,
|
|
5365
|
+
pending.anchorCounts,
|
|
5366
|
+
pending.anchorHashes,
|
|
5367
|
+
);
|
|
5368
|
+
const viewOffset =
|
|
5369
|
+
viewMatch === 'active_suffix'
|
|
5370
|
+
? await nativeSummaryMatchedOffset(
|
|
5371
|
+
messages,
|
|
5372
|
+
pending.expectedCount,
|
|
5373
|
+
pending.expectedPrefixHash,
|
|
5374
|
+
pending.expectedMessageHashes,
|
|
5375
|
+
pending.anchorCounts,
|
|
5376
|
+
pending.anchorHashes,
|
|
5377
|
+
)
|
|
5378
|
+
: undefined;
|
|
5379
|
+
proofFields = {
|
|
5380
|
+
...proofFields,
|
|
5381
|
+
proof_match: viewMatch,
|
|
5382
|
+
...(viewMatch === 'exact' ? { proof_match_offset: 0 } : {}),
|
|
5383
|
+
...(viewOffset === undefined ? {} : { proof_match_offset: viewOffset }),
|
|
5384
|
+
};
|
|
5385
|
+
await flushNativeSummaryProofDiagnostic(
|
|
5386
|
+
api,
|
|
5387
|
+
runtime.observation,
|
|
5388
|
+
pendingOperationId,
|
|
5389
|
+
sessionRef,
|
|
5390
|
+
runtime.observationInstanceId,
|
|
5391
|
+
nativeClientVersion(runtime.options),
|
|
5392
|
+
proofFields,
|
|
5393
|
+
);
|
|
5394
|
+
if (runtime.diagnosticMode) {
|
|
5395
|
+
const apiView = await boundedApiMessages(api).catch(() => undefined);
|
|
5396
|
+
api.ui.log(
|
|
5397
|
+
`AI Brain context diagnostic ${JSON.stringify({
|
|
5398
|
+
hook: 'prompt.submit',
|
|
5399
|
+
phase: 'native_summary_view_probe',
|
|
5400
|
+
expected_count: pending.expectedCount,
|
|
5401
|
+
expected_hash: pending.expectedPrefixHash,
|
|
5402
|
+
default_count: messages.length,
|
|
5403
|
+
default_hash: await sha256(canonicalJson(visibleMessages(messages))),
|
|
5404
|
+
api_count: apiView?.length ?? null,
|
|
5405
|
+
api_hash: apiView === undefined ? null : await sha256(canonicalJson(apiView)),
|
|
5406
|
+
anchor_counts: pending.anchorCounts ?? null,
|
|
5407
|
+
view_match: viewMatch,
|
|
5408
|
+
})}`,
|
|
5409
|
+
);
|
|
5410
|
+
}
|
|
5411
|
+
if (viewMatch === 'mismatch' || viewMatch === 'ambiguous') {
|
|
5412
|
+
await uncertain(viewMatch === 'ambiguous' ? 'native_view_ambiguous' : 'native_prefix_mismatch');
|
|
4553
5413
|
return;
|
|
4554
5414
|
}
|
|
4555
5415
|
// The native result is now proven at the current session prefix. The note
|
|
@@ -4579,10 +5439,12 @@ async function recoverNativeSummaryNote(
|
|
|
4579
5439
|
await uncertain('note_read_failed');
|
|
4580
5440
|
return;
|
|
4581
5441
|
}
|
|
4582
|
-
const wrapper =
|
|
5442
|
+
const wrapper = nativeSummaryWrapper(pending.capsuleId, noteText);
|
|
4583
5443
|
let hasNote: boolean;
|
|
4584
5444
|
try {
|
|
4585
|
-
|
|
5445
|
+
const matchCount = await apiViewExactTextCount(api, wrapper);
|
|
5446
|
+
proofFields = { ...proofFields, proof_delivery_match_count: matchCount };
|
|
5447
|
+
hasNote = matchCount === 1;
|
|
4586
5448
|
} catch {
|
|
4587
5449
|
if (!allowAppend && pending.appendAttempted) await uncertain('api_view_unavailable');
|
|
4588
5450
|
return;
|
|
@@ -4611,7 +5473,9 @@ async function recoverNativeSummaryNote(
|
|
|
4611
5473
|
}
|
|
4612
5474
|
let apiHasNoteAfter: boolean;
|
|
4613
5475
|
try {
|
|
4614
|
-
|
|
5476
|
+
const matchCount = await apiViewExactTextCount(api, wrapper);
|
|
5477
|
+
proofFields = { ...proofFields, proof_delivery_match_count: matchCount };
|
|
5478
|
+
apiHasNoteAfter = matchCount === 1;
|
|
4615
5479
|
} catch {
|
|
4616
5480
|
if (!allowAppend)
|
|
4617
5481
|
await uncertain(appendError ? 'append_ack_ambiguous' : 'api_view_reread_failed');
|
|
@@ -4625,6 +5489,15 @@ async function recoverNativeSummaryNote(
|
|
|
4625
5489
|
}
|
|
4626
5490
|
runtime.pendingNativeSummary = undefined;
|
|
4627
5491
|
await clearNativeSummaryPending(api, sessionRef);
|
|
5492
|
+
await flushNativeSummaryProofDiagnostic(
|
|
5493
|
+
api,
|
|
5494
|
+
runtime.observation,
|
|
5495
|
+
pending.operationId,
|
|
5496
|
+
sessionRef,
|
|
5497
|
+
runtime.observationInstanceId,
|
|
5498
|
+
nativeClientVersion(runtime.options),
|
|
5499
|
+
proofFields,
|
|
5500
|
+
);
|
|
4628
5501
|
telemetry.recoveryAttached(pending.operationId);
|
|
4629
5502
|
}
|
|
4630
5503
|
|
|
@@ -4790,7 +5663,11 @@ async function runShadowPreview(
|
|
|
4790
5663
|
);
|
|
4791
5664
|
}
|
|
4792
5665
|
|
|
4793
|
-
|
|
5666
|
+
const registerWithClock = (
|
|
5667
|
+
on: Parameters<Register>[0],
|
|
5668
|
+
options: HookOptions,
|
|
5669
|
+
monotonicClock: MonotonicClock,
|
|
5670
|
+
): void => {
|
|
4794
5671
|
const enabled = options.enabled === true;
|
|
4795
5672
|
const disclosed = options.disclosed === true;
|
|
4796
5673
|
const preservationMode: PreservationMode =
|
|
@@ -4908,7 +5785,10 @@ export const register: Register = (on, options) => {
|
|
|
4908
5785
|
|
|
4909
5786
|
on('prompt.submit', async (api, rawEvent, next) => {
|
|
4910
5787
|
const event = rawEvent as unknown as PromptSubmitHookEvent;
|
|
4911
|
-
|
|
5788
|
+
// The host has completed the prior native turn before accepting a new
|
|
5789
|
+
// prompt. This is the final proof opportunity for a fallback whose first
|
|
5790
|
+
// turn.complete callback observed the retired/default view.
|
|
5791
|
+
await confirmPendingProof(api, proofRuntime);
|
|
4912
5792
|
if (
|
|
4913
5793
|
proofRuntime.compactionStrategy === 'native_summary_notes' &&
|
|
4914
5794
|
enabled &&
|
|
@@ -4983,7 +5863,10 @@ export const register: Register = (on, options) => {
|
|
|
4983
5863
|
});
|
|
4984
5864
|
|
|
4985
5865
|
on('turn.complete', async (api, event, next) => {
|
|
4986
|
-
|
|
5866
|
+
// The native host can publish the result before its active API view catches
|
|
5867
|
+
// up. Keep only the atomic fallback marker pending through this lifecycle
|
|
5868
|
+
// edge; prompt.submit performs the strict confirmation on the next turn.
|
|
5869
|
+
await confirmPendingProof(api, proofRuntime, true);
|
|
4987
5870
|
if (
|
|
4988
5871
|
proofRuntime.compactionStrategy === 'native_summary_notes' &&
|
|
4989
5872
|
enabled &&
|
|
@@ -5155,7 +6038,7 @@ export const register: Register = (on, options) => {
|
|
|
5155
6038
|
nativeContinuationStarted = true;
|
|
5156
6039
|
const nativeResult = await next(event);
|
|
5157
6040
|
if (fallbackCapture !== undefined)
|
|
5158
|
-
|
|
6041
|
+
return queueNativeFallbackProof(
|
|
5159
6042
|
api,
|
|
5160
6043
|
proofRuntime,
|
|
5161
6044
|
fallbackCapture.operationId,
|
|
@@ -5163,6 +6046,8 @@ export const register: Register = (on, options) => {
|
|
|
5163
6046
|
fallbackCapture.baselineHash,
|
|
5164
6047
|
fallbackCapture.baselineCount,
|
|
5165
6048
|
nativeResult,
|
|
6049
|
+
fallbackCapture.beforeVisibleMessages,
|
|
6050
|
+
fallbackCapture.activeOffset,
|
|
5166
6051
|
);
|
|
5167
6052
|
return nativeResult;
|
|
5168
6053
|
};
|
|
@@ -5225,6 +6110,8 @@ export const register: Register = (on, options) => {
|
|
|
5225
6110
|
| 'request_id'
|
|
5226
6111
|
| 'correlation_id'
|
|
5227
6112
|
| 'retry_after_ms'
|
|
6113
|
+
| 'provider_response_format'
|
|
6114
|
+
| 'reason'
|
|
5228
6115
|
> = {},
|
|
5229
6116
|
): void => {
|
|
5230
6117
|
if (!observation.diagnosticsEnabled || diagnosticRecords.length >= 64) return;
|
|
@@ -5285,6 +6172,8 @@ export const register: Register = (on, options) => {
|
|
|
5285
6172
|
let proofSessionRef: string | undefined;
|
|
5286
6173
|
let proofBaselineHash: string | undefined;
|
|
5287
6174
|
let proofBaselineCount = 0;
|
|
6175
|
+
let baselineActiveOffset: number | undefined;
|
|
6176
|
+
let proofBaselineVisibleMessages: readonly string[] | undefined;
|
|
5288
6177
|
let historicalNotes: readonly VerifiedHistoricalNote[] = [];
|
|
5289
6178
|
try {
|
|
5290
6179
|
const current = event.messages;
|
|
@@ -5303,14 +6192,22 @@ export const register: Register = (on, options) => {
|
|
|
5303
6192
|
});
|
|
5304
6193
|
const rereadBaselineHash = await sha256(canonicalJson(reread));
|
|
5305
6194
|
proofBaselineHash = rereadBaselineHash;
|
|
5306
|
-
proofBaselineCount =
|
|
5307
|
-
|
|
6195
|
+
proofBaselineCount = reread.length;
|
|
6196
|
+
const rereadActiveOffset = visibleSnapshotsMatch(
|
|
6197
|
+
current,
|
|
6198
|
+
reread,
|
|
6199
|
+
event.trigger,
|
|
6200
|
+
event.instructions,
|
|
6201
|
+
);
|
|
6202
|
+
if (rereadActiveOffset === undefined) {
|
|
5308
6203
|
await diagnosticMismatch(api, diagnosticMode, 'reread_before', current, reread);
|
|
5309
6204
|
markDiagnostic('snapshot', 'invalid_response');
|
|
5310
6205
|
markDiagnostic('fallback', 'invalid_response');
|
|
5311
6206
|
logFallback(api, 'reread_before', undefined, policyVersion === 2);
|
|
5312
6207
|
return continueWithTelemetryFlush();
|
|
5313
6208
|
}
|
|
6209
|
+
baselineActiveOffset = rereadActiveOffset;
|
|
6210
|
+
proofBaselineVisibleMessages = visibleMessageCanonicalStrings(reread);
|
|
5314
6211
|
stage = 'projection';
|
|
5315
6212
|
const built = buildProjection(
|
|
5316
6213
|
current,
|
|
@@ -5359,7 +6256,7 @@ export const register: Register = (on, options) => {
|
|
|
5359
6256
|
markDiagnostic('budget', 'provider_not_attempted', {
|
|
5360
6257
|
item_count: built.pairs.filter((pair) => !pair.protectedPair).length,
|
|
5361
6258
|
});
|
|
5362
|
-
const budgetRead = await readContextBudget(api, brainServer);
|
|
6259
|
+
const budgetRead = await readContextBudget(api, brainServer, monotonicClock);
|
|
5363
6260
|
if (!budgetRead.ok) {
|
|
5364
6261
|
const admission = budgetRead.kind === 'admission_denied';
|
|
5365
6262
|
const category: ObservationRecord['category'] = admission
|
|
@@ -5374,8 +6271,13 @@ export const register: Register = (on, options) => {
|
|
|
5374
6271
|
const errorMetadata: Partial<ContextErrorMetadata> = isRecord(budgetRead.error)
|
|
5375
6272
|
? (budgetRead.error as ContextErrorMetadata)
|
|
5376
6273
|
: {};
|
|
6274
|
+
const budgetErrorCode = admission
|
|
6275
|
+
? 'shared_admission_exceeded'
|
|
6276
|
+
: budgetRead.kind === 'invalid_response'
|
|
6277
|
+
? 'budget_response_invalid'
|
|
6278
|
+
: 'provider_unavailable';
|
|
5377
6279
|
markDiagnostic('budget', category, {
|
|
5378
|
-
compact_error_code:
|
|
6280
|
+
compact_error_code: budgetErrorCode,
|
|
5379
6281
|
...(typeof errorMetadata.admissionDetail === 'string'
|
|
5380
6282
|
? { admission_detail: errorMetadata.admissionDetail }
|
|
5381
6283
|
: {}),
|
|
@@ -5390,7 +6292,7 @@ export const register: Register = (on, options) => {
|
|
|
5390
6292
|
: {}),
|
|
5391
6293
|
});
|
|
5392
6294
|
markDiagnostic('fallback', category, {
|
|
5393
|
-
compact_error_code:
|
|
6295
|
+
compact_error_code: budgetErrorCode,
|
|
5394
6296
|
});
|
|
5395
6297
|
logFallback(
|
|
5396
6298
|
api,
|
|
@@ -5406,7 +6308,9 @@ export const register: Register = (on, options) => {
|
|
|
5406
6308
|
operationId,
|
|
5407
6309
|
sessionRef,
|
|
5408
6310
|
baselineHash: rereadBaselineHash,
|
|
5409
|
-
baselineCount:
|
|
6311
|
+
baselineCount: reread.length,
|
|
6312
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6313
|
+
activeOffset: baselineActiveOffset,
|
|
5410
6314
|
});
|
|
5411
6315
|
}
|
|
5412
6316
|
const budget = budgetRead.snapshot;
|
|
@@ -5431,7 +6335,9 @@ export const register: Register = (on, options) => {
|
|
|
5431
6335
|
operationId,
|
|
5432
6336
|
sessionRef,
|
|
5433
6337
|
baselineHash: rereadBaselineHash,
|
|
5434
|
-
baselineCount:
|
|
6338
|
+
baselineCount: reread.length,
|
|
6339
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6340
|
+
activeOffset: baselineActiveOffset,
|
|
5435
6341
|
});
|
|
5436
6342
|
}
|
|
5437
6343
|
let providerProjection = built.projection;
|
|
@@ -5506,7 +6412,9 @@ export const register: Register = (on, options) => {
|
|
|
5506
6412
|
operationId,
|
|
5507
6413
|
sessionRef,
|
|
5508
6414
|
baselineHash: rereadBaselineHash,
|
|
5509
|
-
baselineCount:
|
|
6415
|
+
baselineCount: reread.length,
|
|
6416
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6417
|
+
activeOffset: baselineActiveOffset,
|
|
5510
6418
|
});
|
|
5511
6419
|
}
|
|
5512
6420
|
providerProjection = window.projection;
|
|
@@ -5552,18 +6460,42 @@ export const register: Register = (on, options) => {
|
|
|
5552
6460
|
projection: providerProjection,
|
|
5553
6461
|
...(policyVersion === 2 ? { schema_version: 2, options: wireOptions } : {}),
|
|
5554
6462
|
};
|
|
6463
|
+
await persistDiagnostics();
|
|
6464
|
+
if (!contextBudgetIsFresh(budget, monotonicClock())) {
|
|
6465
|
+
markDiagnostic('budget', 'invalid_response', {
|
|
6466
|
+
compact_error_code: 'budget_response_invalid',
|
|
6467
|
+
});
|
|
6468
|
+
markDiagnostic('fallback', 'invalid_response', {
|
|
6469
|
+
compact_error_code: 'budget_response_invalid',
|
|
6470
|
+
});
|
|
6471
|
+
logFallback(api, 'budget_unavailable', undefined, policyVersion === 2);
|
|
6472
|
+
getTelemetry(api, brainServer, options, telemetryState).fallback(
|
|
6473
|
+
operationId,
|
|
6474
|
+
'budget_unavailable',
|
|
6475
|
+
);
|
|
6476
|
+
return continueWithTelemetryFlush({
|
|
6477
|
+
operationId,
|
|
6478
|
+
sessionRef,
|
|
6479
|
+
baselineHash: rereadBaselineHash,
|
|
6480
|
+
baselineCount: reread.length,
|
|
6481
|
+
beforeVisibleMessages: visibleMessageCanonicalStrings(reread),
|
|
6482
|
+
activeOffset: baselineActiveOffset,
|
|
6483
|
+
});
|
|
6484
|
+
}
|
|
5555
6485
|
stage = 'mcp_call';
|
|
5556
6486
|
markDiagnostic('provider_request', 'request_attempted', {
|
|
5557
6487
|
item_count: providerPairs.filter((pair) => !pair.protectedPair).length,
|
|
5558
6488
|
byte_count: byteLength(JSON.stringify(input)),
|
|
5559
6489
|
});
|
|
5560
|
-
await persistDiagnostics();
|
|
5561
6490
|
if (diagnosticMode)
|
|
5562
6491
|
api.ui.log(
|
|
5563
6492
|
`AI Brain context diagnostic ${JSON.stringify({ hook: 'session.compact', phase: 'mcp_started' })}`,
|
|
5564
6493
|
);
|
|
5565
6494
|
const call = api.mcp.call(brainServer, 'brain_context_compact', input);
|
|
5566
6495
|
pendingMcp = call;
|
|
6496
|
+
// The request has started, so persist its attempt without delaying the
|
|
6497
|
+
// budget freshness check or extending the provider deadline.
|
|
6498
|
+
void persistDiagnostics();
|
|
5567
6499
|
let timedOut = false;
|
|
5568
6500
|
const hostTimers = globalThis as unknown as {
|
|
5569
6501
|
setTimeout?: (callback: () => void, delay: number) => unknown;
|
|
@@ -5610,13 +6542,41 @@ export const register: Register = (on, options) => {
|
|
|
5610
6542
|
markDiagnostic(
|
|
5611
6543
|
'provider_response',
|
|
5612
6544
|
response.isError === true ? 'native_error' : 'response_obtained',
|
|
6545
|
+
providerResponseMetadata(response),
|
|
5613
6546
|
);
|
|
5614
6547
|
if (response.isError === true) throw providerResponseError(response);
|
|
5615
6548
|
stage = 'parse_plan';
|
|
5616
|
-
const structured = mcpStructuredValue(response);
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
6549
|
+
const structured = mcpStructuredValue(response, MAX_RESULT_BYTES);
|
|
6550
|
+
let plan: Record<string, unknown>;
|
|
6551
|
+
try {
|
|
6552
|
+
const serialized = JSON.stringify(structured);
|
|
6553
|
+
const responseText = response.content?.find(
|
|
6554
|
+
(block): block is { readonly type: string; readonly text: string } =>
|
|
6555
|
+
block.type === 'text' && typeof block.text === 'string',
|
|
6556
|
+
)?.text;
|
|
6557
|
+
if (
|
|
6558
|
+
(serialized === undefined &&
|
|
6559
|
+
response.structuredContent === undefined &&
|
|
6560
|
+
responseText === undefined) ||
|
|
6561
|
+
(response.structuredContent === undefined &&
|
|
6562
|
+
responseText !== undefined &&
|
|
6563
|
+
byteLength(responseText) > MAX_RESULT_BYTES) ||
|
|
6564
|
+
(serialized !== undefined && byteLength(serialized) > MAX_RESULT_BYTES)
|
|
6565
|
+
)
|
|
6566
|
+
throw new Error(
|
|
6567
|
+
serialized === undefined &&
|
|
6568
|
+
response.structuredContent === undefined &&
|
|
6569
|
+
responseText === undefined
|
|
6570
|
+
? 'AI Brain plan envelope is missing'
|
|
6571
|
+
: 'AI Brain response is too large',
|
|
6572
|
+
);
|
|
6573
|
+
plan = parsePlan(structured, policyVersion, v2Options);
|
|
6574
|
+
} catch (error) {
|
|
6575
|
+
throw planBindingFailure(
|
|
6576
|
+
planParseFailureReason(error),
|
|
6577
|
+
'AI Brain plan failed validation',
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
5620
6580
|
const planDecisions = plan.decisions;
|
|
5621
6581
|
const planStats = plan.stats;
|
|
5622
6582
|
markDiagnostic('plan_validation', 'provider_decision', {
|
|
@@ -5627,22 +6587,30 @@ export const register: Register = (on, options) => {
|
|
|
5627
6587
|
`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
6588
|
);
|
|
5629
6589
|
stage = 'match_plan';
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
(policyVersion === 2
|
|
5635
|
-
|
|
5636
|
-
)
|
|
5637
|
-
throw
|
|
6590
|
+
const bindingReason = planBindingFailureReason(plan, {
|
|
6591
|
+
sessionRef,
|
|
6592
|
+
revision,
|
|
6593
|
+
projectionHash: await sha256(canonicalJson(providerProjection)),
|
|
6594
|
+
...(policyVersion === 2 ? { optionsHash: optionsHash ?? null } : {}),
|
|
6595
|
+
});
|
|
6596
|
+
if (policyVersion === 2 && optionsHash === undefined)
|
|
6597
|
+
throw planBindingFailure('plan_options_mismatch', 'AI Brain plan options are missing');
|
|
6598
|
+
if (bindingReason !== undefined)
|
|
6599
|
+
throw planBindingFailure(bindingReason, 'AI Brain plan binding was rejected');
|
|
5638
6600
|
if ((await api.session.id()) !== sessionRef)
|
|
5639
|
-
throw
|
|
6601
|
+
throw planBindingFailure(
|
|
6602
|
+
'session_changed_during_planning',
|
|
6603
|
+
'Claude session changed during planning',
|
|
6604
|
+
);
|
|
5640
6605
|
stage = 'reread_after';
|
|
5641
6606
|
const latestVisible = await api.session.messages();
|
|
5642
6607
|
if ((await sha256(canonicalJson(latestVisible))) !== rereadBaselineHash) {
|
|
5643
6608
|
await diagnosticMismatch(api, diagnosticMode, 'reread_after', current, latestVisible);
|
|
5644
6609
|
logFallback(api, 'reread_after', undefined, policyVersion === 2);
|
|
5645
|
-
throw
|
|
6610
|
+
throw planBindingFailure(
|
|
6611
|
+
'snapshot_changed_during_planning',
|
|
6612
|
+
'Claude transcript changed during planning',
|
|
6613
|
+
);
|
|
5646
6614
|
}
|
|
5647
6615
|
if (compactionStrategy === 'native_summary_notes') {
|
|
5648
6616
|
const latestHistoricalNotes = await collectVerifiedHistoricalNotes(
|
|
@@ -5655,7 +6623,10 @@ export const register: Register = (on, options) => {
|
|
|
5655
6623
|
latestHistoricalNotes.map((note) => [note.capsuleId, note.noteSha256]),
|
|
5656
6624
|
) !== canonicalJson(historicalNotes.map((note) => [note.capsuleId, note.noteSha256]))
|
|
5657
6625
|
)
|
|
5658
|
-
throw
|
|
6626
|
+
throw planBindingFailure(
|
|
6627
|
+
'historical_notes_changed_during_planning',
|
|
6628
|
+
'Claude historical notes changed during planning',
|
|
6629
|
+
);
|
|
5659
6630
|
}
|
|
5660
6631
|
if (plan.status === 'noop') {
|
|
5661
6632
|
markDiagnostic('candidate_preparation', 'skipped');
|
|
@@ -5729,6 +6700,7 @@ export const register: Register = (on, options) => {
|
|
|
5729
6700
|
transformed,
|
|
5730
6701
|
);
|
|
5731
6702
|
getTelemetry(api, brainServer, options, telemetryState).notesWritten(operationId);
|
|
6703
|
+
const beforeVisibleMessages = visibleMessageCanonicalStrings(reread);
|
|
5732
6704
|
nativeContinuationStarted = true;
|
|
5733
6705
|
const nativeInput = appendHistoricalNotes(transformed, historicalNotes);
|
|
5734
6706
|
const nativeResult = await next({ ...event, messages: nativeInput });
|
|
@@ -5738,6 +6710,29 @@ export const register: Register = (on, options) => {
|
|
|
5738
6710
|
throw new Error('native_summary_result_missing');
|
|
5739
6711
|
const configuration = fileCheckpointConfiguration(options);
|
|
5740
6712
|
if (configuration.mode !== 'helper') throw new Error('native_note_helper_unavailable');
|
|
6713
|
+
const expectedMessageHashes =
|
|
6714
|
+
expectedMessages.length <= MAX_NATIVE_SUMMARY_MESSAGE_HASHES
|
|
6715
|
+
? await Promise.all(
|
|
6716
|
+
expectedMessages.map(async (message) =>
|
|
6717
|
+
(await sha256(canonicalJson(visibleMessage(message)))).slice(0, 16),
|
|
6718
|
+
),
|
|
6719
|
+
)
|
|
6720
|
+
: undefined;
|
|
6721
|
+
const wrapper = nativeSummaryWrapper(note.capsuleId, note.noteText);
|
|
6722
|
+
const deliveredResult = nativeSummaryDeliveryResult(nativeResult, wrapper);
|
|
6723
|
+
if (deliveredResult === undefined) {
|
|
6724
|
+
getTelemetry(api, brainServer, options, telemetryState).uncertain(
|
|
6725
|
+
operationId,
|
|
6726
|
+
'native_summary_recovery',
|
|
6727
|
+
);
|
|
6728
|
+
return nativeResult;
|
|
6729
|
+
}
|
|
6730
|
+
const deliveryMode: NativeSummaryPending['deliveryMode'] = 'atomic_result';
|
|
6731
|
+
const anchors = await nativeProofAnchors(
|
|
6732
|
+
beforeVisibleMessages,
|
|
6733
|
+
expectedMessages.length,
|
|
6734
|
+
baselineActiveOffset,
|
|
6735
|
+
);
|
|
5741
6736
|
const pendingNativeSummary: NativeSummaryPending = {
|
|
5742
6737
|
schemaVersion: 1,
|
|
5743
6738
|
operationId,
|
|
@@ -5747,13 +6742,19 @@ export const register: Register = (on, options) => {
|
|
|
5747
6742
|
noteSha256: note.noteSha256,
|
|
5748
6743
|
expectedPrefixHash: await sha256(canonicalJson(visibleMessages(expectedMessages))),
|
|
5749
6744
|
expectedCount: expectedMessages.length,
|
|
6745
|
+
...(expectedMessageHashes === undefined ? {} : { expectedMessageHashes }),
|
|
6746
|
+
deliveryMode,
|
|
6747
|
+
anchorCounts: anchors.counts,
|
|
6748
|
+
anchorHashes: anchors.hashes,
|
|
5750
6749
|
expiresAt: new Date(Date.now() + PENDING_PROOF_TTL_MS).toISOString(),
|
|
5751
6750
|
appendAttempted: false,
|
|
5752
6751
|
nativeCompletedReported: false,
|
|
5753
6752
|
};
|
|
6753
|
+
let deliveryIntentSaved = false;
|
|
5754
6754
|
try {
|
|
5755
6755
|
await saveNativeSummaryPending(api, pendingNativeSummary);
|
|
5756
6756
|
proofRuntime.pendingNativeSummary = pendingNativeSummary;
|
|
6757
|
+
deliveryIntentSaved = true;
|
|
5757
6758
|
} catch {
|
|
5758
6759
|
if (nativeSessionRef !== undefined)
|
|
5759
6760
|
await flushProofDiagnostic(
|
|
@@ -5763,7 +6764,7 @@ export const register: Register = (on, options) => {
|
|
|
5763
6764
|
nativeSessionRef,
|
|
5764
6765
|
observationInstanceId,
|
|
5765
6766
|
nativeClientVersion(options),
|
|
5766
|
-
'
|
|
6767
|
+
'native_summary_attachment_uncertain',
|
|
5767
6768
|
);
|
|
5768
6769
|
getTelemetry(api, brainServer, options, telemetryState).uncertain(
|
|
5769
6770
|
operationId,
|
|
@@ -5772,7 +6773,7 @@ export const register: Register = (on, options) => {
|
|
|
5772
6773
|
}
|
|
5773
6774
|
if (options.telemetryEnabled === true && options.telemetryDisclosed === true)
|
|
5774
6775
|
await getTelemetry(api, brainServer, options, telemetryState).flush();
|
|
5775
|
-
return nativeResult;
|
|
6776
|
+
return deliveryIntentSaved ? (deliveredResult ?? nativeResult) : nativeResult;
|
|
5776
6777
|
}
|
|
5777
6778
|
return { messages: transformed };
|
|
5778
6779
|
} catch (error) {
|
|
@@ -5791,7 +6792,7 @@ export const register: Register = (on, options) => {
|
|
|
5791
6792
|
nativeSessionRef,
|
|
5792
6793
|
observationInstanceId,
|
|
5793
6794
|
nativeClientVersion(options),
|
|
5794
|
-
'
|
|
6795
|
+
'native_summary_attachment_uncertain',
|
|
5795
6796
|
);
|
|
5796
6797
|
}
|
|
5797
6798
|
throw error;
|
|
@@ -5817,6 +6818,14 @@ export const register: Register = (on, options) => {
|
|
|
5817
6818
|
const rpcStatus = safeRpcStatus(error);
|
|
5818
6819
|
const checkpoint = failureStage === 'checkpoint' ? checkpointFailure(error) : undefined;
|
|
5819
6820
|
const errorMetadata = isRecord(error) ? error : {};
|
|
6821
|
+
const planReason =
|
|
6822
|
+
failureStage === 'parse_plan' ||
|
|
6823
|
+
failureStage === 'match_plan' ||
|
|
6824
|
+
failureStage === 'reread_after'
|
|
6825
|
+
? isPlanBindingFailureReason(errorMetadata.planBindingFailureReason)
|
|
6826
|
+
? errorMetadata.planBindingFailureReason
|
|
6827
|
+
: undefined
|
|
6828
|
+
: undefined;
|
|
5820
6829
|
const admissionFallback = isAdmissionFallback(error);
|
|
5821
6830
|
const compactCode =
|
|
5822
6831
|
isRecord(error) &&
|
|
@@ -5855,6 +6864,7 @@ export const register: Register = (on, options) => {
|
|
|
5855
6864
|
...(typeof errorMetadata.retryAfterMs === 'number'
|
|
5856
6865
|
? { retry_after_ms: errorMetadata.retryAfterMs }
|
|
5857
6866
|
: {}),
|
|
6867
|
+
...(planReason === undefined ? {} : { reason: planReason }),
|
|
5858
6868
|
...(checkpoint === undefined
|
|
5859
6869
|
? {}
|
|
5860
6870
|
: { checkpoint_phase: checkpoint.phase, checkpoint_error: checkpoint.code }),
|
|
@@ -5867,6 +6877,7 @@ export const register: Register = (on, options) => {
|
|
|
5867
6877
|
markDiagnostic('fallback', failureCategory, {
|
|
5868
6878
|
...(rpcStatus === undefined ? {} : { rpc_status: rpcStatus }),
|
|
5869
6879
|
...(compactCode === undefined ? {} : { compact_error_code: compactCode }),
|
|
6880
|
+
...(planReason === undefined ? {} : { reason: planReason }),
|
|
5870
6881
|
});
|
|
5871
6882
|
const telemetry = getTelemetry(api, brainServer, options, telemetryState);
|
|
5872
6883
|
telemetry.fallback(operationId, admissionFallback ? 'admission_denied' : stage);
|
|
@@ -5878,6 +6889,12 @@ export const register: Register = (on, options) => {
|
|
|
5878
6889
|
sessionRef: proofSessionRef,
|
|
5879
6890
|
baselineHash: proofBaselineHash,
|
|
5880
6891
|
baselineCount: proofBaselineCount,
|
|
6892
|
+
...(proofBaselineVisibleMessages === undefined
|
|
6893
|
+
? {}
|
|
6894
|
+
: { beforeVisibleMessages: proofBaselineVisibleMessages }),
|
|
6895
|
+
...(baselineActiveOffset === undefined
|
|
6896
|
+
? {}
|
|
6897
|
+
: { activeOffset: baselineActiveOffset }),
|
|
5881
6898
|
}
|
|
5882
6899
|
: undefined,
|
|
5883
6900
|
);
|
|
@@ -5905,3 +6922,13 @@ export const register: Register = (on, options) => {
|
|
|
5905
6922
|
});
|
|
5906
6923
|
});
|
|
5907
6924
|
};
|
|
6925
|
+
|
|
6926
|
+
export const register: Register = (on, options) => registerWithClock(on, options, monotonicNow);
|
|
6927
|
+
|
|
6928
|
+
export function registerForTesting(
|
|
6929
|
+
on: Parameters<Register>[0],
|
|
6930
|
+
options: HookOptions,
|
|
6931
|
+
monotonicClock: MonotonicClock,
|
|
6932
|
+
): void {
|
|
6933
|
+
registerWithClock(on, options, monotonicClock);
|
|
6934
|
+
}
|