@erichll/pi-auto-review 0.4.0 → 0.6.0

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/src/policy.ts CHANGED
@@ -47,16 +47,103 @@ export type TranscriptConfig = {
47
47
 
48
48
  export type TranscriptResult = {
49
49
  text: string;
50
+ surfaceProfile: EvidenceSurfaceProfile;
51
+ reviewerEvidence: {
52
+ userMessages: ReviewerEvidenceItem[];
53
+ toolCalls: ReviewerEvidenceItem[];
54
+ relevantResults: ReviewerEvidenceItem[];
55
+ };
56
+ budgetRemovals: ReviewerBudgetRemoval[];
50
57
  userCharacters: number;
51
58
  toolCharacters: number;
52
59
  relevantResultCharacters: number;
53
60
  truncated: boolean;
61
+ selectedCandidates: EvidenceCandidateMetadata[];
62
+ failureCode?:
63
+ | "critical_evidence_overflow"
64
+ | "required_profile_overflow"
65
+ | "reviewer_input_budget_exceeded";
66
+ userAuthorizationCeiling: "unknown" | "low" | "medium" | "high";
67
+ userConstraint: "none" | "narrowed" | "revoked";
68
+ compactionState: "none" | "summary-present" | "authorization-unavailable";
69
+ };
70
+
71
+ export type ReviewerBudgetRemoval = {
72
+ reason:
73
+ | "secondary-reasons"
74
+ | "older-structured-tool"
75
+ | "optional-result";
76
+ count: number;
77
+ };
78
+
79
+ export type ReviewerEvidenceItem = {
80
+ id: string;
81
+ reason: EvidenceSelectionReason;
82
+ secondaryReasons: EvidenceSelectionReason[];
83
+ toolCallId?: string;
84
+ content: string;
54
85
  };
55
86
 
56
87
  type Evidence = {
88
+ id: string;
57
89
  index: number;
90
+ entryIndex: number;
58
91
  kind: "user" | "tool";
59
92
  text: string;
93
+ toolCallId?: string;
94
+ reason: EvidenceSelectionReason;
95
+ secondaryReasons: EvidenceSelectionReason[];
96
+ sensitivity: EvidenceSensitivity;
97
+ originalCharacters: number;
98
+ preTruncated: boolean;
99
+ representationCharacters?: number;
100
+ };
101
+
102
+ export type EvidenceSensitivity =
103
+ | "user-intent"
104
+ | "tool-input"
105
+ | "tool-output"
106
+ | "boundary"
107
+ | "credential"
108
+ | "permission-control"
109
+ | "authorization-persistence"
110
+ | "transport-weakening"
111
+ | "audit-control"
112
+ | "security-control";
113
+
114
+ export type EvidenceSelectionReason =
115
+ | "latest-user"
116
+ | "exact-request-reference"
117
+ | "trusted-retry-user-message"
118
+ | "exact-tool-call"
119
+ | "structured-request-match"
120
+ | "security-combination"
121
+ | "same-tool"
122
+ | "delete-precheck"
123
+ | "git-push-context"
124
+ | "provider-branch-protection"
125
+ | "sandbox-trap";
126
+
127
+ export type EvidenceSurfaceProfile =
128
+ | "network"
129
+ | "delete"
130
+ | "git-push"
131
+ | "forwarded"
132
+ | "generic";
133
+
134
+ export type EvidenceCandidateMetadata = {
135
+ id: string;
136
+ entryIndex: number;
137
+ kind: "user" | "tool-call" | "tool-result" | "sandbox-trap";
138
+ toolCallId?: string;
139
+ reason: EvidenceSelectionReason;
140
+ secondaryReasons: EvidenceSelectionReason[];
141
+ surfaceProfile: EvidenceSurfaceProfile;
142
+ originalCharacters: number;
143
+ selectedCharacters: number;
144
+ estimatedTokens: number;
145
+ truncated: boolean;
146
+ sensitivity: EvidenceSensitivity;
60
147
  };
61
148
 
62
149
  export type RelevantBoundaryRequest = {
@@ -64,16 +151,40 @@ export type RelevantBoundaryRequest = {
64
151
  source?: string;
65
152
  surface?: string;
66
153
  operation?: string;
154
+ cwd?: string;
67
155
  command?: string;
68
156
  path?: string;
69
157
  resolvedPath?: string;
70
158
  destination?: string;
71
159
  toolCallId?: string;
72
160
  toolName?: string;
161
+ toolInputPreview?: string;
162
+ agentName?: string;
163
+ requesterSessionId?: string;
164
+ accessIntent?: {
165
+ surface: string;
166
+ matchValues: readonly string[];
167
+ boundaryValue?: string;
168
+ };
169
+ trustedRetryOriginalRequestId?: string;
73
170
  };
74
171
 
75
172
  const MAX_EVIDENCE_ITEM_CHARACTERS = 4_000;
76
173
 
174
+ /** Stable compact JSON for reviewer-only representations. */
175
+ export function canonicalReviewerJson(value: unknown): string {
176
+ return JSON.stringify(value, (_key, current: unknown) => {
177
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
178
+ return current;
179
+ }
180
+ return Object.fromEntries(
181
+ Object.keys(current as Record<string, unknown>)
182
+ .sort()
183
+ .map((key) => [key, (current as Record<string, unknown>)[key]]),
184
+ );
185
+ }) ?? "null";
186
+ }
187
+
77
188
  function exactKeys(
78
189
  record: Record<string, unknown>,
79
190
  expected: readonly string[],
@@ -492,7 +603,9 @@ function userText(message: Record<string, unknown>): string {
492
603
  .trim();
493
604
  }
494
605
 
495
- function toolTexts(message: Record<string, unknown>): string[] {
606
+ function toolTexts(
607
+ message: Record<string, unknown>,
608
+ ): Array<{ id?: string; name: string; text: string }> {
496
609
  if (message.role !== "assistant" || !Array.isArray(message.content)) {
497
610
  return [];
498
611
  }
@@ -502,13 +615,23 @@ function toolTexts(message: Record<string, unknown>): string[] {
502
615
  if (record.type !== "toolCall" || typeof record.name !== "string") {
503
616
  return [];
504
617
  }
505
- return [`${record.name} ${boundedString(record.arguments ?? {})}`];
618
+ const id =
619
+ typeof record.id === "string"
620
+ ? record.id
621
+ : typeof record.toolCallId === "string"
622
+ ? record.toolCallId
623
+ : undefined;
624
+ return [{
625
+ id,
626
+ name: record.name,
627
+ text: `${record.name} ${boundedString(record.arguments ?? {})}`,
628
+ }];
506
629
  });
507
630
  }
508
631
 
509
632
  function extractEvidence(entries: readonly unknown[]): Evidence[] {
510
633
  const evidence: Evidence[] = [];
511
- entries.forEach((entry) => {
634
+ entries.forEach((entry, entryIndex) => {
512
635
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
513
636
  const message = (entry as Record<string, unknown>).message;
514
637
  if (!message || typeof message !== "object" || Array.isArray(message)) {
@@ -518,13 +641,34 @@ function extractEvidence(entries: readonly unknown[]): Evidence[] {
518
641
  const user = userText(record);
519
642
  if (user) {
520
643
  evidence.push({
644
+ id: `${stableEntryId(entry, entryIndex)}:user`,
521
645
  index: evidence.length,
646
+ entryIndex,
522
647
  kind: "user",
523
648
  text: boundedString(user),
649
+ reason: "latest-user",
650
+ secondaryReasons: [],
651
+ sensitivity: "user-intent",
652
+ originalCharacters: user.length,
653
+ preTruncated: boundedString(user).length < user.length,
524
654
  });
525
655
  }
526
656
  for (const tool of toolTexts(record)) {
527
- evidence.push({ index: evidence.length, kind: "tool", text: tool });
657
+ evidence.push({
658
+ id: tool.id
659
+ ? `tool-call:${tool.id}`
660
+ : `${stableEntryId(entry, entryIndex)}:tool-${evidence.length}`,
661
+ index: evidence.length,
662
+ entryIndex,
663
+ kind: "tool",
664
+ text: tool.text,
665
+ toolCallId: tool.id,
666
+ reason: "structured-request-match",
667
+ secondaryReasons: [],
668
+ sensitivity: "tool-input",
669
+ originalCharacters: tool.text.length,
670
+ preTruncated: false,
671
+ });
528
672
  }
529
673
  });
530
674
  return evidence;
@@ -535,6 +679,8 @@ type ToolCallRecord = {
535
679
  name: string;
536
680
  arguments: unknown;
537
681
  rendered: string;
682
+ entryIndex: number;
683
+ candidateId: string;
538
684
  };
539
685
 
540
686
  function messageRecord(entry: unknown): Record<string, unknown> | undefined {
@@ -544,6 +690,14 @@ function messageRecord(entry: unknown): Record<string, unknown> | undefined {
544
690
  return message as Record<string, unknown>;
545
691
  }
546
692
 
693
+ function stableEntryId(entry: unknown, index: number): string {
694
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
695
+ const id = (entry as Record<string, unknown>).id;
696
+ if (typeof id === "string" && id.trim()) return `entry:${id}`;
697
+ }
698
+ return `entry-index:${index}`;
699
+ }
700
+
547
701
  function redactSensitiveResult(value: string): string {
548
702
  return value
549
703
  .replace(
@@ -668,20 +822,6 @@ function relevanceReason(
668
822
  | "provider-branch-protection"
669
823
  | undefined {
670
824
  if (request.toolCallId && call.id === request.toolCallId) return "same-tool";
671
- const callText = call.rendered.toLowerCase();
672
- const needles = [
673
- request.path,
674
- request.resolvedPath,
675
- request.destination,
676
- request.command,
677
- ]
678
- .filter((value): value is string => Boolean(value && value.length >= 3))
679
- .map((value) => value.toLowerCase());
680
- const namesMatch =
681
- Boolean(request.toolName) &&
682
- (call.name === request.toolName ||
683
- call.name.endsWith(`/${request.toolName}`) ||
684
- request.toolName?.endsWith(`/${call.name}`));
685
825
  const currentCommand = request.command || "";
686
826
  const priorCommand = commandArgument(call);
687
827
  const pushedBranch = explicitPushBranch(currentCommand);
@@ -690,13 +830,19 @@ function relevanceReason(
690
830
  /\b(?:rm|rmdir|unlink|trash|delete)\b/i.test(currentCommand);
691
831
  const readOnlyCheck =
692
832
  /^\s*(?:stat|ls|find|test|readlink|realpath)\b/i.test(priorCommand);
693
- const targets = [request.path, request.resolvedPath]
694
- .filter((value): value is string => Boolean(value))
695
- .map((value) => value.toLowerCase());
833
+ const priorWords = /[\n;&|`<>]|\$\(/.test(priorCommand)
834
+ ? []
835
+ : priorCommand
836
+ .trim()
837
+ .split(/\s+/)
838
+ .map((word) => word.replace(/^(["'])(.*)\1$/, "$2"));
839
+ const targets = [request.path, request.resolvedPath].filter(
840
+ (value): value is string => Boolean(value),
841
+ );
696
842
  if (
697
843
  destructive &&
698
844
  readOnlyCheck &&
699
- targets.some((target) => priorCommand.toLowerCase().includes(target))
845
+ targets.some((target) => priorWords.includes(target))
700
846
  ) {
701
847
  return "delete-precheck";
702
848
  }
@@ -715,21 +861,39 @@ function relevanceReason(
715
861
  ) {
716
862
  return "provider-branch-protection";
717
863
  }
718
- if (
719
- (namesMatch || needles.length > 0) &&
720
- needles.some((value) => callText.includes(value))
721
- ) {
722
- return "same-tool";
723
- }
724
864
  return undefined;
725
865
  }
726
866
 
727
867
  function relevantResultEvidence(
728
868
  entries: readonly unknown[],
729
869
  request: RelevantBoundaryRequest,
730
- ): Array<{ index: number; text: string }> {
870
+ currentTurnStart: number,
871
+ ): Array<{
872
+ index: number;
873
+ reason: Exclude<
874
+ EvidenceSelectionReason,
875
+ | "latest-user"
876
+ | "exact-request-reference"
877
+ | "trusted-retry-user-message"
878
+ | "exact-tool-call"
879
+ | "structured-request-match"
880
+ | "security-combination"
881
+ | "sandbox-trap"
882
+ >;
883
+ call: ToolCallRecord;
884
+ resultId: string;
885
+ resultText: string;
886
+ renderedResult: string;
887
+ }> {
731
888
  const calls = new Map<string, ToolCallRecord>();
732
- const results: Array<{ index: number; text: string }> = [];
889
+ const results: Array<{
890
+ index: number;
891
+ reason: "same-tool" | "delete-precheck" | "git-push-context" | "provider-branch-protection";
892
+ call: ToolCallRecord;
893
+ resultId: string;
894
+ resultText: string;
895
+ renderedResult: string;
896
+ }> = [];
733
897
  entries.forEach((entry, index) => {
734
898
  const message = messageRecord(entry);
735
899
  if (!message) return;
@@ -750,6 +914,8 @@ function relevantResultEvidence(
750
914
  name: record.name,
751
915
  arguments: record.arguments,
752
916
  rendered: `${record.name} ${boundedString(record.arguments ?? {})}`,
917
+ entryIndex: index,
918
+ candidateId: `tool-call:${id}`,
753
919
  });
754
920
  }
755
921
  return;
@@ -764,58 +930,616 @@ function relevantResultEvidence(
764
930
  if (!call) return;
765
931
  const reason = relevanceReason(call, request);
766
932
  const text = resultText(message);
767
- if (reason && text) {
933
+ const crossTurnAllowed = reason === "same-tool";
934
+ if (reason && text && (crossTurnAllowed || call.entryIndex >= currentTurnStart)) {
768
935
  results.push({
769
936
  index,
770
- text: `<tool-result reason="${reason}" tool="${escapeEvidenceMarkup(call.name)}">\n${escapeEvidenceMarkup(text)}\n</tool-result>`,
937
+ reason,
938
+ call,
939
+ resultId: `${stableEntryId(entry, index)}:tool-result:${message.toolCallId}`,
940
+ resultText: text,
941
+ renderedResult: `<tool-result id="${escapeEvidenceMarkup(message.toolCallId)}" reason="${reason}" tool="${escapeEvidenceMarkup(call.name)}">\n${escapeEvidenceMarkup(text)}\n</tool-result>`,
771
942
  });
772
943
  }
773
944
  });
774
945
  return results;
775
946
  }
776
947
 
777
- function sandboxTrapEvidence(
948
+ function sandboxTrapSupplement(
778
949
  request: RelevantBoundaryRequest,
779
950
  ): string | undefined {
780
951
  if (request.source !== "sandbox-runtime") return;
952
+ const supplement = {
953
+ ...(request.toolName ? { process: request.toolName } : {}),
954
+ };
955
+ if (Object.keys(supplement).length === 0) return;
956
+ return canonicalReviewerJson(supplement);
957
+ }
958
+
959
+ function sandboxTrapEvidence(
960
+ request: RelevantBoundaryRequest,
961
+ ): string | undefined {
962
+ const supplement = sandboxTrapSupplement(request);
963
+ if (!supplement) return;
781
964
  return `<sandbox-trap>
782
- ${boundedString({
783
- surface: request.surface,
784
- operation: request.operation,
965
+ ${supplement}
966
+ </sandbox-trap>`;
967
+ }
968
+
969
+ function currentTurnStart(entries: readonly unknown[]): number {
970
+ let start = 0;
971
+ entries.forEach((entry, index) => {
972
+ if (messageRecord(entry)?.role === "user") start = index;
973
+ });
974
+ return start;
975
+ }
976
+
977
+ function surfaceProfile(
978
+ request: RelevantBoundaryRequest,
979
+ ): EvidenceSurfaceProfile {
980
+ if (request.agentName || request.requesterSessionId) return "forwarded";
981
+ if (request.surface === "network" || request.destination) return "network";
982
+ if (/\bgit\b[\s\S]*\bpush\b/i.test(request.command ?? "")) return "git-push";
983
+ if (/\b(?:rm|rmdir|unlink|trash|delete)\b/i.test(request.command ?? "")) {
984
+ return "delete";
985
+ }
986
+ return "generic";
987
+ }
988
+
989
+ function collectToolCalls(entries: readonly unknown[]): ToolCallRecord[] {
990
+ const calls: ToolCallRecord[] = [];
991
+ entries.forEach((entry, entryIndex) => {
992
+ const message = messageRecord(entry);
993
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) return;
994
+ for (const part of message.content) {
995
+ if (!part || typeof part !== "object" || Array.isArray(part)) continue;
996
+ const value = part as Record<string, unknown>;
997
+ if (value.type !== "toolCall" || typeof value.name !== "string") continue;
998
+ const id =
999
+ typeof value.id === "string"
1000
+ ? value.id
1001
+ : typeof value.toolCallId === "string"
1002
+ ? value.toolCallId
1003
+ : undefined;
1004
+ calls.push({
1005
+ id,
1006
+ name: value.name,
1007
+ arguments: value.arguments,
1008
+ rendered: `${value.name} ${boundedString(value.arguments ?? {})}`,
1009
+ entryIndex,
1010
+ candidateId: id
1011
+ ? `tool-call:${id}`
1012
+ : `${stableEntryId(entry, entryIndex)}:tool-${calls.length}`,
1013
+ });
1014
+ }
1015
+ });
1016
+ return calls;
1017
+ }
1018
+
1019
+ function exactStructuredMatch(
1020
+ call: ToolCallRecord,
1021
+ request: RelevantBoundaryRequest,
1022
+ ): boolean {
1023
+ if (!call.arguments || typeof call.arguments !== "object" || Array.isArray(call.arguments)) {
1024
+ return false;
1025
+ }
1026
+ const args = call.arguments as Record<string, unknown>;
1027
+ const pairs: Array<[unknown, unknown]> = [
1028
+ [args.command, request.command],
1029
+ [args.path, request.path],
1030
+ [args.resolvedPath, request.resolvedPath],
1031
+ [args.destination, request.destination],
1032
+ [args.agentName, request.agentName],
1033
+ [args.requesterSessionId, request.requesterSessionId],
1034
+ [args.target, request.destination ?? request.path],
1035
+ [args.value, request.destination ?? request.path ?? request.command],
1036
+ ];
1037
+ if (pairs.some(
1038
+ ([actual, expected]) =>
1039
+ typeof actual === "string" &&
1040
+ typeof expected === "string" &&
1041
+ actual === expected,
1042
+ )) return true;
1043
+ const forwarding =
1044
+ args.forwarding && typeof args.forwarding === "object" && !Array.isArray(args.forwarding)
1045
+ ? args.forwarding as Record<string, unknown>
1046
+ : undefined;
1047
+ if (
1048
+ forwarding &&
1049
+ ((typeof forwarding.requesterAgentName === "string" &&
1050
+ forwarding.requesterAgentName === request.agentName) ||
1051
+ (typeof forwarding.requesterSessionId === "string" &&
1052
+ forwarding.requesterSessionId === request.requesterSessionId))
1053
+ ) return true;
1054
+ const intent =
1055
+ args.accessIntent && typeof args.accessIntent === "object" && !Array.isArray(args.accessIntent)
1056
+ ? args.accessIntent as Record<string, unknown>
1057
+ : undefined;
1058
+ return Boolean(
1059
+ intent &&
1060
+ request.accessIntent &&
1061
+ intent.surface === request.accessIntent.surface &&
1062
+ Array.isArray(intent.matchValues) &&
1063
+ intent.matchValues.length === request.accessIntent.matchValues.length &&
1064
+ intent.matchValues.every(
1065
+ (value, index) => value === request.accessIntent!.matchValues[index],
1066
+ ) &&
1067
+ intent.boundaryValue === request.accessIntent.boundaryValue,
1068
+ );
1069
+ }
1070
+
1071
+ function sameReviewerField(actual: unknown, expected: unknown): boolean {
1072
+ return expected !== undefined &&
1073
+ canonicalReviewerJson(actual) === canonicalReviewerJson(expected);
1074
+ }
1075
+
1076
+ function toolArgumentCoveredByRequest(
1077
+ key: string,
1078
+ actual: unknown,
1079
+ request: RelevantBoundaryRequest,
1080
+ ): boolean {
1081
+ if (key === "target") {
1082
+ return sameReviewerField(actual, request.destination ?? request.path);
1083
+ }
1084
+ if (key === "value") {
1085
+ return [request.command, request.path, request.destination].some(
1086
+ (expected) => sameReviewerField(actual, expected),
1087
+ );
1088
+ }
1089
+ if (key === "forwarding") {
1090
+ if (!request.agentName && !request.requesterSessionId) return false;
1091
+ return sameReviewerField(actual, {
1092
+ requesterAgentName: request.agentName,
1093
+ requesterSessionId: request.requesterSessionId,
1094
+ });
1095
+ }
1096
+ const expected = ({
1097
+ command: request.command,
785
1098
  path: request.path,
786
1099
  resolvedPath: request.resolvedPath,
787
1100
  destination: request.destination,
788
- process: request.toolName,
789
- })}
790
- </sandbox-trap>`;
1101
+ cwd: request.cwd,
1102
+ agentName: request.agentName,
1103
+ requesterSessionId: request.requesterSessionId,
1104
+ accessIntent: request.accessIntent,
1105
+ } as Record<string, unknown>)[key];
1106
+ return sameReviewerField(actual, expected);
1107
+ }
1108
+
1109
+ function exactToolCallReviewerRepresentation(
1110
+ call: ToolCallRecord,
1111
+ request: RelevantBoundaryRequest,
1112
+ reason: EvidenceSelectionReason,
1113
+ ): string | undefined {
1114
+ if (
1115
+ !call.id ||
1116
+ call.id !== request.toolCallId ||
1117
+ !call.arguments ||
1118
+ typeof call.arguments !== "object" ||
1119
+ Array.isArray(call.arguments)
1120
+ ) {
1121
+ return;
1122
+ }
1123
+ const args = call.arguments as Record<string, unknown>;
1124
+ const serializedArgs = canonicalReviewerJson(args);
1125
+ const previewCoversAll =
1126
+ request.toolInputPreview === serializedArgs ||
1127
+ request.toolInputPreview === `input ${serializedArgs}`;
1128
+ const supplement = previewCoversAll
1129
+ ? {}
1130
+ : Object.fromEntries(
1131
+ Object.entries(args).filter(
1132
+ ([key, actual]) =>
1133
+ !toolArgumentCoveredByRequest(key, actual, request),
1134
+ ),
1135
+ );
1136
+ return canonicalReviewerJson({
1137
+ id: call.id,
1138
+ name: call.name,
1139
+ reason,
1140
+ ...(Object.keys(supplement).length > 0 ? { supplement } : {}),
1141
+ });
1142
+ }
1143
+
1144
+ function securityEvidenceCategory(
1145
+ call: ToolCallRecord,
1146
+ ):
1147
+ | "credential"
1148
+ | "permission"
1149
+ | "authorization"
1150
+ | "transport"
1151
+ | "audit"
1152
+ | "security-control"
1153
+ | undefined {
1154
+ const command = commandArgument(call);
1155
+ if (!command) return;
1156
+ if (
1157
+ /\b(?:cat|sed|awk|base64|openssl|head|tail|grep|dd|xxd|tar|zip)\b[\s\S]*(?:\.env|\.ssh|\.aws\/credentials|\.kube\/config|\.npmrc|auth\.json)/i.test(
1158
+ command,
1159
+ )
1160
+ ) return "credential";
1161
+ if (/\b(?:chmod|chown|chgrp|setfacl)\b/i.test(command)) return "permission";
1162
+ if (/(?:authorized_keys|\/etc\/sudoers|access[_-]?grant)/i.test(command)) {
1163
+ return "authorization";
1164
+ }
1165
+ if (
1166
+ /(?:--insecure|--no-check-certificate|sslverify\s+false|strict-ssl\s+false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0|https?_proxy)/i.test(
1167
+ command,
1168
+ )
1169
+ ) return "transport";
1170
+ if (/\b(?:disable|truncate|delete|rm)\b[\s\S]*\b(?:audit|log)/i.test(command)) {
1171
+ return "audit";
1172
+ }
1173
+ if (/(?:pi-auto-review|pi-sandbox|permission-system|permissions\.json|sandbox\.json)/i.test(command)) {
1174
+ return "security-control";
1175
+ }
1176
+ return undefined;
1177
+ }
1178
+
1179
+ function selectRequestAwareTools(
1180
+ entries: readonly unknown[],
1181
+ evidence: Evidence[],
1182
+ request: RelevantBoundaryRequest,
1183
+ relevant: ReturnType<typeof relevantResultEvidence>,
1184
+ turnStart: number,
1185
+ ): { selected: Evidence[]; criticalEvidenceOverflow: boolean } {
1186
+ const calls = collectToolCalls(entries);
1187
+ const reasons = new Map<string, EvidenceSelectionReason[]>();
1188
+ const addReason = (call: ToolCallRecord, reason: EvidenceSelectionReason) => {
1189
+ const current = reasons.get(call.candidateId) ?? [];
1190
+ if (!current.includes(reason)) current.push(reason);
1191
+ reasons.set(call.candidateId, current);
1192
+ };
1193
+
1194
+ const exact = request.toolCallId
1195
+ ? calls.find((call) => call.id === request.toolCallId)
1196
+ : undefined;
1197
+ if (exact) addReason(exact, "exact-tool-call");
1198
+ for (const unit of relevant) addReason(unit.call, unit.reason);
1199
+ for (const call of calls) {
1200
+ if (call.entryIndex < turnStart || call === exact) continue;
1201
+ if (exactStructuredMatch(call, request)) {
1202
+ addReason(call, "structured-request-match");
1203
+ }
1204
+ }
1205
+ const profile = surfaceProfile(request);
1206
+ const securityCalls = calls.filter(
1207
+ (call) =>
1208
+ call.entryIndex >= turnStart &&
1209
+ securityEvidenceCategory(call) !== undefined &&
1210
+ ["network", "git-push", "forwarded"].includes(profile),
1211
+ );
1212
+ for (const call of securityCalls.slice(-4)) {
1213
+ addReason(call, "security-combination");
1214
+ }
1215
+
1216
+ const priority: EvidenceSelectionReason[] = [
1217
+ "exact-tool-call",
1218
+ "same-tool",
1219
+ "delete-precheck",
1220
+ "git-push-context",
1221
+ "provider-branch-protection",
1222
+ "security-combination",
1223
+ "structured-request-match",
1224
+ ];
1225
+ const selected = evidence
1226
+ .filter((item) => item.kind === "tool" && reasons.has(item.id))
1227
+ .map((item) => {
1228
+ const matched = reasons.get(item.id)!;
1229
+ const ordered = [...matched].sort(
1230
+ (left, right) => priority.indexOf(left) - priority.indexOf(right),
1231
+ );
1232
+ const call = calls.find((candidate) => candidate.candidateId === item.id)!;
1233
+ const linkage = exactToolCallReviewerRepresentation(
1234
+ call,
1235
+ request,
1236
+ ordered[0],
1237
+ );
1238
+ return {
1239
+ ...item,
1240
+ ...(linkage
1241
+ ? { text: linkage, representationCharacters: linkage.length }
1242
+ : {}),
1243
+ reason: ordered[0],
1244
+ secondaryReasons: ordered.slice(1),
1245
+ sensitivity:
1246
+ ordered.includes("security-combination")
1247
+ ? ({
1248
+ credential: "credential",
1249
+ permission: "permission-control",
1250
+ authorization: "authorization-persistence",
1251
+ transport: "transport-weakening",
1252
+ audit: "audit-control",
1253
+ "security-control": "security-control",
1254
+ } as const)[
1255
+ securityEvidenceCategory(
1256
+ calls.find((call) => call.candidateId === item.id)!,
1257
+ )!
1258
+ ]
1259
+ : item.sensitivity,
1260
+ };
1261
+ });
1262
+ return {
1263
+ selected,
1264
+ criticalEvidenceOverflow: securityCalls.length > 4,
1265
+ };
1266
+ }
1267
+
1268
+ function capRelevantUnits(
1269
+ units: ReturnType<typeof relevantResultEvidence>,
1270
+ profile: EvidenceSurfaceProfile,
1271
+ ): ReturnType<typeof relevantResultEvidence> {
1272
+ const newest = [...units].sort((left, right) => right.index - left.index);
1273
+ const selected: typeof units = [];
1274
+ const take = (
1275
+ reason: (typeof units)[number]["reason"],
1276
+ limit: number,
1277
+ predicate: (unit: (typeof units)[number]) => boolean = () => true,
1278
+ ) => {
1279
+ for (const unit of newest) {
1280
+ if (
1281
+ selected.length >= units.length ||
1282
+ selected.includes(unit) ||
1283
+ unit.reason !== reason ||
1284
+ !predicate(unit) ||
1285
+ selected.filter(
1286
+ (candidate) => candidate.reason === reason && predicate(candidate),
1287
+ ).length >= limit
1288
+ ) {
1289
+ continue;
1290
+ }
1291
+ selected.push(unit);
1292
+ }
1293
+ };
1294
+ take("same-tool", 1);
1295
+ if (profile === "delete") take("delete-precheck", 2);
1296
+ if (profile === "git-push") {
1297
+ take("git-push-context", 1, (unit) =>
1298
+ /^\s*git\s+(?:remote|config\s+--get\s+remote)/i.test(
1299
+ commandArgument(unit.call),
1300
+ ),
1301
+ );
1302
+ take("git-push-context", 1, (unit) =>
1303
+ /^\s*git\s+(?:branch|status|rev-parse)/i.test(
1304
+ commandArgument(unit.call),
1305
+ ),
1306
+ );
1307
+ take("provider-branch-protection", 1);
1308
+ }
1309
+ if (profile === "generic") {
1310
+ for (const unit of newest) {
1311
+ if (!selected.includes(unit) && selected.length < 4) selected.push(unit);
1312
+ }
1313
+ }
1314
+ return selected.sort((left, right) => left.index - right.index);
1315
+ }
1316
+
1317
+ function selectedMetadata(
1318
+ item: Evidence,
1319
+ original: Evidence,
1320
+ profile: EvidenceSurfaceProfile,
1321
+ ): EvidenceCandidateMetadata {
1322
+ return {
1323
+ id: item.id,
1324
+ entryIndex: item.entryIndex,
1325
+ kind: item.kind === "user" ? "user" : "tool-call",
1326
+ ...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
1327
+ reason: item.reason,
1328
+ secondaryReasons: [...item.secondaryReasons],
1329
+ surfaceProfile: profile,
1330
+ originalCharacters: original.originalCharacters,
1331
+ selectedCharacters: item.text.length,
1332
+ estimatedTokens: Buffer.byteLength(item.text, "utf8"),
1333
+ truncated:
1334
+ original.preTruncated ||
1335
+ item.text.length <
1336
+ (item.representationCharacters ?? original.originalCharacters),
1337
+ sensitivity: item.sensitivity,
1338
+ };
1339
+ }
1340
+
1341
+ type UserSelection = {
1342
+ selected: Evidence[];
1343
+ truncated: boolean;
1344
+ authorizationCeiling: TranscriptResult["userAuthorizationCeiling"];
1345
+ constraint: TranscriptResult["userConstraint"];
1346
+ compactionState: TranscriptResult["compactionState"];
1347
+ };
1348
+
1349
+ function utf8Prefix(value: string, maxBytes: number): string {
1350
+ if (maxBytes <= 0) return "";
1351
+ let bytes = 0;
1352
+ let result = "";
1353
+ for (const character of value) {
1354
+ const size = Buffer.byteLength(character, "utf8");
1355
+ if (bytes + size > maxBytes) break;
1356
+ result += character;
1357
+ bytes += size;
1358
+ }
1359
+ return result;
1360
+ }
1361
+
1362
+ function utf8Suffix(value: string, maxBytes: number): string {
1363
+ if (maxBytes <= 0) return "";
1364
+ let bytes = 0;
1365
+ const characters = [...value];
1366
+ let result = "";
1367
+ for (let index = characters.length - 1; index >= 0; index--) {
1368
+ const character = characters[index];
1369
+ const size = Buffer.byteLength(character, "utf8");
1370
+ if (bytes + size > maxBytes) break;
1371
+ result = character + result;
1372
+ bytes += size;
1373
+ }
1374
+ return result;
1375
+ }
1376
+
1377
+ function headTail(value: string, limit: number): string {
1378
+ if (Buffer.byteLength(value, "utf8") <= limit) return value;
1379
+ const marker = "\n…[middle truncated]…\n";
1380
+ const markerBytes = Buffer.byteLength(marker, "utf8");
1381
+ if (limit <= markerBytes) return utf8Prefix(marker, limit);
1382
+ const available = limit - markerBytes;
1383
+ const head = Math.ceil(available / 2);
1384
+ return `${utf8Prefix(value, head)}${marker}${utf8Suffix(value, available - head)}`;
1385
+ }
1386
+
1387
+ function summaryEntryIndices(entries: readonly unknown[]): number[] {
1388
+ const indices: number[] = [];
1389
+ entries.forEach((entry, index) => {
1390
+ const role = messageRecord(entry)?.role;
1391
+ if (role === "compactionSummary" || role === "branchSummary") {
1392
+ indices.push(index);
1393
+ }
1394
+ });
1395
+ return indices;
1396
+ }
1397
+
1398
+ function containsExactIdentifier(text: string, value: string): boolean {
1399
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1400
+ return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}(?=$|[^A-Za-z0-9_-])`).test(
1401
+ text,
1402
+ );
1403
+ }
1404
+
1405
+ function exactUserReference(
1406
+ text: string,
1407
+ request: RelevantBoundaryRequest,
1408
+ ): boolean {
1409
+ return [
1410
+ request.id,
1411
+ request.toolCallId,
1412
+ request.requesterSessionId,
1413
+ request.agentName,
1414
+ ].some((value) => {
1415
+ if (typeof value !== "string" || value.length === 0) return false;
1416
+ return containsExactIdentifier(text, value);
1417
+ });
1418
+ }
1419
+
1420
+
1421
+ function selectUserEvidence(
1422
+ entries: readonly unknown[],
1423
+ evidence: Evidence[],
1424
+ request: RelevantBoundaryRequest,
1425
+ budgetTokens: number,
1426
+ ): UserSelection {
1427
+ const users = evidence.filter((item) => item.kind === "user");
1428
+ const summaries = summaryEntryIndices(entries);
1429
+ const lastSummary = summaries.at(-1) ?? -1;
1430
+ const rawAfterSummary = users.filter((item) => item.entryIndex > lastSummary);
1431
+ const eligibleUsers = lastSummary >= 0 ? rawAfterSummary : users;
1432
+ const latest = eligibleUsers.at(-1);
1433
+ const compactionState: TranscriptResult["compactionState"] =
1434
+ summaries.length === 0
1435
+ ? "none"
1436
+ : latest
1437
+ ? "summary-present"
1438
+ : "authorization-unavailable";
1439
+ if (!latest || budgetTokens <= 0) {
1440
+ return {
1441
+ selected: [],
1442
+ truncated: users.length > 0,
1443
+ authorizationCeiling: request.trustedRetryOriginalRequestId
1444
+ ? "high"
1445
+ : "unknown",
1446
+ constraint: "none",
1447
+ compactionState,
1448
+ };
1449
+ }
1450
+
1451
+ let remaining = budgetTokens;
1452
+ const latestText = headTail(latest.text, remaining);
1453
+ const latestTrustedRetry = Boolean(
1454
+ request.trustedRetryOriginalRequestId &&
1455
+ latest.text.includes("I approved one reviewer retry") &&
1456
+ containsExactIdentifier(
1457
+ latest.text,
1458
+ request.trustedRetryOriginalRequestId,
1459
+ ),
1460
+ );
1461
+ const latestExactReference = exactUserReference(latest.text, request);
1462
+ const selected: Evidence[] = [{
1463
+ ...latest,
1464
+ text: latestText,
1465
+ reason: "latest-user",
1466
+ secondaryReasons: [
1467
+ ...(latestTrustedRetry ? ["trusted-retry-user-message" as const] : []),
1468
+ ...(latestExactReference ? ["exact-request-reference" as const] : []),
1469
+ ],
1470
+ }];
1471
+ remaining -= Buffer.byteLength(latestText, "utf8");
1472
+
1473
+ const older = eligibleUsers.slice(0, -1).reverse();
1474
+ for (const candidate of older) {
1475
+ if (remaining <= 0) break;
1476
+ const trustedRetry = Boolean(
1477
+ request.trustedRetryOriginalRequestId &&
1478
+ candidate.text.includes("I approved one reviewer retry") &&
1479
+ containsExactIdentifier(
1480
+ candidate.text,
1481
+ request.trustedRetryOriginalRequestId,
1482
+ ),
1483
+ );
1484
+ const exactReference = exactUserReference(candidate.text, request);
1485
+ if (!trustedRetry && !exactReference) continue;
1486
+ const reason: EvidenceSelectionReason = trustedRetry
1487
+ ? "trusted-retry-user-message"
1488
+ : "exact-request-reference";
1489
+ const text = headTail(candidate.text, remaining);
1490
+ if (!text) continue;
1491
+ selected.push({
1492
+ ...candidate,
1493
+ text,
1494
+ reason,
1495
+ secondaryReasons: [
1496
+ ...(trustedRetry && exactReference
1497
+ ? ["exact-request-reference" as const]
1498
+ : []),
1499
+ ],
1500
+ });
1501
+ remaining -= Buffer.byteLength(text, "utf8");
1502
+ }
1503
+ selected.sort((left, right) => left.entryIndex - right.entryIndex);
1504
+ const latestTruncated =
1505
+ latestText.length < latest.originalCharacters || latest.preTruncated;
1506
+ return {
1507
+ selected,
1508
+ truncated:
1509
+ latestTruncated ||
1510
+ selected.some((item) => item.text.length < item.originalCharacters),
1511
+ authorizationCeiling: "high",
1512
+ constraint: "none",
1513
+ compactionState,
1514
+ };
791
1515
  }
792
1516
 
793
1517
  function selectEvidence(
794
1518
  evidence: Evidence[],
795
1519
  kind: Evidence["kind"],
796
- budgetCharacters: number,
1520
+ budgetTokens: number,
797
1521
  ): { selected: Evidence[]; truncated: boolean } {
798
1522
  const candidates = evidence.filter((item) => item.kind === kind);
799
- if (candidates.length === 0 || budgetCharacters <= 0) {
1523
+ if (candidates.length === 0 || budgetTokens <= 0) {
800
1524
  return { selected: [], truncated: candidates.length > 0 };
801
1525
  }
802
1526
 
803
1527
  const selected = new Map<number, Evidence>();
804
- let remaining = budgetCharacters;
1528
+ let remaining = budgetTokens;
805
1529
  const add = (item: Evidence, limit = remaining): void => {
806
1530
  if (selected.has(item.index) || remaining <= 0) return;
807
- const text = item.text.slice(0, Math.min(remaining, limit));
1531
+ const text = utf8Prefix(item.text, Math.min(remaining, limit));
808
1532
  if (!text) return;
809
1533
  selected.set(item.index, { ...item, text });
810
- remaining -= text.length;
1534
+ remaining -= Buffer.byteLength(text, "utf8");
811
1535
  };
812
1536
 
813
1537
  // Keep the original user intent as an anchor, then fill from newest to oldest.
814
1538
  if (kind === "user") {
815
1539
  const firstBudget =
816
1540
  candidates.length > 1
817
- ? Math.max(1, Math.floor(budgetCharacters / 2))
818
- : budgetCharacters;
1541
+ ? Math.max(1, Math.floor(budgetTokens / 2))
1542
+ : budgetTokens;
819
1543
  add(candidates[0], firstBudget);
820
1544
  if (candidates.length > 1) add(candidates[candidates.length - 1]);
821
1545
  }
@@ -841,68 +1565,226 @@ export function buildClassifierTranscript(
841
1565
  request: RelevantBoundaryRequest = {},
842
1566
  ): TranscriptResult {
843
1567
  const evidence = extractEvidence(entries);
844
- const users = selectEvidence(
1568
+ const profile = surfaceProfile(request);
1569
+ const turnStart = currentTurnStart(entries);
1570
+ const relevantUnits = capRelevantUnits(
1571
+ relevantResultEvidence(entries, request, turnStart),
1572
+ profile,
1573
+ );
1574
+ const users = selectUserEvidence(
1575
+ entries,
845
1576
  evidence,
846
- "user",
847
- config.maxUserTranscriptTokens * 4,
1577
+ request,
1578
+ config.maxUserTranscriptTokens,
848
1579
  );
849
- const tools = selectEvidence(
1580
+ const requestAwareTools = selectRequestAwareTools(
1581
+ entries,
850
1582
  evidence,
851
- "tool",
852
- config.maxToolTranscriptTokens * 4,
1583
+ request,
1584
+ relevantUnits,
1585
+ turnStart,
853
1586
  );
854
- const selected = [...users.selected, ...tools.selected].sort(
855
- (left, right) => left.index - right.index,
1587
+ const tools = selectEvidence(
1588
+ requestAwareTools.selected,
1589
+ "tool",
1590
+ config.maxToolTranscriptTokens,
856
1591
  );
857
- const baseRendered = selected
858
- .map(
859
- (item) =>
860
- `<${item.kind}>\n${escapeEvidenceMarkup(item.text)}\n</${item.kind}>`,
861
- )
862
- .join("\n\n");
863
1592
  const relevantBudget =
864
- (config.maxRelevantResultTokens ?? config.maxToolTranscriptTokens) * 4;
1593
+ config.maxRelevantResultTokens ?? config.maxToolTranscriptTokens;
1594
+ const sandboxSupplement = sandboxTrapSupplement(request);
1595
+ const sandboxEvidence = sandboxTrapEvidence(request);
865
1596
  const relevantCandidates = [
866
- ...(sandboxTrapEvidence(request)
867
- ? [{ index: Number.MAX_SAFE_INTEGER - 1, text: sandboxTrapEvidence(request)! }]
1597
+ ...(sandboxEvidence && sandboxSupplement
1598
+ ? [{
1599
+ index: Number.MAX_SAFE_INTEGER - 1,
1600
+ id: `sandbox-trap:${request.id ?? "request"}`,
1601
+ reason: "sandbox-trap" as const,
1602
+ toolCallId: undefined,
1603
+ text: sandboxEvidence,
1604
+ content: sandboxSupplement,
1605
+ rawCharacters: sandboxEvidence.length,
1606
+ sensitivity: "boundary" as const,
1607
+ }]
868
1608
  : []),
869
- ...relevantResultEvidence(entries, request),
1609
+ ...relevantUnits
1610
+ .filter((unit) =>
1611
+ tools.selected.some((tool) => tool.id === unit.call.candidateId),
1612
+ )
1613
+ .map((unit) => ({
1614
+ index: unit.index,
1615
+ id: unit.resultId,
1616
+ reason: unit.reason,
1617
+ toolCallId: unit.call.id,
1618
+ text: unit.renderedResult,
1619
+ content: unit.resultText,
1620
+ rawCharacters: unit.renderedResult.length,
1621
+ sensitivity: "tool-output" as const,
1622
+ })),
870
1623
  ];
871
1624
  let relevantRemaining = relevantBudget;
872
- const relevantSelected: string[] = [];
873
- for (let index = relevantCandidates.length - 1; index >= 0; index--) {
1625
+ let requiredProfileOverflow = false;
1626
+ const relevantSelected: Array<
1627
+ (typeof relevantCandidates)[number] & { selectedText: string }
1628
+ > = [];
1629
+ const relevantSelectionOrder = [
1630
+ ...relevantCandidates.filter((candidate) => candidate.reason === "sandbox-trap"),
1631
+ ...relevantCandidates
1632
+ .filter((candidate) => candidate.reason !== "sandbox-trap")
1633
+ .sort((left, right) => right.index - left.index),
1634
+ ];
1635
+ for (const candidate of relevantSelectionOrder) {
874
1636
  if (relevantRemaining <= 0) break;
875
- const text = relevantCandidates[index].text.slice(0, relevantRemaining);
1637
+ if (
1638
+ candidate.reason === "sandbox-trap" &&
1639
+ Buffer.byteLength(candidate.text, "utf8") > relevantRemaining
1640
+ ) {
1641
+ requiredProfileOverflow = true;
1642
+ break;
1643
+ }
1644
+ const text = utf8Prefix(candidate.text, relevantRemaining);
876
1645
  if (text) {
877
- relevantSelected.unshift(text);
878
- relevantRemaining -= text.length;
1646
+ relevantSelected.push({ ...candidate, selectedText: text });
1647
+ relevantRemaining -= Buffer.byteLength(text, "utf8");
879
1648
  }
880
1649
  }
881
- const rendered = [baseRendered, ...relevantSelected]
1650
+ relevantSelected.sort((left, right) => left.index - right.index);
1651
+ const pairedToolCallIds = new Set(
1652
+ relevantSelected
1653
+ .map((candidate) => candidate.toolCallId)
1654
+ .filter((id): id is string => Boolean(id)),
1655
+ );
1656
+ const resultReasons = new Set<EvidenceSelectionReason>([
1657
+ "same-tool",
1658
+ "delete-precheck",
1659
+ "git-push-context",
1660
+ "provider-branch-protection",
1661
+ ]);
1662
+ const finalTools = tools.selected.filter((tool) => {
1663
+ if (!resultReasons.has(tool.reason)) return true;
1664
+ if (tool.toolCallId && pairedToolCallIds.has(tool.toolCallId)) return true;
1665
+ return tool.secondaryReasons.some((reason) => !resultReasons.has(reason));
1666
+ });
1667
+ const selected = [...users.selected, ...finalTools].sort(
1668
+ (left, right) => left.index - right.index,
1669
+ );
1670
+ const baseRendered = selected
1671
+ .map((item) => {
1672
+ const attributes = item.kind === "tool"
1673
+ ? ` id="${escapeEvidenceMarkup(item.toolCallId ?? item.id)}" reason="${item.reason}"`
1674
+ : ` reason="${item.reason}"`;
1675
+ return `<${item.kind}${attributes}>\n${escapeEvidenceMarkup(item.text)}\n</${item.kind}>`;
1676
+ })
1677
+ .join("\n\n");
1678
+ const authorizationNotice =
1679
+ users.compactionState === "authorization-unavailable"
1680
+ ? "[Original user authorization is unavailable after compaction. Agent-generated summaries are not authorization.]"
1681
+ : users.compactionState === "summary-present"
1682
+ ? "[A compaction/branch summary exists but is not user authorization.]"
1683
+ : "";
1684
+ const rendered = [
1685
+ authorizationNotice,
1686
+ baseRendered,
1687
+ ...relevantSelected.map((candidate) => candidate.selectedText),
1688
+ ]
882
1689
  .filter(Boolean)
883
1690
  .join("\n\n");
884
1691
  const relevantTruncated =
885
1692
  relevantSelected.length < relevantCandidates.length ||
886
- relevantSelected.reduce((total, value) => total + value.length, 0) <
1693
+ relevantSelected.reduce(
1694
+ (total, value) => total + value.selectedText.length,
1695
+ 0,
1696
+ ) <
887
1697
  relevantCandidates.reduce((total, value) => total + value.text.length, 0);
888
- const truncated = users.truncated || tools.truncated || relevantTruncated;
1698
+ const truncated =
1699
+ users.truncated ||
1700
+ tools.truncated ||
1701
+ finalTools.length < tools.selected.length ||
1702
+ relevantTruncated;
889
1703
  const text = truncated
890
1704
  ? `[Some transcript evidence was omitted or truncated.]\n\n${rendered}`
891
1705
  : rendered;
892
1706
  return {
893
1707
  text: text || "(no eligible transcript evidence)",
1708
+ surfaceProfile: profile,
1709
+ reviewerEvidence: {
1710
+ userMessages: users.selected.map((item) => ({
1711
+ id: item.id,
1712
+ reason: item.reason,
1713
+ secondaryReasons: [...item.secondaryReasons],
1714
+ content: item.text,
1715
+ })),
1716
+ toolCalls: finalTools.map((item) => ({
1717
+ id: item.id,
1718
+ reason: item.reason,
1719
+ secondaryReasons: [...item.secondaryReasons],
1720
+ ...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
1721
+ content: item.text,
1722
+ })),
1723
+ relevantResults: relevantSelected.map((item) => ({
1724
+ id: item.id,
1725
+ reason: item.reason,
1726
+ secondaryReasons: [],
1727
+ ...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
1728
+ content: item.selectedText === item.text
1729
+ ? item.content
1730
+ : item.selectedText,
1731
+ })),
1732
+ },
1733
+ budgetRemovals: [],
894
1734
  userCharacters: users.selected.reduce(
895
1735
  (total, item) => total + item.text.length,
896
1736
  0,
897
1737
  ),
898
- toolCharacters: tools.selected.reduce(
1738
+ toolCharacters: finalTools.reduce(
899
1739
  (total, item) => total + item.text.length,
900
1740
  0,
901
1741
  ),
902
1742
  relevantResultCharacters: relevantSelected.reduce(
903
- (total, item) => total + item.length,
1743
+ (total, item) =>
1744
+ total +
1745
+ (item.selectedText === item.text
1746
+ ? item.content.length
1747
+ : item.selectedText.length),
904
1748
  0,
905
1749
  ),
906
1750
  truncated,
1751
+ selectedCandidates: [
1752
+ ...users.selected.map((item) =>
1753
+ selectedMetadata(
1754
+ item,
1755
+ evidence.find((candidate) => candidate.id === item.id)!,
1756
+ profile,
1757
+ ),
1758
+ ),
1759
+ ...finalTools.map((item) =>
1760
+ selectedMetadata(
1761
+ item,
1762
+ evidence.find((candidate) => candidate.id === item.id)!,
1763
+ profile,
1764
+ ),
1765
+ ),
1766
+ ...relevantSelected.map((item) => ({
1767
+ id: item.id,
1768
+ entryIndex: item.index,
1769
+ kind: item.sensitivity === "boundary" ? "sandbox-trap" as const : "tool-result" as const,
1770
+ ...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
1771
+ reason: item.reason,
1772
+ secondaryReasons: [],
1773
+ surfaceProfile: profile,
1774
+ originalCharacters: item.rawCharacters,
1775
+ selectedCharacters: item.selectedText.length,
1776
+ estimatedTokens: Buffer.byteLength(item.selectedText, "utf8"),
1777
+ truncated: item.selectedText.length < item.rawCharacters,
1778
+ sensitivity: item.sensitivity,
1779
+ })),
1780
+ ].sort((left, right) => left.entryIndex - right.entryIndex),
1781
+ ...(requestAwareTools.criticalEvidenceOverflow
1782
+ ? { failureCode: "critical_evidence_overflow" as const }
1783
+ : requiredProfileOverflow
1784
+ ? { failureCode: "required_profile_overflow" as const }
1785
+ : {}),
1786
+ userAuthorizationCeiling: users.authorizationCeiling,
1787
+ userConstraint: users.constraint,
1788
+ compactionState: users.compactionState,
907
1789
  };
908
1790
  }