@diegopetrucci/pi-extensions 0.1.76 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +277 -44
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/git.ts +0 -8
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/web/index.html +3 -3
- package/extensions/code-reviewer/README.md +5 -5
- package/extensions/code-reviewer/index.ts +258 -54
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/index.ts +2 -8
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +3 -3
- package/extensions/context-cap/package.json +2 -2
- package/extensions/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +47 -40
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +0 -4
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +5 -5
- package/extensions/dynamic-context-pruning/index.ts +114 -232
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +2 -2
- package/extensions/fast/package.json +1 -1
- package/extensions/inline-bash/index.ts +0 -16
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +247 -39
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +3 -10
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/README.md +4 -3
- package/extensions/oracle/index.ts +45 -38
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +151 -28
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/project-mcp-json/README.md +2 -2
- package/extensions/project-mcp-json/package.json +1 -1
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/README.md +30 -19
- package/extensions/quiet-tools/index.ts +233 -166
- package/extensions/quiet-tools/package.json +2 -2
- package/extensions/todo/index.ts +0 -19
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/README.md +4 -2
- package/extensions/triage-comments/index.ts +349 -29
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +4 -4
|
@@ -7,10 +7,7 @@ import {
|
|
|
7
7
|
type ExtensionContext,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
// Types (kept intentionally minimal/structural so pure helpers stay testable
|
|
12
|
-
// without depending on the full @earendil-works/pi-ai type graph).
|
|
13
|
-
// ============================================================================
|
|
10
|
+
// Structural stand-ins so pure helpers stay testable without the full pi-ai type graph.
|
|
14
11
|
|
|
15
12
|
export interface MinimalTextContent {
|
|
16
13
|
type: "text";
|
|
@@ -149,21 +146,17 @@ export function sessionProjectionToMessages(
|
|
|
149
146
|
return projection.entries.flatMap((entry) => entry.messages);
|
|
150
147
|
}
|
|
151
148
|
|
|
152
|
-
// ============================================================================
|
|
153
|
-
// Constants
|
|
154
|
-
// ============================================================================
|
|
155
|
-
|
|
156
149
|
const EXTENSION_ID = "dynamic-context-pruning";
|
|
157
150
|
const CONFIG_FILE_NAME = "dynamic-context-pruning.json";
|
|
158
151
|
const DECISION_ENTRY_TYPE = "dynamic-context-pruning:decision";
|
|
159
152
|
const STATS_ENTRY_TYPE = "dynamic-context-pruning:stats";
|
|
160
153
|
/**
|
|
161
|
-
* Restore/undo entries
|
|
162
|
-
*
|
|
163
|
-
* `resolvePruneTombstoneState` for the
|
|
154
|
+
* Restore/undo entries: a lightweight tombstone that records "the decision
|
|
155
|
+
* with this idempotencyKey should be treated as reverted". See
|
|
156
|
+
* `resolvePruneTombstoneState` for the chronological last-event-wins rule.
|
|
164
157
|
*/
|
|
165
158
|
const RESTORE_ENTRY_TYPE = "dynamic-context-pruning:restore";
|
|
166
|
-
/** strategyId used for user-initiated prunes from the /prune picker
|
|
159
|
+
/** strategyId used for user-initiated prunes from the /prune picker. Always source:"manual". */
|
|
167
160
|
const MANUAL_STRATEGY_ID = "manual";
|
|
168
161
|
|
|
169
162
|
const DEFAULT_RECENT_TURNS = 4;
|
|
@@ -173,7 +166,7 @@ const DEFAULT_CACHED_PRICE_RATIO = 0.1;
|
|
|
173
166
|
|
|
174
167
|
/**
|
|
175
168
|
* Default net-benefit gate threshold (max amortization calls). Recalibrated
|
|
176
|
-
*
|
|
169
|
+
* from the representative-corpus benchmark (~/.the-last-harness/
|
|
177
170
|
* agent/sessions, 1,390+ session files, 556 candidates): this is the
|
|
178
171
|
* hindsight-optimal break-even T at cachedPriceRatio r=0.1 (aggressive
|
|
179
172
|
* prompt caching, the common Anthropic case). IMPORTANT caveats:
|
|
@@ -189,7 +182,7 @@ const DEFAULT_CACHED_PRICE_RATIO = 0.1;
|
|
|
189
182
|
const DEFAULT_BREAK_EVEN_THRESHOLD = 22;
|
|
190
183
|
|
|
191
184
|
/**
|
|
192
|
-
* Default mid_loop break-even threshold
|
|
185
|
+
* Default mid_loop break-even threshold. The package's own
|
|
193
186
|
* representative-corpus benchmark (turn-START state definition — the same one
|
|
194
187
|
* the runtime `context` handler uses) favored rejecting mid_loop candidates
|
|
195
188
|
* at cachedPriceRatio r=0.1 (mid_loop-optimal T=1, total realized
|
|
@@ -240,10 +233,6 @@ const DEFAULT_ERROR_PURGE_MIN_TURNS_OLD = 4;
|
|
|
240
233
|
const DEDUPE_STRATEGY_ID = "dedupe";
|
|
241
234
|
const ERROR_PURGE_STRATEGY_ID = "error-purge";
|
|
242
235
|
|
|
243
|
-
// ============================================================================
|
|
244
|
-
// Config
|
|
245
|
-
// ============================================================================
|
|
246
|
-
|
|
247
236
|
export interface PruneProtections {
|
|
248
237
|
/** Tool names (case-insensitive) whose calls are never pruned. */
|
|
249
238
|
toolNames: string[];
|
|
@@ -257,7 +246,7 @@ export interface PruneThresholds {
|
|
|
257
246
|
/**
|
|
258
247
|
* Minimum raw text characters a fresh AUTOMATIC prune proposal must remove
|
|
259
248
|
* (original text chars minus placeholder chars) to be considered at all
|
|
260
|
-
*
|
|
249
|
+
* Enforced by `runDynamicContextPruningPipeline` BEFORE the
|
|
261
250
|
* net-benefit gate (see `PruneGateConfig`/`gate`, which uses break-even token
|
|
262
251
|
* math and is a separate, later check): text-only candidates below this floor
|
|
263
252
|
* are dropped up front and never reach gate/batch consideration. Image-bearing
|
|
@@ -269,7 +258,7 @@ export interface PruneThresholds {
|
|
|
269
258
|
minCharsSaved: number;
|
|
270
259
|
}
|
|
271
260
|
|
|
272
|
-
/** Per-strategy toggles/config
|
|
261
|
+
/** Per-strategy toggles/config. Each strategy can be disabled independently. */
|
|
273
262
|
export interface StrategiesConfig {
|
|
274
263
|
dedupe: { enabled: boolean };
|
|
275
264
|
errorPurge: {
|
|
@@ -277,11 +266,10 @@ export interface StrategiesConfig {
|
|
|
277
266
|
/** A tool call's input is only eligible for purging once it is older than this many turns. */
|
|
278
267
|
minTurnsOld: number;
|
|
279
268
|
};
|
|
280
|
-
/** Superseded file-ops strategy
|
|
269
|
+
/** Superseded file-ops strategy: stale read/write/edit outputs replaced by a placeholder. */
|
|
281
270
|
supersededFileOps: { enabled: boolean };
|
|
282
271
|
}
|
|
283
272
|
|
|
284
|
-
/** Net-benefit gate operating mode (pe-s2ho). */
|
|
285
273
|
export type GateMode = "on" | "off" | "always-apply";
|
|
286
274
|
|
|
287
275
|
/**
|
|
@@ -484,11 +472,6 @@ async function writeConfig(config: DynamicContextPruningConfig): Promise<void> {
|
|
|
484
472
|
await fs.writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
485
473
|
}
|
|
486
474
|
|
|
487
|
-
// ============================================================================
|
|
488
|
-
// Token estimator (chars/4-style; deliberately simple, good enough for
|
|
489
|
-
// relative before/after accounting used by later savings-accounting work).
|
|
490
|
-
// ============================================================================
|
|
491
|
-
|
|
492
475
|
export function estimateTokensForText(text: string): number {
|
|
493
476
|
if (!text) return 0;
|
|
494
477
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
@@ -510,7 +493,7 @@ export function estimateTokensForContent(
|
|
|
510
493
|
export type TokenEstimator = (text: string) => number;
|
|
511
494
|
|
|
512
495
|
/**
|
|
513
|
-
* Classify the agent's current state from the message payload alone
|
|
496
|
+
* Classify the agent's current state from the message payload alone:
|
|
514
497
|
* this is the runtime-observable definition used both by the live `context`
|
|
515
498
|
* event handler and (via re-export) by the offline benchmark, so the two can
|
|
516
499
|
* never drift apart.
|
|
@@ -527,7 +510,7 @@ export type TokenEstimator = (text: string) => number;
|
|
|
527
510
|
* default: nothing suggests an in-progress loop).
|
|
528
511
|
*
|
|
529
512
|
* The benchmark (scripts/benchmark.mjs) reclassifies its candidate labels to
|
|
530
|
-
* this SAME definition
|
|
513
|
+
* this SAME definition, reusing this function directly, so the
|
|
531
514
|
* defaults below rest on evidence measured with the exact definition the
|
|
532
515
|
* runtime observes. (An earlier turn-end-based benchmark definition -- "idle"
|
|
533
516
|
* meant "this IS the turn's last assistant message", only knowable in
|
|
@@ -544,7 +527,7 @@ export function classifyAgentStateFromMessages(messages: MinimalMessage[]): Agen
|
|
|
544
527
|
return "idle";
|
|
545
528
|
}
|
|
546
529
|
|
|
547
|
-
/** Resolve which break-even threshold applies for a given agent state
|
|
530
|
+
/** Resolve which break-even threshold applies for a given agent state. */
|
|
548
531
|
export function resolveBreakEvenThreshold(
|
|
549
532
|
gateConfig: PruneGateConfig,
|
|
550
533
|
agentState: AgentState,
|
|
@@ -578,7 +561,7 @@ function estimateMessageTokens(message: MinimalMessage, estimateTokens: TokenEst
|
|
|
578
561
|
|
|
579
562
|
/**
|
|
580
563
|
* Sum estimated tokens for messages from `fromIndex` to the end of the array.
|
|
581
|
-
* Used both for cache-bust tail sizing
|
|
564
|
+
* Used both for cache-bust tail sizing and for raw/
|
|
582
565
|
* effective context-size snapshots. Clamped to valid indices; an out-of-range
|
|
583
566
|
* `fromIndex` (e.g. the very last message, or beyond) degrades gracefully.
|
|
584
567
|
*/
|
|
@@ -595,10 +578,6 @@ export function estimateTailTokens(
|
|
|
595
578
|
return total;
|
|
596
579
|
}
|
|
597
580
|
|
|
598
|
-
// ============================================================================
|
|
599
|
-
// Protections
|
|
600
|
-
// ============================================================================
|
|
601
|
-
|
|
602
581
|
function globToRegExp(glob: string): RegExp {
|
|
603
582
|
let pattern = "^";
|
|
604
583
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -706,10 +685,6 @@ export function computeTurnsElapsedSince(messages: MinimalMessage[], index: numb
|
|
|
706
685
|
return count;
|
|
707
686
|
}
|
|
708
687
|
|
|
709
|
-
// ============================================================================
|
|
710
|
-
// Correlation
|
|
711
|
-
// ============================================================================
|
|
712
|
-
|
|
713
688
|
export interface ToolCallPairIndices {
|
|
714
689
|
assistantIndex?: number;
|
|
715
690
|
toolCallBlockIndex?: number;
|
|
@@ -722,7 +697,7 @@ export interface ToolCallPairIndices {
|
|
|
722
697
|
* toolCallIds against the same `messages` array (e.g. the pipeline's
|
|
723
698
|
* per-decision protection/savings/apply passes) should build this once and
|
|
724
699
|
* pass it to `findToolCallPairIndices` and friends to avoid an O(n) rescan
|
|
725
|
-
* per decision
|
|
700
|
+
* per decision.
|
|
726
701
|
*/
|
|
727
702
|
export function buildToolCallPairIndex(
|
|
728
703
|
messages: MinimalMessage[],
|
|
@@ -798,10 +773,6 @@ export function findToolCallPairIndices(
|
|
|
798
773
|
return pair;
|
|
799
774
|
}
|
|
800
775
|
|
|
801
|
-
// ============================================================================
|
|
802
|
-
// Proposals & persisted decisions
|
|
803
|
-
// ============================================================================
|
|
804
|
-
|
|
805
776
|
export type PruneTargetKind = "tool_result_content" | "tool_call_input";
|
|
806
777
|
|
|
807
778
|
/**
|
|
@@ -826,7 +797,7 @@ export interface ProposedPrune {
|
|
|
826
797
|
export interface PruneDecisionRecord {
|
|
827
798
|
idempotencyKey: string;
|
|
828
799
|
strategyId: string;
|
|
829
|
-
correlation: { type: "toolCallId"; toolCallId: string }
|
|
800
|
+
correlation: { type: "toolCallId"; toolCallId: string };
|
|
830
801
|
kind: PruneTargetKind;
|
|
831
802
|
reason: string;
|
|
832
803
|
placeholder?: string;
|
|
@@ -843,15 +814,11 @@ export function buildIdempotencyKey(proposal: ProposedPrune): string {
|
|
|
843
814
|
* omitting `strategyId` (unlike `idempotencyKey`). Two decisions from
|
|
844
815
|
* different strategies that both replace the same tool result's content
|
|
845
816
|
* share this key even though their idempotencyKeys differ — that's exactly
|
|
846
|
-
* the overlap
|
|
817
|
+
* the overlap the pipeline collapses before gating/apply (see
|
|
847
818
|
* `runDynamicContextPruningPipeline`).
|
|
848
819
|
*/
|
|
849
820
|
function buildDecisionTargetKey(decision: PruneDecisionRecord): string {
|
|
850
|
-
|
|
851
|
-
decision.correlation.type === "toolCallId"
|
|
852
|
-
? decision.correlation.toolCallId
|
|
853
|
-
: decision.correlation.entryId;
|
|
854
|
-
return `${decision.kind}:${decision.correlation.type}:${correlationId}`;
|
|
821
|
+
return `${decision.kind}:${decision.correlation.type}:${decision.correlation.toolCallId}`;
|
|
855
822
|
}
|
|
856
823
|
|
|
857
824
|
export function proposalToDecisionRecord(
|
|
@@ -885,8 +852,6 @@ export function parseDecisionRecord(data: unknown): PruneDecisionRecord | undefi
|
|
|
885
852
|
let correlation: PruneDecisionRecord["correlation"] | undefined;
|
|
886
853
|
if (rawCorrelation?.type === "toolCallId" && typeof rawCorrelation.toolCallId === "string") {
|
|
887
854
|
correlation = { type: "toolCallId", toolCallId: rawCorrelation.toolCallId };
|
|
888
|
-
} else if (rawCorrelation?.type === "entryId" && typeof rawCorrelation.entryId === "string") {
|
|
889
|
-
correlation = { type: "entryId", entryId: rawCorrelation.entryId };
|
|
890
855
|
}
|
|
891
856
|
if (!correlation) return undefined;
|
|
892
857
|
|
|
@@ -902,10 +867,6 @@ export function parseDecisionRecord(data: unknown): PruneDecisionRecord | undefi
|
|
|
902
867
|
};
|
|
903
868
|
}
|
|
904
869
|
|
|
905
|
-
// ============================================================================
|
|
906
|
-
// Restore / undo (pe-8re9): tombstones that reverse a persisted decision
|
|
907
|
-
// ============================================================================
|
|
908
|
-
|
|
909
870
|
/**
|
|
910
871
|
* A restore record is a tombstone: it records that the decision identified by
|
|
911
872
|
* `idempotencyKey` should stop being applied, WITHOUT deleting or mutating
|
|
@@ -1013,10 +974,6 @@ export function resolvePruneTombstoneState(entries: MinimalSessionEntry[]): Prun
|
|
|
1013
974
|
return { activeDecisions, activeIdempotencyKeys, restoredKeys, lastDecisionByKey };
|
|
1014
975
|
}
|
|
1015
976
|
|
|
1016
|
-
// ============================================================================
|
|
1017
|
-
// Apply step (content-replacement only; never drops messages)
|
|
1018
|
-
// ============================================================================
|
|
1019
|
-
|
|
1020
977
|
export function buildPlaceholderText(reason: string, charsRemoved: number): string {
|
|
1021
978
|
return `[pruned by ${EXTENSION_ID}: ${reason} (${charsRemoved} chars removed)]`;
|
|
1022
979
|
}
|
|
@@ -1031,8 +988,7 @@ function isImageBearingToolResultTarget(
|
|
|
1031
988
|
decision: PruneDecisionRecord,
|
|
1032
989
|
pairIndex?: Map<string, ToolCallPairIndices>,
|
|
1033
990
|
): boolean {
|
|
1034
|
-
if (decision.kind !== "tool_result_content"
|
|
1035
|
-
return false;
|
|
991
|
+
if (decision.kind !== "tool_result_content") return false;
|
|
1036
992
|
const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
|
|
1037
993
|
if (pair.resultIndex === undefined) return false;
|
|
1038
994
|
const target = messages[pair.resultIndex];
|
|
@@ -1056,11 +1012,6 @@ export function applyPruneDecision(
|
|
|
1056
1012
|
decision: PruneDecisionRecord,
|
|
1057
1013
|
pairIndex?: Map<string, ToolCallPairIndices>,
|
|
1058
1014
|
): ApplyResult {
|
|
1059
|
-
if (decision.correlation.type !== "toolCallId") {
|
|
1060
|
-
// v1 only ever targets tool call/result pairs; entryId correlation is
|
|
1061
|
-
// reserved for future strategies and intentionally not applied yet.
|
|
1062
|
-
return { applied: false, charsRemoved: 0 };
|
|
1063
|
-
}
|
|
1064
1015
|
const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
|
|
1065
1016
|
|
|
1066
1017
|
if (decision.kind === "tool_result_content") {
|
|
@@ -1081,7 +1032,7 @@ export function applyPruneDecision(
|
|
|
1081
1032
|
}
|
|
1082
1033
|
if (pair.resultIndex === undefined) return { applied: false, charsRemoved: 0 };
|
|
1083
1034
|
const resultMessage = messages[pair.resultIndex] as MinimalToolResultMessage;
|
|
1084
|
-
// Only errored-call inputs may ever be redacted
|
|
1035
|
+
// Only errored-call inputs may ever be redacted.
|
|
1085
1036
|
if (!resultMessage.isError) return { applied: false, charsRemoved: 0 };
|
|
1086
1037
|
|
|
1087
1038
|
const assistantMessage = messages[pair.assistantIndex] as MinimalAssistantMessage;
|
|
@@ -1132,7 +1083,6 @@ export function isDecisionProtected(
|
|
|
1132
1083
|
recencyBoundaryIndex: number,
|
|
1133
1084
|
pairIndex?: Map<string, ToolCallPairIndices>,
|
|
1134
1085
|
): boolean {
|
|
1135
|
-
if (decision.correlation.type !== "toolCallId") return true; // no safe way to check protections yet
|
|
1136
1086
|
const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
|
|
1137
1087
|
if (
|
|
1138
1088
|
pair.resultIndex !== undefined &&
|
|
@@ -1154,10 +1104,6 @@ export function isDecisionProtected(
|
|
|
1154
1104
|
return false;
|
|
1155
1105
|
}
|
|
1156
1106
|
|
|
1157
|
-
// ============================================================================
|
|
1158
|
-
// Savings estimation, cache cost model & net-benefit gate (pe-s2ho)
|
|
1159
|
-
// ============================================================================
|
|
1160
|
-
|
|
1161
1107
|
export interface DecisionSavingsEstimate {
|
|
1162
1108
|
/** Earliest message index this decision changes; used for cache-bust tail sizing. */
|
|
1163
1109
|
position: number;
|
|
@@ -1165,36 +1111,44 @@ export interface DecisionSavingsEstimate {
|
|
|
1165
1111
|
tokensRemoved: number;
|
|
1166
1112
|
}
|
|
1167
1113
|
|
|
1114
|
+
type DecisionApplyPreview =
|
|
1115
|
+
| {
|
|
1116
|
+
kind: "tool_result_content";
|
|
1117
|
+
position: number;
|
|
1118
|
+
before: (MinimalTextContent | MinimalImageContent)[];
|
|
1119
|
+
after: (MinimalTextContent | MinimalImageContent)[];
|
|
1120
|
+
}
|
|
1121
|
+
| {
|
|
1122
|
+
kind: "tool_call_input";
|
|
1123
|
+
position: number;
|
|
1124
|
+
beforeArgs: Record<string, unknown>;
|
|
1125
|
+
afterArgs: Record<string, unknown>;
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1168
1128
|
/**
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1129
|
+
* Non-mutating before/after for one decision. A shallow array copy keeps the
|
|
1130
|
+
* caller's messages intact; pair-index positions stay valid because slice()
|
|
1131
|
+
* preserves length and order. Undefined when the target is absent or apply
|
|
1132
|
+
* would no-op.
|
|
1173
1133
|
*/
|
|
1174
|
-
|
|
1134
|
+
function previewDecisionApply(
|
|
1175
1135
|
messages: MinimalMessage[],
|
|
1176
1136
|
decision: PruneDecisionRecord,
|
|
1177
|
-
estimateTokens: TokenEstimator = estimateTokensForText,
|
|
1178
1137
|
pairIndex?: Map<string, ToolCallPairIndices>,
|
|
1179
|
-
):
|
|
1180
|
-
if (decision.correlation.type !== "toolCallId") return undefined;
|
|
1138
|
+
): DecisionApplyPreview | undefined {
|
|
1181
1139
|
const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
|
|
1182
|
-
|
|
1183
|
-
// Apply against a shallow copy of the array (not the message objects) so
|
|
1184
|
-
// the caller's messages/content are never mutated by this preview. Indices
|
|
1185
|
-
// in `pairIndex` (built from `messages`) remain valid against `preview`
|
|
1186
|
-
// since slice() preserves length/order.
|
|
1187
1140
|
const preview = messages.slice();
|
|
1188
1141
|
const result = applyPruneDecision(preview, decision, pairIndex);
|
|
1189
1142
|
if (!result.applied) return undefined;
|
|
1190
1143
|
|
|
1191
1144
|
if (decision.kind === "tool_result_content") {
|
|
1192
1145
|
if (pair.resultIndex === undefined) return undefined;
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1146
|
+
return {
|
|
1147
|
+
kind: "tool_result_content",
|
|
1148
|
+
position: pair.resultIndex,
|
|
1149
|
+
before: (messages[pair.resultIndex] as MinimalToolResultMessage).content,
|
|
1150
|
+
after: (preview[pair.resultIndex] as MinimalToolResultMessage).content,
|
|
1151
|
+
};
|
|
1198
1152
|
}
|
|
1199
1153
|
|
|
1200
1154
|
if (decision.kind === "tool_call_input") {
|
|
@@ -1206,11 +1160,11 @@ export function estimateDecisionSavings(
|
|
|
1206
1160
|
const afterBlock = (preview[pair.assistantIndex] as MinimalAssistantMessage).content[
|
|
1207
1161
|
pair.toolCallBlockIndex
|
|
1208
1162
|
] as MinimalToolCallContent;
|
|
1209
|
-
const tokensBefore = estimateTokens(JSON.stringify(beforeBlock.arguments ?? {}));
|
|
1210
|
-
const tokensAfter = estimateTokens(JSON.stringify(afterBlock.arguments ?? {}));
|
|
1211
1163
|
return {
|
|
1164
|
+
kind: "tool_call_input",
|
|
1212
1165
|
position: pair.assistantIndex,
|
|
1213
|
-
|
|
1166
|
+
beforeArgs: beforeBlock.arguments ?? {},
|
|
1167
|
+
afterArgs: afterBlock.arguments ?? {},
|
|
1214
1168
|
};
|
|
1215
1169
|
}
|
|
1216
1170
|
|
|
@@ -1218,60 +1172,56 @@ export function estimateDecisionSavings(
|
|
|
1218
1172
|
}
|
|
1219
1173
|
|
|
1220
1174
|
/**
|
|
1221
|
-
* Estimate the
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
* *before* they reach the net-benefit gate. Returns undefined under the same
|
|
1226
|
-
* conditions as `estimateDecisionSavings` (target absent / would not apply).
|
|
1227
|
-
* Mirrors `estimateDecisionSavings`'s `Math.max(0, ...)` clamp, so a
|
|
1228
|
-
* decision whose placeholder text would be LONGER than the original content
|
|
1229
|
-
* (negative savings) is reported as 0 chars saved, not a negative number.
|
|
1175
|
+
* Estimate the token savings and affected position of a single decision
|
|
1176
|
+
* without mutating the caller's `messages`. Returns undefined when the
|
|
1177
|
+
* decision's target is absent or the decision would not actually apply
|
|
1178
|
+
* (mirrors `applyPruneDecision`'s no-op conditions).
|
|
1230
1179
|
*/
|
|
1231
|
-
export function
|
|
1180
|
+
export function estimateDecisionSavings(
|
|
1232
1181
|
messages: MinimalMessage[],
|
|
1233
1182
|
decision: PruneDecisionRecord,
|
|
1183
|
+
estimateTokens: TokenEstimator = estimateTokensForText,
|
|
1234
1184
|
pairIndex?: Map<string, ToolCallPairIndices>,
|
|
1235
|
-
):
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
if (!result.applied) return undefined;
|
|
1243
|
-
|
|
1244
|
-
if (decision.kind === "tool_result_content") {
|
|
1245
|
-
if (pair.resultIndex === undefined) return undefined;
|
|
1246
|
-
const before = messages[pair.resultIndex] as MinimalToolResultMessage;
|
|
1247
|
-
const after = preview[pair.resultIndex] as MinimalToolResultMessage;
|
|
1248
|
-
const charsBefore = contentTextLength(before.content);
|
|
1249
|
-
const charsAfter = contentTextLength(after.content);
|
|
1250
|
-
return Math.max(0, charsBefore - charsAfter);
|
|
1185
|
+
): DecisionSavingsEstimate | undefined {
|
|
1186
|
+
const preview = previewDecisionApply(messages, decision, pairIndex);
|
|
1187
|
+
if (!preview) return undefined;
|
|
1188
|
+
if (preview.kind === "tool_result_content") {
|
|
1189
|
+
const tokensRemoved =
|
|
1190
|
+
estimateTokensForContent(preview.before) - estimateTokensForContent(preview.after);
|
|
1191
|
+
return { position: preview.position, tokensRemoved: Math.max(0, tokensRemoved) };
|
|
1251
1192
|
}
|
|
1193
|
+
const tokensRemoved =
|
|
1194
|
+
estimateTokens(JSON.stringify(preview.beforeArgs)) -
|
|
1195
|
+
estimateTokens(JSON.stringify(preview.afterArgs));
|
|
1196
|
+
return { position: preview.position, tokensRemoved: Math.max(0, tokensRemoved) };
|
|
1197
|
+
}
|
|
1252
1198
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1199
|
+
/**
|
|
1200
|
+
* Character savings for one decision: original text or argument JSON minus
|
|
1201
|
+
* the placeholder. Same no-op conditions as `estimateDecisionSavings`.
|
|
1202
|
+
* Clamped at 0 so a longer placeholder is reported as no savings. The
|
|
1203
|
+
* pipeline uses this as the minimum-size floor before the net-benefit gate.
|
|
1204
|
+
*/
|
|
1205
|
+
export function estimateDecisionCharsSaved(
|
|
1206
|
+
messages: MinimalMessage[],
|
|
1207
|
+
decision: PruneDecisionRecord,
|
|
1208
|
+
pairIndex?: Map<string, ToolCallPairIndices>,
|
|
1209
|
+
): number | undefined {
|
|
1210
|
+
const preview = previewDecisionApply(messages, decision, pairIndex);
|
|
1211
|
+
if (!preview) return undefined;
|
|
1212
|
+
if (preview.kind === "tool_result_content") {
|
|
1213
|
+
return Math.max(0, contentTextLength(preview.before) - contentTextLength(preview.after));
|
|
1265
1214
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1215
|
+
return Math.max(
|
|
1216
|
+
0,
|
|
1217
|
+
JSON.stringify(preview.beforeArgs).length - JSON.stringify(preview.afterArgs).length,
|
|
1218
|
+
);
|
|
1268
1219
|
}
|
|
1269
1220
|
|
|
1270
1221
|
/**
|
|
1271
|
-
* Cache-aware cost model
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
* once. Modelled per NOTES as:
|
|
1222
|
+
* Cache-aware cost model: prompt caches are prefix-based, so pruning at
|
|
1223
|
+
* message position p invalidates ('busts') the cached tail from p onward
|
|
1224
|
+
* exactly once:
|
|
1275
1225
|
*
|
|
1276
1226
|
* penalty ~= (1 - r) * tailTokensAfterEarliestChange
|
|
1277
1227
|
* recurringSaving ~= r * tokensRemoved
|
|
@@ -1326,7 +1276,7 @@ export interface NetBenefitGateResult {
|
|
|
1326
1276
|
}
|
|
1327
1277
|
|
|
1328
1278
|
/**
|
|
1329
|
-
* Net-benefit gate
|
|
1279
|
+
* Net-benefit gate: decides whether fresh, automatic prune
|
|
1330
1280
|
* proposals are worth the one-time cache bust they cause. Batch-aware: since
|
|
1331
1281
|
* prompt caches are a single linear prefix, every candidate in one pipeline
|
|
1332
1282
|
* pass is "behind" the earliest changed position and shares that one cache
|
|
@@ -1410,11 +1360,6 @@ export function evaluateNetBenefitGate(
|
|
|
1410
1360
|
};
|
|
1411
1361
|
}
|
|
1412
1362
|
|
|
1413
|
-
// ============================================================================
|
|
1414
|
-
// Savings accounting & stats API (consumed by pe-8re9 /context-pruning stats
|
|
1415
|
-
// and the pe-e9pv benchmark harness)
|
|
1416
|
-
// ============================================================================
|
|
1417
|
-
|
|
1418
1363
|
export interface PruneStatsRecord {
|
|
1419
1364
|
idempotencyKey: string;
|
|
1420
1365
|
strategyId: string;
|
|
@@ -1530,14 +1475,8 @@ export function computeContextSizeSnapshot(
|
|
|
1530
1475
|
};
|
|
1531
1476
|
}
|
|
1532
1477
|
|
|
1533
|
-
// ============================================================================
|
|
1534
|
-
// Strategy registry (populated by follow-up tickets pe-u8gd/pe-qs8j)
|
|
1535
|
-
// ============================================================================
|
|
1536
|
-
|
|
1537
1478
|
export interface StrategyProposeInput {
|
|
1538
1479
|
messages: MinimalMessage[];
|
|
1539
|
-
protections: PruneProtections;
|
|
1540
|
-
estimateTokens: TokenEstimator;
|
|
1541
1480
|
/**
|
|
1542
1481
|
* Full config, so strategies can read their own enable flag and any
|
|
1543
1482
|
* strategy-specific settings (e.g. error-purge's `minTurnsOld`).
|
|
@@ -1546,7 +1485,7 @@ export interface StrategyProposeInput {
|
|
|
1546
1485
|
*/
|
|
1547
1486
|
config: DynamicContextPruningConfig;
|
|
1548
1487
|
/**
|
|
1549
|
-
* Session working directory
|
|
1488
|
+
* Session working directory, used to normalize relative file
|
|
1550
1489
|
* paths (e.g. from read/write/edit tool args) against an absolute base so
|
|
1551
1490
|
* the same file referenced via different relative/absolute spellings is
|
|
1552
1491
|
* recognized as the same path. Optional/backward-compatible: undefined
|
|
@@ -1562,15 +1501,10 @@ export interface PruneStrategy {
|
|
|
1562
1501
|
propose(input: StrategyProposeInput): ProposedPrune[];
|
|
1563
1502
|
}
|
|
1564
1503
|
|
|
1565
|
-
// ----------------------------------------------------------------------
|
|
1566
|
-
// Argument canonicalization (shared by the dedupe strategy; exported for
|
|
1567
|
-
// direct unit testing of key-order/nesting/whitespace behavior).
|
|
1568
|
-
// ----------------------------------------------------------------------
|
|
1569
|
-
|
|
1570
1504
|
/**
|
|
1571
1505
|
* Recursively canonicalize a parsed tool-call arguments value: object keys
|
|
1572
1506
|
* are sorted (recursively); array order and all string/number/boolean VALUES
|
|
1573
|
-
* are left untouched (
|
|
1507
|
+
* are left untouched (never normalize argument values beyond
|
|
1574
1508
|
* structural key ordering). Serializing the result via JSON.stringify then
|
|
1575
1509
|
* yields a deterministic, whitespace-free representation regardless of the
|
|
1576
1510
|
* original key order.
|
|
@@ -1596,7 +1530,7 @@ export function buildDedupeKey(toolName: string, args: unknown): string {
|
|
|
1596
1530
|
return `${toolName.trim().toLowerCase()}::${canonicalizeArgumentsJSON(args)}`;
|
|
1597
1531
|
}
|
|
1598
1532
|
|
|
1599
|
-
/** Exported for /prune picker item building
|
|
1533
|
+
/** Exported for /prune picker item building and direct unit testing. */
|
|
1600
1534
|
export interface ToolCallOccurrence {
|
|
1601
1535
|
toolCallId: string;
|
|
1602
1536
|
toolName: string;
|
|
@@ -1646,10 +1580,6 @@ export function collectCompletedToolCallOccurrences(
|
|
|
1646
1580
|
return occurrences;
|
|
1647
1581
|
}
|
|
1648
1582
|
|
|
1649
|
-
// ----------------------------------------------------------------------
|
|
1650
|
-
// Strategy 1: deduplication (pe-u8gd)
|
|
1651
|
-
// ----------------------------------------------------------------------
|
|
1652
|
-
|
|
1653
1583
|
/** Placeholder text for a duplicate tool result whose content was replaced. */
|
|
1654
1584
|
export function buildDedupePlaceholder(toolName: string, newestToolCallId: string): string {
|
|
1655
1585
|
return `[pruned by ${EXTENSION_ID}: duplicate ${toolName} call result; see newest occurrence (call ${newestToolCallId})]`;
|
|
@@ -1688,10 +1618,6 @@ export const dedupeStrategy: PruneStrategy = {
|
|
|
1688
1618
|
},
|
|
1689
1619
|
};
|
|
1690
1620
|
|
|
1691
|
-
// ----------------------------------------------------------------------
|
|
1692
|
-
// Strategy 2: error-input purge (pe-u8gd)
|
|
1693
|
-
// ----------------------------------------------------------------------
|
|
1694
|
-
|
|
1695
1621
|
export const errorPurgeStrategy: PruneStrategy = {
|
|
1696
1622
|
id: ERROR_PURGE_STRATEGY_ID,
|
|
1697
1623
|
propose(input: StrategyProposeInput): ProposedPrune[] {
|
|
@@ -1716,10 +1642,6 @@ export const errorPurgeStrategy: PruneStrategy = {
|
|
|
1716
1642
|
},
|
|
1717
1643
|
};
|
|
1718
1644
|
|
|
1719
|
-
// ----------------------------------------------------------------------
|
|
1720
|
-
// Strategy 3: superseded file operations (pe-qs8j)
|
|
1721
|
-
// ----------------------------------------------------------------------
|
|
1722
|
-
//
|
|
1723
1645
|
// When the same file path is read and/or written multiple times in a
|
|
1724
1646
|
// session, older read/write/edit tool *outputs* for that path can become
|
|
1725
1647
|
// stale relative to a later operation. This strategy proposes replacing
|
|
@@ -1916,7 +1838,7 @@ export const supersededFileOpsStrategy: PruneStrategy = {
|
|
|
1916
1838
|
// `group` is already in message/chronological order because
|
|
1917
1839
|
// `collectCompletedToolCallOccurrences` iterates messages in order.
|
|
1918
1840
|
//
|
|
1919
|
-
// Single reverse pass
|
|
1841
|
+
// Single reverse pass: instead of `group.slice(i + 1).find(...)`
|
|
1920
1842
|
// per index (O(k^2) allocations+scans for a group of size k), walk the
|
|
1921
1843
|
// group back-to-front and maintain:
|
|
1922
1844
|
// - `nearestLaterSuccessfulWrite`: the nearest (chronologically first)
|
|
@@ -2040,10 +1962,6 @@ function collectProposals(input: StrategyProposeInput): ProposedPrune[] {
|
|
|
2040
1962
|
return proposals;
|
|
2041
1963
|
}
|
|
2042
1964
|
|
|
2043
|
-
// ============================================================================
|
|
2044
|
-
// Pipeline
|
|
2045
|
-
// ============================================================================
|
|
2046
|
-
|
|
2047
1965
|
export interface PipelineInput {
|
|
2048
1966
|
messages: MinimalMessage[];
|
|
2049
1967
|
config: DynamicContextPruningConfig;
|
|
@@ -2052,7 +1970,7 @@ export interface PipelineInput {
|
|
|
2052
1970
|
knownIdempotencyKeys: ReadonlySet<string>;
|
|
2053
1971
|
/**
|
|
2054
1972
|
* Idempotency keys most-recently restored/undone via the /prune picker
|
|
2055
|
-
* (
|
|
1973
|
+
* (see `resolvePruneTombstoneState`: last event wins). Fresh AUTOMATIC proposals
|
|
2056
1974
|
* matching one of these keys are dropped so a restored decision does not
|
|
2057
1975
|
* silently re-apply on the very next context event. Manual re-prunes
|
|
2058
1976
|
* always use a fresh idempotencyKey namespaced by "manual" (see
|
|
@@ -2063,7 +1981,7 @@ export interface PipelineInput {
|
|
|
2063
1981
|
restoredIdempotencyKeys?: ReadonlySet<string>;
|
|
2064
1982
|
/** Omitted defaults to idle. The context handler passes the classified state. */
|
|
2065
1983
|
agentState?: AgentState;
|
|
2066
|
-
/** Session working directory
|
|
1984
|
+
/** Session working directory, forwarded to strategies for path normalization. */
|
|
2067
1985
|
cwd?: string;
|
|
2068
1986
|
}
|
|
2069
1987
|
|
|
@@ -2085,7 +2003,7 @@ export interface PipelineResult {
|
|
|
2085
2003
|
* single place that mutates content. Pairing invariants are preserved by
|
|
2086
2004
|
* construction: messages/blocks are never added or removed, only replaced.
|
|
2087
2005
|
*
|
|
2088
|
-
* Net-benefit gate
|
|
2006
|
+
* Net-benefit gate: only NEW, automatic (non-manual) proposals are
|
|
2089
2007
|
* gated; already-persisted decisions stay applied (their cache bust already
|
|
2090
2008
|
* happened) and manual decisions always bypass the gate (user intent wins).
|
|
2091
2009
|
*/
|
|
@@ -2116,16 +2034,14 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
|
|
|
2116
2034
|
input.config.protections.recentTurns,
|
|
2117
2035
|
);
|
|
2118
2036
|
// Built once and reused across all protection/savings/apply checks below so
|
|
2119
|
-
// each per-decision lookup is O(1) instead of an O(n) rescan of `messages
|
|
2120
|
-
//
|
|
2037
|
+
// each per-decision lookup is O(1) instead of an O(n) rescan of `messages`.
|
|
2038
|
+
// Positions are stable across the apply loop below (and against
|
|
2121
2039
|
// `input.messages`) because decisions only ever replace a message's
|
|
2122
2040
|
// content at its existing index, never insert/remove/reorder messages.
|
|
2123
2041
|
const pairIndex = buildToolCallPairIndex(messages);
|
|
2124
2042
|
|
|
2125
2043
|
const proposals = collectProposals({
|
|
2126
2044
|
messages,
|
|
2127
|
-
protections: input.config.protections,
|
|
2128
|
-
estimateTokens: estimateTokensForText,
|
|
2129
2045
|
config: input.config,
|
|
2130
2046
|
cwd: input.cwd,
|
|
2131
2047
|
});
|
|
@@ -2154,18 +2070,12 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
|
|
|
2154
2070
|
const freshManual = dedupedFresh.filter((decision) => decision.source === "manual");
|
|
2155
2071
|
const freshAutomaticUnfiltered = dedupedFresh.filter((decision) => decision.source !== "manual");
|
|
2156
2072
|
|
|
2157
|
-
// Minimum-size floor
|
|
2158
|
-
//
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2161
|
-
//
|
|
2162
|
-
//
|
|
2163
|
-
// bypass this floor entirely, same as the net-benefit gate. Tool-result
|
|
2164
|
-
// candidates containing images also bypass this raw-text floor because
|
|
2165
|
-
// images have no character count; their token savings still go through the
|
|
2166
|
-
// existing net-benefit gate. A floor of 0 (the config default before
|
|
2167
|
-
// pe-c5n9's DEFAULT_MIN_CHARS_SAVED constant was introduced) is a no-op:
|
|
2168
|
-
// every non-negative delta clears it.
|
|
2073
|
+
// Minimum-size floor: drop fresh automatic proposals whose character
|
|
2074
|
+
// savings fall below `thresholds.minCharsSaved` BEFORE gate evaluation, so
|
|
2075
|
+
// dropped candidates never consume batch/earliest-position consideration.
|
|
2076
|
+
// Manual and already-persisted decisions bypass this floor. Image-bearing
|
|
2077
|
+
// tool results bypass it too (no character count) and still hit the token
|
|
2078
|
+
// gate. A floor of 0 is a no-op.
|
|
2169
2079
|
const minCharsSaved = input.config.thresholds.minCharsSaved;
|
|
2170
2080
|
const freshAutomatic =
|
|
2171
2081
|
minCharsSaved <= 0
|
|
@@ -2177,7 +2087,7 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
|
|
|
2177
2087
|
});
|
|
2178
2088
|
|
|
2179
2089
|
// Collapse fresh AUTOMATIC candidates that target the same (kind,
|
|
2180
|
-
// correlation) down to a single decision
|
|
2090
|
+
// correlation) down to a single decision. Two strategies (e.g.
|
|
2181
2091
|
// `dedupe` and `superseded-file-ops`) can independently propose a decision
|
|
2182
2092
|
// for the exact same tool result; their idempotencyKeys differ (each
|
|
2183
2093
|
// embeds its own strategyId) so the self-dedup above does not catch this,
|
|
@@ -2216,12 +2126,9 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
|
|
|
2216
2126
|
// target's tokensRemoved even though the apply-loop guard further down
|
|
2217
2127
|
// correctly skips applying the fresh candidate — corrupting the gate's
|
|
2218
2128
|
// net-benefit math (and potentially flipping ACCEPT/REJECT) even though no
|
|
2219
|
-
// double-apply occurs.
|
|
2220
|
-
// "toolCallId"` are applyable targets worth tracking here; anything else is
|
|
2221
|
-
// ignored safely (it cannot collide with a toolCallId-keyed target).
|
|
2129
|
+
// double-apply occurs.
|
|
2222
2130
|
const claimedTargetKeys = new Set<string>();
|
|
2223
2131
|
for (const decision of [...input.persistedDecisions, ...freshManual]) {
|
|
2224
|
-
if (decision.correlation.type !== "toolCallId") continue;
|
|
2225
2132
|
claimedTargetKeys.add(buildDecisionTargetKey(decision));
|
|
2226
2133
|
}
|
|
2227
2134
|
|
|
@@ -2269,8 +2176,8 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
|
|
|
2269
2176
|
// `applyPruneDecision` unconditionally re-applies its placeholder even when
|
|
2270
2177
|
// the target is already a placeholder and reports `applied: true`, so
|
|
2271
2178
|
// without this guard the second candidate would silently overwrite the
|
|
2272
|
-
// first's placeholder AND still be credited full stats/newlyAppliedDecisions
|
|
2273
|
-
//
|
|
2179
|
+
// first's placeholder AND still be credited full stats/newlyAppliedDecisions.
|
|
2180
|
+
// The pre-gate collapse above already prevents this among fresh
|
|
2274
2181
|
// automatic candidates; this guard additionally covers persisted-vs-fresh
|
|
2275
2182
|
// (and any other) overlap within a single call.
|
|
2276
2183
|
const appliedTargetKeys = new Set<string>();
|
|
@@ -2310,10 +2217,6 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
|
|
|
2310
2217
|
return { messages, newlyAppliedDecisions, newlyAppliedStats, contextSizeSnapshot, gate };
|
|
2311
2218
|
}
|
|
2312
2219
|
|
|
2313
|
-
// ============================================================================
|
|
2314
|
-
// /prune picker helpers (pe-8re9)
|
|
2315
|
-
// ============================================================================
|
|
2316
|
-
|
|
2317
2220
|
function resolveSessionProjection(ctx: ExtensionContext): MinimalSessionProjection | undefined {
|
|
2318
2221
|
try {
|
|
2319
2222
|
const sessionManager = ctx.sessionManager as unknown as {
|
|
@@ -2364,14 +2267,12 @@ export interface PrunableItem {
|
|
|
2364
2267
|
* for manual prune/restore. `activeByToolCallId` should be built from
|
|
2365
2268
|
* currently-ACTIVE decisions only (kind: "tool_result_content"), keyed by
|
|
2366
2269
|
* toolCallId; `restoredToolCallIds` from currently-restored keys' underlying
|
|
2367
|
-
* toolCallId (see `buildActiveResultDecisionMap
|
|
2368
|
-
* usage in the extension wiring).
|
|
2270
|
+
* toolCallId (see `buildActiveResultDecisionMap` in the extension wiring).
|
|
2369
2271
|
*/
|
|
2370
2272
|
export function buildPrunableItems(
|
|
2371
2273
|
messages: MinimalMessage[],
|
|
2372
2274
|
activeByToolCallId: Map<string, PruneDecisionRecord>,
|
|
2373
2275
|
restoredToolCallIds: ReadonlySet<string>,
|
|
2374
|
-
_estimateTokens: TokenEstimator = estimateTokensForText,
|
|
2375
2276
|
): PrunableItem[] {
|
|
2376
2277
|
const items: PrunableItem[] = [];
|
|
2377
2278
|
for (const occurrence of collectCompletedToolCallOccurrences(messages)) {
|
|
@@ -2404,7 +2305,6 @@ export function buildActiveResultDecisionMap(
|
|
|
2404
2305
|
const map = new Map<string, PruneDecisionRecord>();
|
|
2405
2306
|
for (const decision of activeDecisions) {
|
|
2406
2307
|
if (decision.kind !== "tool_result_content") continue;
|
|
2407
|
-
if (decision.correlation.type !== "toolCallId") continue;
|
|
2408
2308
|
if (!map.has(decision.correlation.toolCallId))
|
|
2409
2309
|
map.set(decision.correlation.toolCallId, decision);
|
|
2410
2310
|
}
|
|
@@ -2466,10 +2366,6 @@ export function formatPrunableItemsReport(items: PrunableItem[]): string[] {
|
|
|
2466
2366
|
return items.map((item, index) => `${index + 1}. ${formatPrunableItemOption(item)}`);
|
|
2467
2367
|
}
|
|
2468
2368
|
|
|
2469
|
-
// ============================================================================
|
|
2470
|
-
// /context-pruning control + status/stats helpers (pe-8re9)
|
|
2471
|
-
// ============================================================================
|
|
2472
|
-
|
|
2473
2369
|
export type StrategyKey = "dedupe" | "errorPurge" | "supersededFileOps";
|
|
2474
2370
|
|
|
2475
2371
|
const STRATEGY_ALIASES: Record<string, StrategyKey> = {
|
|
@@ -2659,10 +2555,6 @@ export function formatStatsReport(stats: CumulativePruneStats): string[] {
|
|
|
2659
2555
|
return lines;
|
|
2660
2556
|
}
|
|
2661
2557
|
|
|
2662
|
-
// ============================================================================
|
|
2663
|
-
// Extension wiring
|
|
2664
|
-
// ============================================================================
|
|
2665
|
-
|
|
2666
2558
|
function notify(
|
|
2667
2559
|
ctx: ExtensionContext,
|
|
2668
2560
|
message: string,
|
|
@@ -2722,9 +2614,6 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
|
|
|
2722
2614
|
persistedDecisions,
|
|
2723
2615
|
knownIdempotencyKeys,
|
|
2724
2616
|
restoredIdempotencyKeys,
|
|
2725
|
-
// Real mid-loop/idle agent-state detection (pe-zy4s): derived straight from
|
|
2726
|
-
// the message payload via classifyAgentStateFromMessages -- see its doc
|
|
2727
|
-
// comment for the exact runtime-observable definition.
|
|
2728
2617
|
agentState: classifyAgentStateFromMessages(event.messages as unknown as MinimalMessage[]),
|
|
2729
2618
|
cwd: ctx.sessionManager.getCwd(),
|
|
2730
2619
|
});
|
|
@@ -2805,11 +2694,8 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
|
|
|
2805
2694
|
const activeByToolCallId = buildActiveResultDecisionMap(persistedDecisions);
|
|
2806
2695
|
const restoredToolCallIds = new Set(
|
|
2807
2696
|
Array.from(lastDecisionByKey.values())
|
|
2808
|
-
.filter(
|
|
2809
|
-
|
|
2810
|
-
d.correlation.type === "toolCallId" && restoredIdempotencyKeys.has(d.idempotencyKey),
|
|
2811
|
-
)
|
|
2812
|
-
.map((d) => (d.correlation as { type: "toolCallId"; toolCallId: string }).toolCallId),
|
|
2697
|
+
.filter((d) => restoredIdempotencyKeys.has(d.idempotencyKey))
|
|
2698
|
+
.map((d) => d.correlation.toolCallId),
|
|
2813
2699
|
);
|
|
2814
2700
|
const items = buildPrunableItems(messages, activeByToolCallId, restoredToolCallIds);
|
|
2815
2701
|
|
|
@@ -2839,12 +2725,8 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
|
|
|
2839
2725
|
const latestActive = buildActiveResultDecisionMap(persistedDecisions);
|
|
2840
2726
|
const latestRestoredToolCallIds = new Set(
|
|
2841
2727
|
Array.from(lastDecisionByKey.values())
|
|
2842
|
-
.filter(
|
|
2843
|
-
|
|
2844
|
-
d.correlation.type === "toolCallId" &&
|
|
2845
|
-
restoredIdempotencyKeys.has(d.idempotencyKey),
|
|
2846
|
-
)
|
|
2847
|
-
.map((d) => (d.correlation as { type: "toolCallId"; toolCallId: string }).toolCallId),
|
|
2728
|
+
.filter((d) => restoredIdempotencyKeys.has(d.idempotencyKey))
|
|
2729
|
+
.map((d) => d.correlation.toolCallId),
|
|
2848
2730
|
);
|
|
2849
2731
|
const latestItems = buildPrunableItems(
|
|
2850
2732
|
latestMessages,
|