pi-condense 2.10.1 → 2.10.3
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/CHANGELOG.md +8 -0
- package/PRUNING.md +13 -4
- package/README.md +3 -1
- package/index.ts +34 -1
- package/package.json +1 -1
- package/src/commands.ts +1 -1
- package/src/config.test.ts +16 -0
- package/src/protected.test.ts +30 -1
- package/src/protected.ts +6 -1
- package/src/pruner.test.ts +74 -1
- package/src/pruner.ts +19 -1
- package/src/reload-rearm.integration.test.ts +518 -0
- package/src/supersede.test.ts +302 -0
- package/src/supersede.ts +117 -0
- package/src/types.ts +3 -2
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
applySupersede,
|
|
4
|
+
createSupersedeState,
|
|
5
|
+
earliestChainStart,
|
|
6
|
+
earliestResultTimestamp,
|
|
7
|
+
findSuperseded,
|
|
8
|
+
lowerFloor,
|
|
9
|
+
supersededStub,
|
|
10
|
+
type SupersedeState,
|
|
11
|
+
} from "./supersede.js";
|
|
12
|
+
import { isProtected } from "./protected.js";
|
|
13
|
+
import { occKey } from "./occurrence-key.js";
|
|
14
|
+
|
|
15
|
+
const cfg = { protectedTools: ["phase_tracker"], protectedPaths: ["**/skills/**/*.md"] };
|
|
16
|
+
const prot = (name: string, args: unknown) => isProtected(name, args, cfg);
|
|
17
|
+
|
|
18
|
+
const SKILL = "/h/skills/x/SKILL.md";
|
|
19
|
+
const OTHER = "/h/skills/y/SKILL.md";
|
|
20
|
+
|
|
21
|
+
function call(id: string, ts: number, args: unknown, name = "read", argKey: "input" | "args" | "arguments" = "input"): any[] {
|
|
22
|
+
return [
|
|
23
|
+
{ role: "assistant", timestamp: ts, content: [{ type: "toolCall", id, name, [argKey]: args }] },
|
|
24
|
+
{ role: "toolResult", toolCallId: id, toolName: name, content: [{ type: "text", text: `BODY-${id}` }], isError: false, timestamp: ts + 1 },
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("findSuperseded", () => {
|
|
29
|
+
test("single protected read -> []", () => {
|
|
30
|
+
expect(findSuperseded(call("a", 10, { path: SKILL }), prot)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("A < B same path -> [A]", () => {
|
|
34
|
+
const msgs = [...call("a", 10, { path: SKILL }), ...call("b", 20, { path: SKILL })];
|
|
35
|
+
const out = findSuperseded(msgs, prot);
|
|
36
|
+
expect(out.map((c) => c.toolCallId)).toEqual(["a"]);
|
|
37
|
+
expect(out[0]).toEqual({ toolCallId: "a", path: SKILL, timestamp: 11, resultIndex: 1 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("A < B < C -> [A, B]", () => {
|
|
41
|
+
const msgs = [...call("a", 10, { path: SKILL }), ...call("b", 20, { path: SKILL }), ...call("c", 30, { path: SKILL })];
|
|
42
|
+
expect(findSuperseded(msgs, prot).map((c) => c.toolCallId)).toEqual(["a", "b"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("different offset/limit slices of one path -> older is a candidate", () => {
|
|
46
|
+
const msgs = [...call("a", 10, { path: SKILL, offset: 1, limit: 400 }), ...call("b", 20, { path: SKILL, offset: 400, limit: 50 })];
|
|
47
|
+
expect(findSuperseded(msgs, prot).map((c) => c.toolCallId)).toEqual(["a"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("two paths interleaved -> per-path winners and candidates", () => {
|
|
51
|
+
const msgs = [
|
|
52
|
+
...call("a1", 10, { path: SKILL }),
|
|
53
|
+
...call("b1", 20, { path: OTHER }),
|
|
54
|
+
...call("a2", 30, { path: SKILL }),
|
|
55
|
+
...call("b2", 40, { path: OTHER }),
|
|
56
|
+
];
|
|
57
|
+
expect(findSuperseded(msgs, prot).map((c) => c.toolCallId)).toEqual(["a1", "b1"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("protected call without path -> ignored", () => {
|
|
61
|
+
const msgs = [...call("t1", 10, {}, "phase_tracker"), ...call("t2", 20, {}, "phase_tracker")];
|
|
62
|
+
expect(findSuperseded(msgs, prot)).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("unprotected read of the same path -> neither candidate nor winner", () => {
|
|
66
|
+
// bash's real args carry `command`, not `path` (protected.test.ts: "does not infer paths from bash commands");
|
|
67
|
+
// isProtected matches path globs regardless of tool name, so a synthetic `path` arg here would wrongly qualify.
|
|
68
|
+
const msgs = [...call("a", 10, { path: SKILL }), ...call("u", 20, { command: `cat ${SKILL}` }, "bash")];
|
|
69
|
+
expect(findSuperseded(msgs, prot)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("backslash path equals forward-slash path", () => {
|
|
73
|
+
const msgs = [...call("a", 10, { path: "\\h\\skills\\x\\SKILL.md" }), ...call("b", 20, { path: SKILL })];
|
|
74
|
+
expect(findSuperseded(msgs, prot).map((c) => c.toolCallId)).toEqual(["a"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("newest call without a paired result does not participate; previous read wins", () => {
|
|
78
|
+
const msgs = [
|
|
79
|
+
...call("a", 10, { path: SKILL }),
|
|
80
|
+
...call("b", 20, { path: SKILL }),
|
|
81
|
+
{ role: "assistant", timestamp: 30, content: [{ type: "toolCall", id: "c", name: "read", input: { path: SKILL } }] },
|
|
82
|
+
];
|
|
83
|
+
expect(findSuperseded(msgs, prot).map((c) => c.toolCallId)).toEqual(["a"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("missing result timestamp -> timestamp undefined, pairing still by id", () => {
|
|
87
|
+
const msgs = [...call("a", 10, { path: SKILL }), ...call("b", 20, { path: SKILL })];
|
|
88
|
+
delete msgs[1].timestamp;
|
|
89
|
+
expect(findSuperseded(msgs, prot)[0]).toEqual({ toolCallId: "a", path: SKILL, timestamp: undefined, resultIndex: 1 });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("args read from input / args / arguments shapes alike", () => {
|
|
93
|
+
for (const key of ["input", "args", "arguments"] as const) {
|
|
94
|
+
const msgs = [...call("a", 10, { path: SKILL }, "read", key), ...call("b", 20, { path: SKILL }, "read", key)];
|
|
95
|
+
expect(findSuperseded(msgs, prot).map((c) => c.toolCallId)).toEqual(["a"]);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("reused id with an interleaved non-participating call pairs each read with its own result", () => {
|
|
100
|
+
const msgs = [
|
|
101
|
+
...call("X", 10, { path: SKILL }), // idx 0-1: protected read
|
|
102
|
+
...call("X", 20, { command: "echo hi" }, "bash"), // idx 2-3: same id, not participating
|
|
103
|
+
...call("X", 30, { path: SKILL }), // idx 4-5: protected read
|
|
104
|
+
...call("X", 40, { path: SKILL }), // idx 6-7: protected read (winner)
|
|
105
|
+
];
|
|
106
|
+
const out = findSuperseded(msgs, prot);
|
|
107
|
+
expect(out.map((c) => [c.resultIndex, c.timestamp])).toEqual([[1, 11], [5, 31]]);
|
|
108
|
+
const s = createSupersedeState();
|
|
109
|
+
s.floor = 0;
|
|
110
|
+
const pruned = applySupersede(msgs, s, prot);
|
|
111
|
+
expect(pruned[3].content[0].text).toBe("BODY-X"); // bash result untouched
|
|
112
|
+
expect(pruned[7].content[0].text).toBe("BODY-X"); // newest read untouched
|
|
113
|
+
expect(pruned[1].content[0].text).toBe(supersededStub(SKILL));
|
|
114
|
+
expect(pruned[5].content[0].text).toBe(supersededStub(SKILL));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("aborted call under a reused id in the middle does not steal a later result", () => {
|
|
118
|
+
const msgs = [
|
|
119
|
+
...call("X", 10, { path: SKILL }), // idx 0-1: only read of SKILL
|
|
120
|
+
{ role: "assistant", timestamp: 20, content: [{ type: "toolCall", id: "X", name: "read", input: { path: SKILL } }] }, // idx 2: aborted, no result
|
|
121
|
+
...call("X", 30, { path: OTHER }), // idx 3-4: only read of OTHER
|
|
122
|
+
];
|
|
123
|
+
expect(findSuperseded(msgs, prot)).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a result is paired only with the immediately preceding assistant turn", () => {
|
|
127
|
+
const msgs = [
|
|
128
|
+
{ role: "assistant", timestamp: 10, content: [{ type: "toolCall", id: "X", name: "read", input: { path: SKILL } }] },
|
|
129
|
+
{ role: "user", timestamp: 15, content: [{ type: "text", text: "barrier" }] },
|
|
130
|
+
{ role: "toolResult", toolCallId: "X", toolName: "read", content: [{ type: "text", text: "STRAY" }], isError: false, timestamp: 16 },
|
|
131
|
+
...call("X", 20, { path: SKILL }),
|
|
132
|
+
];
|
|
133
|
+
// the stray result after a barrier pairs with nothing; the turn-10 call has no result and does not participate
|
|
134
|
+
expect(findSuperseded(msgs, prot)).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("the last occurrence per path is never returned (shuffled fixtures)", () => {
|
|
138
|
+
const paths = [SKILL, OTHER, "/h/skills/z/SKILL.md"];
|
|
139
|
+
let seed = 7;
|
|
140
|
+
const rnd = () => (seed = (seed * 9301 + 49297) % 233280) / 233280;
|
|
141
|
+
for (let round = 0; round < 20; round++) {
|
|
142
|
+
const msgs: any[] = [];
|
|
143
|
+
const lastId = new Map<string, string>();
|
|
144
|
+
for (let i = 0; i < 12; i++) {
|
|
145
|
+
const p = paths[Math.floor(rnd() * paths.length)];
|
|
146
|
+
const id = `r${round}-${i}`;
|
|
147
|
+
msgs.push(...call(id, i * 10, { path: p }));
|
|
148
|
+
lastId.set(p, id);
|
|
149
|
+
}
|
|
150
|
+
const returned = new Set(findSuperseded(msgs, prot).map((c) => c.toolCallId));
|
|
151
|
+
for (const id of lastId.values()) expect(returned.has(id)).toBe(false);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("floor helpers", () => {
|
|
157
|
+
test("lowerFloor sets when undefined, lowers monotonically, ignores undefined input", () => {
|
|
158
|
+
const s = createSupersedeState();
|
|
159
|
+
lowerFloor(s, undefined);
|
|
160
|
+
expect(s.floor).toBeUndefined();
|
|
161
|
+
lowerFloor(s, 500);
|
|
162
|
+
expect(s.floor).toBe(500);
|
|
163
|
+
lowerFloor(s, 900);
|
|
164
|
+
expect(s.floor).toBe(500);
|
|
165
|
+
lowerFloor(s, 100);
|
|
166
|
+
expect(s.floor).toBe(100);
|
|
167
|
+
lowerFloor(s, 0);
|
|
168
|
+
expect(s.floor).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("earliestResultTimestamp: min over defined timestamps, undefined when none", () => {
|
|
172
|
+
expect(earliestResultTimestamp([{ resultTimestamp: 30 }, { resultTimestamp: 10 }, {}])).toBe(10);
|
|
173
|
+
expect(earliestResultTimestamp([{}, {}])).toBeUndefined();
|
|
174
|
+
expect(earliestResultTimestamp([])).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("earliestChainStart: min startUserTimestamp, undefined when empty", () => {
|
|
178
|
+
expect(earliestChainStart([{ startUserTimestamp: 300 }, { startUserTimestamp: 100 }])).toBe(100);
|
|
179
|
+
expect(earliestChainStart([])).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("applySupersede", () => {
|
|
184
|
+
const two = () => [...call("a", 10, { path: SKILL }), ...call("b", 20, { path: SKILL })];
|
|
185
|
+
|
|
186
|
+
test("floor undefined -> input reference returned, activated empty", () => {
|
|
187
|
+
const s = createSupersedeState();
|
|
188
|
+
const msgs = two();
|
|
189
|
+
expect(applySupersede(msgs, s, prot)).toBe(msgs);
|
|
190
|
+
expect(s.activated.size).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("floor 0 -> candidate stubbed, newest byte-for-byte verbatim, input untouched", () => {
|
|
194
|
+
const s = createSupersedeState();
|
|
195
|
+
s.floor = 0;
|
|
196
|
+
const msgs = two();
|
|
197
|
+
const before = JSON.stringify(msgs);
|
|
198
|
+
const out = applySupersede(msgs, s, prot);
|
|
199
|
+
expect(out).not.toBe(msgs);
|
|
200
|
+
expect(JSON.stringify(msgs)).toBe(before);
|
|
201
|
+
expect(out[1]).toEqual({ ...msgs[1], content: [{ type: "text", text: supersededStub(SKILL) }] });
|
|
202
|
+
expect(out[3]).toBe(msgs[3]);
|
|
203
|
+
expect(s.floor).toBeUndefined();
|
|
204
|
+
expect(s.activated.has(occKey("a", 11))).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("stub text is the spec literal", () => {
|
|
208
|
+
expect(supersededStub(SKILL)).toBe(
|
|
209
|
+
`[Superseded: ${SKILL} was read again later in this conversation - see the newer read. Re-read the file if this earlier content is needed.]`,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("floor above candidate -> verbatim; lowered below -> stubbed; then undefined -> sticky", () => {
|
|
214
|
+
const s = createSupersedeState();
|
|
215
|
+
const msgs = two();
|
|
216
|
+
s.floor = 15;
|
|
217
|
+
expect(applySupersede(msgs, s, prot)).toBe(msgs);
|
|
218
|
+
expect(s.floor).toBeUndefined();
|
|
219
|
+
s.floor = 5;
|
|
220
|
+
expect(applySupersede(msgs, s, prot)[1].content[0].text).toBe(supersededStub(SKILL));
|
|
221
|
+
expect(s.floor).toBeUndefined();
|
|
222
|
+
expect(applySupersede(msgs, s, prot)[1].content[0].text).toBe(supersededStub(SKILL));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("floor exactly equal to candidate timestamp activates (>=)", () => {
|
|
226
|
+
const s = createSupersedeState();
|
|
227
|
+
const msgs = two();
|
|
228
|
+
s.floor = 11;
|
|
229
|
+
expect(applySupersede(msgs, s, prot)).not.toBe(msgs);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("isProtected flips to false -> stub not applied even though key is activated", () => {
|
|
233
|
+
const s = createSupersedeState();
|
|
234
|
+
s.floor = 0;
|
|
235
|
+
const msgs = two();
|
|
236
|
+
applySupersede(msgs, s, prot);
|
|
237
|
+
const none = (_n: string, _a: unknown) => false;
|
|
238
|
+
expect(applySupersede(msgs, s, none)).toBe(msgs);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("newer read removed -> older read verbatim even though key is activated", () => {
|
|
242
|
+
const s = createSupersedeState();
|
|
243
|
+
s.floor = 0;
|
|
244
|
+
const msgs = two();
|
|
245
|
+
applySupersede(msgs, s, prot);
|
|
246
|
+
const onlyOld = msgs.slice(0, 2);
|
|
247
|
+
expect(applySupersede(onlyOld, s, prot)).toBe(onlyOld);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("candidate with undefined timestamp: not activated by positional floor, activated by floor 0", () => {
|
|
251
|
+
const s = createSupersedeState();
|
|
252
|
+
const msgs = two();
|
|
253
|
+
delete msgs[1].timestamp;
|
|
254
|
+
s.floor = 5;
|
|
255
|
+
expect(applySupersede(msgs, s, prot)).toBe(msgs);
|
|
256
|
+
s.floor = 0;
|
|
257
|
+
const out = applySupersede(msgs, s, prot);
|
|
258
|
+
expect(out[1].content[0].text).toBe(supersededStub(SKILL));
|
|
259
|
+
expect(s.activated.has("a")).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("same toolCallId reused across turns -> only the activated occurrence is stubbed", () => {
|
|
263
|
+
const s = createSupersedeState();
|
|
264
|
+
const msgs = [...call("dup", 10, { path: SKILL }), ...call("dup", 20, { path: SKILL }), ...call("dup", 30, { path: SKILL })];
|
|
265
|
+
s.floor = 21;
|
|
266
|
+
const out = applySupersede(msgs, s, prot);
|
|
267
|
+
expect(out[1].content[0].text).toBe("BODY-dup");
|
|
268
|
+
expect(out[3].content[0].text).toBe(supersededStub(SKILL));
|
|
269
|
+
expect(out[5].content[0].text).toBe("BODY-dup");
|
|
270
|
+
expect([...s.activated]).toEqual([occKey("dup", 21)]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("newest read errored still wins", () => {
|
|
274
|
+
const s = createSupersedeState();
|
|
275
|
+
s.floor = 0;
|
|
276
|
+
const msgs = two();
|
|
277
|
+
msgs[3] = { ...msgs[3], isError: true, content: [{ type: "text", text: "ENOENT" }] };
|
|
278
|
+
const out = applySupersede(msgs, s, prot);
|
|
279
|
+
expect(out[1].content[0].text).toBe(supersededStub(SKILL));
|
|
280
|
+
expect(out[3]).toBe(msgs[3]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("A < B < C: A and B stubbed, C verbatim", () => {
|
|
284
|
+
const s = createSupersedeState();
|
|
285
|
+
s.floor = 0;
|
|
286
|
+
const msgs = [...call("a", 10, { path: SKILL }), ...call("b", 20, { path: SKILL }), ...call("c", 30, { path: SKILL })];
|
|
287
|
+
const out = applySupersede(msgs, s, prot);
|
|
288
|
+
expect(out[1].content[0].text).toBe(supersededStub(SKILL));
|
|
289
|
+
expect(out[3].content[0].text).toBe(supersededStub(SKILL));
|
|
290
|
+
expect(out[5]).toBe(msgs[5]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("superseded candidate that had errored is stubbed with isError false", () => {
|
|
294
|
+
const s = createSupersedeState();
|
|
295
|
+
s.floor = 0;
|
|
296
|
+
const msgs = two();
|
|
297
|
+
msgs[1] = { ...msgs[1], isError: true, content: [{ type: "text", text: "EACCES" }] };
|
|
298
|
+
const out = applySupersede(msgs, s, prot);
|
|
299
|
+
expect(out[1].isError).toBe(false);
|
|
300
|
+
expect(out[1].content[0].text).toBe(supersededStub(SKILL));
|
|
301
|
+
});
|
|
302
|
+
});
|
package/src/supersede.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { normalizePath } from "./protected.js";
|
|
2
|
+
import { occKey, resultTimestampOf } from "./occurrence-key.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Protected reads are never indexed, so nothing else in the pipeline ever
|
|
6
|
+
* collapses a re-read of the same skill file. This module keeps only the
|
|
7
|
+
* newest protected read per `args.path` verbatim (spec 2026-09-07).
|
|
8
|
+
*/
|
|
9
|
+
export interface SupersededCandidate {
|
|
10
|
+
toolCallId: string;
|
|
11
|
+
path: string;
|
|
12
|
+
timestamp: number | undefined;
|
|
13
|
+
resultIndex: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SupersedeState {
|
|
17
|
+
/** Earliest result timestamp the next render will rewrite anyway; 0 = cold cache, activate everything. */
|
|
18
|
+
floor: number | undefined;
|
|
19
|
+
/** occKey(toolCallId, resultTimestamp) of candidates whose stub has taken effect (session-sticky). */
|
|
20
|
+
activated: Set<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createSupersedeState(): SupersedeState {
|
|
24
|
+
return { floor: undefined, activated: new Set() };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function lowerFloor(state: SupersedeState, t: number | undefined): void {
|
|
28
|
+
if (t === undefined) return;
|
|
29
|
+
state.floor = state.floor === undefined ? t : Math.min(state.floor, t);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function earliestResultTimestamp(toolCalls: readonly { resultTimestamp?: number }[]): number | undefined {
|
|
33
|
+
let min: number | undefined;
|
|
34
|
+
for (const tc of toolCalls) {
|
|
35
|
+
if (tc.resultTimestamp !== undefined && (min === undefined || tc.resultTimestamp < min)) min = tc.resultTimestamp;
|
|
36
|
+
}
|
|
37
|
+
return min;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function earliestChainStart(entries: readonly { startUserTimestamp: number }[]): number | undefined {
|
|
41
|
+
let min: number | undefined;
|
|
42
|
+
for (const e of entries) if (min === undefined || e.startUserTimestamp < min) min = e.startUserTimestamp;
|
|
43
|
+
return min;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function supersededStub(path: string): string {
|
|
47
|
+
return `[Superseded: ${path} was read again later in this conversation - see the newer read. Re-read the file if this earlier content is needed.]`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type IsProtectedFn = (toolName: string, args: unknown) => boolean;
|
|
51
|
+
|
|
52
|
+
export function findSuperseded(messages: any[], isProtected: IsProtectedFn): SupersededCandidate[] {
|
|
53
|
+
// Provider ids repeat across turns and an aborted call has no result, so pairing
|
|
54
|
+
// uses the same per-turn open-set model as orphan-sweep, not a global per-id cursor.
|
|
55
|
+
let open = new Map<string, any>();
|
|
56
|
+
const byPath = new Map<string, SupersededCandidate[]>();
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < messages.length; i++) {
|
|
59
|
+
const m = messages[i];
|
|
60
|
+
if (m?.role === "assistant" && Array.isArray(m.content)) {
|
|
61
|
+
open = new Map();
|
|
62
|
+
for (const block of m.content) if (block?.type === "toolCall") open.set(block.id, block);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (m?.role === "toolResult") {
|
|
66
|
+
const block = open.get(m.toolCallId);
|
|
67
|
+
if (!block) continue;
|
|
68
|
+
open.delete(m.toolCallId);
|
|
69
|
+
const args = block.input ?? block.args ?? block.arguments ?? {};
|
|
70
|
+
if (!isProtected(block.name, args)) continue;
|
|
71
|
+
const rawPath = (args as Record<string, unknown>)?.path;
|
|
72
|
+
if (typeof rawPath !== "string") continue;
|
|
73
|
+
const path = normalizePath(rawPath);
|
|
74
|
+
const cand: SupersededCandidate = {
|
|
75
|
+
toolCallId: block.id,
|
|
76
|
+
path,
|
|
77
|
+
timestamp: resultTimestampOf(m.timestamp),
|
|
78
|
+
resultIndex: i,
|
|
79
|
+
};
|
|
80
|
+
const list = byPath.get(path);
|
|
81
|
+
if (list) list.push(cand);
|
|
82
|
+
else byPath.set(path, [cand]);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
open = new Map();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const out: SupersededCandidate[] = [];
|
|
89
|
+
for (const list of byPath.values()) for (let i = 0; i < list.length - 1; i++) out.push(list[i]);
|
|
90
|
+
out.sort((a, b) => a.resultIndex - b.resultIndex);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const keyOf = (c: SupersededCandidate) => occKey(c.toolCallId, c.timestamp);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Phase 1b of pruneMessages. Reference-preserving when nothing is stubbed.
|
|
98
|
+
* Consumes `state.floor` exactly once per call.
|
|
99
|
+
*/
|
|
100
|
+
export function applySupersede(messages: any[], state: SupersedeState, isProtected: IsProtectedFn): any[] {
|
|
101
|
+
const candidates = findSuperseded(messages, isProtected);
|
|
102
|
+
if (state.floor !== undefined) {
|
|
103
|
+
const floor = state.floor;
|
|
104
|
+
for (const c of candidates) {
|
|
105
|
+
if (floor === 0 || (c.timestamp !== undefined && c.timestamp >= floor)) state.activated.add(keyOf(c));
|
|
106
|
+
}
|
|
107
|
+
state.floor = undefined;
|
|
108
|
+
}
|
|
109
|
+
let out = messages;
|
|
110
|
+
for (const c of candidates) {
|
|
111
|
+
if (!state.activated.has(keyOf(c))) continue;
|
|
112
|
+
if (out === messages) out = messages.slice();
|
|
113
|
+
const orig = messages[c.resultIndex];
|
|
114
|
+
out[c.resultIndex] = { ...orig, content: [{ type: "text", text: supersededStub(c.path) }], isError: false };
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -352,7 +352,8 @@ export interface ContextPruneConfig {
|
|
|
352
352
|
/**
|
|
353
353
|
* Glob patterns matched against a tool call's `args.path`. Matching calls are
|
|
354
354
|
* protected with identical semantics to protectedTools. Default protects
|
|
355
|
-
* skill files and their sibling reference docs under any `skills/` dir
|
|
355
|
+
* skill files and their sibling reference docs under any `skills/` dir,
|
|
356
|
+
* plus per-repo `gauntlet-overrides.md` files.
|
|
356
357
|
* Kill switch: set to [] in settings.json (`contextPrune.protectedPaths`).
|
|
357
358
|
*/
|
|
358
359
|
protectedPaths: string[];
|
|
@@ -562,7 +563,7 @@ export const DEFAULT_CONFIG: ContextPruneConfig = {
|
|
|
562
563
|
summarizerIdleTimeoutMs: 20000,
|
|
563
564
|
summarizerMaxTimeoutMs: 180000,
|
|
564
565
|
protectedTools: [],
|
|
565
|
-
protectedPaths: ["**/skills/**/*.md"],
|
|
566
|
+
protectedPaths: ["**/skills/**/*.md", "**/gauntlet-overrides.md"],
|
|
566
567
|
chainCompression: {
|
|
567
568
|
enabled: true,
|
|
568
569
|
rollingWindow: 3,
|