Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions proxy/extensions/upstream-change-detection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,17 @@ async function _processRequest(body, headers, { dir, map, fs }) {
};
map.set(nsKey, updated);

// messages.count grows on every ordinary turn — a conversation living is
// not an upstream structural change, and logging it drowned the alarm
// file (97% of all recorded events were count-only diffs, measured
// 2026-07-30 over 4661 events). A count-only diff updates the stored
// fingerprint silently; any OTHER changed path still alarms, with the
// count change riding along in its diff if both moved.
const alarmDiff = diff.filter((d) => d.path !== "messages.count");
if (alarmDiff.length === 0) {
return { event: "noop", nsKey };
}

await appendEvent(
{
ts,
Expand Down
59 changes: 59 additions & 0 deletions test/proxy-upstream-change-detection.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,62 @@ test("loadBaseline tolerates corrupt file", async () => {
await rm(dir, { recursive: true, force: true });
}
});

// --- 16. Count-only growth is not an upstream change ---

test("16. a messages.count-only diff updates the baseline silently — no structural_change event", async () => {
const dir = await newTmp();
process.env.CACHE_FIX_UPSTREAM_DETECTION = "1";
process.env.CACHE_FIX_UPSTREAM_DIR = dir;
try {
const ext = await freshExt();
await ext.default.onRequest({ body: makeBody() });
// Same shape, one more ordinary message — a conversation growing.
await ext.default.onRequest({
body: makeBody({
messages: [
{ role: "user", content: [{ type: "text", text: "hello" }] },
{ role: "assistant", content: [{ type: "text", text: "hi" }] },
{ role: "user", content: [{ type: "text", text: "more" }] },
],
}),
});
const text = await readFile(join(dir, "upstream-changes.jsonl"), "utf8");
const events = text.split("\n").filter(Boolean).map((l) => JSON.parse(l).event);
assert.deepEqual(events, ["baseline_established"], "growth alone must not alarm");
} finally {
delete process.env.CACHE_FIX_UPSTREAM_DETECTION;
delete process.env.CACHE_FIX_UPSTREAM_DIR;
await rm(dir, { recursive: true, force: true });
}
});

test("17. count change RIDING a real structural change still alarms, count in the diff", async () => {
const dir = await newTmp();
process.env.CACHE_FIX_UPSTREAM_DETECTION = "1";
process.env.CACHE_FIX_UPSTREAM_DIR = dir;
try {
const ext = await freshExt();
await ext.default.onRequest({ body: makeBody() });
await ext.default.onRequest({
body: makeBody({
messages: [
{ role: "user", content: [{ type: "text", text: "hello" }] },
{ role: "assistant", content: [{ type: "text", text: "hi" }] },
],
tools: [
{ name: "Bash", input_schema: { properties: { command: {} } } },
],
}),
});
const text = await readFile(join(dir, "upstream-changes.jsonl"), "utf8");
const parsed = text.split("\n").filter(Boolean).map((l) => JSON.parse(l));
const change = parsed.find((e) => e.event === "structural_change");
assert.ok(change, "a tools change must still alarm");
assert.ok(change.diff.some((d) => d.path === "messages.count"), "the count delta rides in the diff");
} finally {
delete process.env.CACHE_FIX_UPSTREAM_DETECTION;
delete process.env.CACHE_FIX_UPSTREAM_DIR;
await rm(dir, { recursive: true, force: true });
}
});