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