MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / dedupHistory

Function dedupHistory

src/agent/dedup.ts:63–116  ·  view source on GitHub ↗
(messages: Message[], opts: DedupOptions = {})

Source from the content-addressed store, hash-verified

61 * Pure: returns a new array; the input is unchanged.
62 */
63export function dedupHistory(messages: Message[], opts: DedupOptions = {}): DedupResult {
64 const minBytes = opts.minBytes ?? 200;
65 const keepRecent = opts.keepRecent ?? 4;
66 const safeTools = opts.extraSafeTools
67 ? new Set([...SAFE_FOR_DEDUP, ...opts.extraSafeTools])
68 : SAFE_FOR_DEDUP;
69
70 // First pass: index every tool result by (toolName + contentHash) → first turn index.
71 // We need toolName, which is stored on the tool message as `name` (we set this in the
72 // agent loop when persisting). Without it, dedup is skipped for that message.
73 type Indexed = { index: number; toolName: string; hash: string };
74 const firstSeen = new Map<string, Indexed>(); // key = toolName + ':' + hash
75
76 // Locate tool-message indices, in order
77 const toolIdxs: number[] = [];
78 for (let i = 0; i < messages.length; i++) {
79 if (messages[i]!.role === 'tool') toolIdxs.push(i);
80 }
81 // Mark which tool messages are within the "keep recent" tail
82 const tailStart = Math.max(0, toolIdxs.length - keepRecent);
83
84 const out = messages.slice(); // shallow copy; we'll replace specific entries
85 let replaced = 0;
86 let bytesSaved = 0;
87
88 for (let pos = 0; pos < toolIdxs.length; pos++) {
89 const i = toolIdxs[pos]!;
90 const msg = out[i]!;
91 const content = msg.content ?? '';
92 const toolName = (msg as any).name as string | undefined;
93 if (!toolName || !safeTools.has(toolName)) continue;
94 if (content.length < minBytes) continue;
95 if (pos >= tailStart) continue; // keep recent results full
96
97 const hash = hashContent(content);
98 const key = toolName + ':' + hash;
99 const seen = firstSeen.get(key);
100 if (seen) {
101 // Build a pointer line that's also useful to the model — same toolName + hash
102 // means the model knows nothing changed without us saying it changed/didn't.
103 const pointer =
104 `[DEDUP] Same content as turn earlier in this session ` +
105 `(tool=${toolName}, sha=${hash.slice(0, 10)}, ${content.length}B suppressed). ` +
106 `Reuse what you already saw; call the tool again if you suspect the file changed.`;
107 out[i] = { ...msg, content: pointer };
108 replaced += 1;
109 bytesSaved += content.length - pointer.length;
110 } else {
111 firstSeen.set(key, { index: i, toolName, hash });
112 }
113 }
114
115 return { messages: out, replaced, bytesSaved };
116}
117
118function hashContent(s: string): string {
119 return crypto.createHash('sha256').update(s).digest('hex');

Callers 2

phase-a.test.tsFile · 0.85
runMethod · 0.85

Calls 5

hashContentFunction · 0.70
pushMethod · 0.45
hasMethod · 0.45
getMethod · 0.45
setMethod · 0.45

Tested by

no test coverage detected