Skip to content

Commit f54b118

Browse files
authored
πŸ”Š fix: warn when summarization.trigger.type is unrecognized (#108)
* πŸ”Š fix: warn when `trigger.type` is unrecognized `shouldTriggerSummarization` silently returned `false` whenever the configured `trigger.type` was not one of the three it implements (`token_ratio`, `remaining_tokens`, `messages_to_refine`). That turned any upstream schema/SDK drift into a silent no-op β€” summarization just never fired, with no signal to the operator. Emit a one-time `console.warn` per unrecognized type so the misconfiguration surfaces in logs without spamming every agent turn. Behavior is unchanged for all valid trigger types and for the no-trigger-configured default. * 🧹 fix: bound the unrecognized-trigger dedup set Per Codex review: the dedup `Set` for warned trigger types was process-global and grew unboundedly. Consumers that thread dynamic or user-provided values through `trigger.type` could accumulate unique keys in long-lived workers β€” a minor but real memory vector. Cap the set at 32 entries and evict the oldest on overflow (Set preserves insertion order, so this is FIFO-ish). 32 is well above the handful of typos a legitimate deployment would ever see, and the dedup contract still holds: we never warn twice for a type that is currently in the set.
1 parent 1c7fccd commit f54b118

2 files changed

Lines changed: 147 additions & 1 deletion

File tree

β€Žsrc/summarization/__tests__/trigger.test.tsβ€Ž

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { shouldTriggerSummarization } from '@/summarization';
1+
import {
2+
shouldTriggerSummarization,
3+
_resetUnrecognizedTriggerWarnings,
4+
} from '@/summarization';
25

36
describe('shouldTriggerSummarization', () => {
47
it('uses pre-prune pressure for token_ratio triggers when messages were pruned', () => {
@@ -47,4 +50,100 @@ describe('shouldTriggerSummarization', () => {
4750

4851
expect(result).toBe(false);
4952
});
53+
54+
describe('unrecognized trigger type', () => {
55+
let warnSpy: jest.SpyInstance;
56+
57+
beforeEach(() => {
58+
_resetUnrecognizedTriggerWarnings();
59+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
60+
});
61+
62+
afterEach(() => {
63+
warnSpy.mockRestore();
64+
});
65+
66+
it('does not fire and warns once per unrecognized type', () => {
67+
const baseParams = {
68+
maxContextTokens: 2500,
69+
prePruneContextTokens: 2400,
70+
remainingContextTokens: 100,
71+
messagesToRefineCount: 4,
72+
};
73+
74+
// Cast via `unknown` because the type union guards against this at compile
75+
// time; we are intentionally exercising the runtime fallback.
76+
const result1 = shouldTriggerSummarization({
77+
...baseParams,
78+
trigger: { type: 'token_count', value: 8000 } as unknown as {
79+
type: 'token_ratio';
80+
value: number;
81+
},
82+
});
83+
84+
expect(result1).toBe(false);
85+
expect(warnSpy).toHaveBeenCalledTimes(1);
86+
expect(warnSpy.mock.calls[0][0]).toContain('token_count');
87+
expect(warnSpy.mock.calls[0][0]).toContain('token_ratio');
88+
expect(warnSpy.mock.calls[0][0]).toContain('remaining_tokens');
89+
expect(warnSpy.mock.calls[0][0]).toContain('messages_to_refine');
90+
91+
// Same unrecognized type a second time: no duplicate warning.
92+
shouldTriggerSummarization({
93+
...baseParams,
94+
trigger: { type: 'token_count', value: 8000 } as unknown as {
95+
type: 'token_ratio';
96+
value: number;
97+
},
98+
});
99+
expect(warnSpy).toHaveBeenCalledTimes(1);
100+
101+
// Different unrecognized type: warns again, once.
102+
shouldTriggerSummarization({
103+
...baseParams,
104+
trigger: { type: 'nonsense', value: 1 } as unknown as {
105+
type: 'token_ratio';
106+
value: number;
107+
},
108+
});
109+
expect(warnSpy).toHaveBeenCalledTimes(2);
110+
expect(warnSpy.mock.calls[1][0]).toContain('nonsense');
111+
});
112+
113+
it('does not grow memory unboundedly under a flood of unique types', () => {
114+
const baseParams = {
115+
maxContextTokens: 2500,
116+
prePruneContextTokens: 2400,
117+
remainingContextTokens: 100,
118+
messagesToRefineCount: 4,
119+
};
120+
121+
for (let i = 0; i < 500; i++) {
122+
shouldTriggerSummarization({
123+
...baseParams,
124+
trigger: { type: `bogus-${i}`, value: 1 } as unknown as {
125+
type: 'token_ratio';
126+
value: number;
127+
},
128+
});
129+
}
130+
131+
// Still logged each new type (up to the cap) β€” we never silently dropped
132+
// warnings; we just evicted oldest entries from the dedup set.
133+
expect(warnSpy).toHaveBeenCalledTimes(500);
134+
135+
// Re-warns for a recently-seen type that should still be in the cache
136+
// (last one just inserted). No duplicate warning means the dedup set
137+
// still functions; the size cap did not break the dedup contract.
138+
const beforeRecent = warnSpy.mock.calls.length;
139+
shouldTriggerSummarization({
140+
...baseParams,
141+
trigger: { type: 'bogus-499', value: 1 } as unknown as {
142+
type: 'token_ratio';
143+
value: number;
144+
},
145+
});
146+
expect(warnSpy).toHaveBeenCalledTimes(beforeRecent);
147+
});
148+
});
50149
});

β€Žsrc/summarization/index.tsβ€Ž

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
import type { SummarizationTrigger } from '@/types';
22

3+
const VALID_TRIGGER_TYPES = [
4+
'token_ratio',
5+
'remaining_tokens',
6+
'messages_to_refine',
7+
] as const;
8+
9+
/**
10+
* Upper bound on the dedup set for unrecognized trigger types. Bounds memory in
11+
* case a caller threads dynamic/user-provided strings through `trigger.type`.
12+
* Well above the handful of legit misconfigurations a process would ever see.
13+
*/
14+
const MAX_WARNED_TRIGGER_TYPES = 32;
15+
16+
const warnedUnrecognizedTriggerTypes = new Set<string>();
17+
18+
/**
19+
* Warn (once per process, per unrecognized type) when the configured trigger
20+
* type is something the runtime does not evaluate. Without this, a misconfigured
21+
* `trigger.type` silently disables summarization with no visible signal.
22+
*
23+
* The dedup set is size-capped; on overflow we evict the oldest entry (Set
24+
* preserves insertion order) so we keep bounded memory and still warn on
25+
* recently-seen types.
26+
*/
27+
function warnUnrecognizedTriggerType(type: string): void {
28+
if (warnedUnrecognizedTriggerTypes.has(type)) {
29+
return;
30+
}
31+
if (warnedUnrecognizedTriggerTypes.size >= MAX_WARNED_TRIGGER_TYPES) {
32+
const oldest = warnedUnrecognizedTriggerTypes.values().next().value;
33+
if (oldest !== undefined) {
34+
warnedUnrecognizedTriggerTypes.delete(oldest);
35+
}
36+
}
37+
warnedUnrecognizedTriggerTypes.add(type);
38+
console.warn(
39+
`[shouldTriggerSummarization] Unrecognized trigger.type: "${type}". ` +
40+
`Summarization will not fire. Valid values: ${VALID_TRIGGER_TYPES.join(', ')}.`
41+
);
42+
}
43+
44+
/** For tests only. Resets the dedup set so warnings can be observed again. */
45+
export function _resetUnrecognizedTriggerWarnings(): void {
46+
warnedUnrecognizedTriggerTypes.clear();
47+
}
48+
349
/**
450
* Determines whether summarization should be triggered based on the configured trigger
551
* and current context state.
@@ -98,5 +144,6 @@ export function shouldTriggerSummarization(params: {
98144
}
99145

100146
// Unrecognized trigger type: cannot evaluate, do not fire.
147+
warnUnrecognizedTriggerType(trigger.type);
101148
return false;
102149
}

0 commit comments

Comments
Β (0)