| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createHash } from 'crypto'; |
|
|
| function positiveIntEnv(name, fallback) { |
| const n = parseInt(process.env[name] || '', 10); |
| return Number.isFinite(n) && n > 0 ? n : fallback; |
| } |
|
|
| const POOL_TTL_MS = positiveIntEnv('CASCADE_POOL_TTL_MS', 30 * 60 * 1000); |
| const POOL_MAX = 500; |
| const KEY_VERSION = 2; |
|
|
| const _pool = new Map(); |
|
|
| const stats = { hits: 0, misses: 0, stores: 0, evictions: 0, expired: 0 }; |
|
|
| function sha256(s) { |
| return createHash('sha256').update(s).digest('hex'); |
| } |
|
|
| function shortDigest(s, n = 16) { |
| return sha256(String(s ?? '')).slice(0, n); |
| } |
|
|
| |
| |
| |
| |
| |
| const META_TAG_NAMES = new Set([ |
| 'system-reminder', |
| 'command-message', |
| 'command-name', |
| 'command-args', |
| 'local-command-stdout', |
| 'local-command-stderr', |
| 'user-prompt-submit-hook', |
| 'analysis', |
| 'summary', |
| 'example', |
| ]); |
|
|
| function buildMetaTagRe() { |
| const escaped = [...META_TAG_NAMES].map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); |
| return new RegExp( |
| `<(${escaped.join('|')})[^>]*>[\\s\\S]*?</\\1>`, |
| 'g' |
| ); |
| } |
| let META_TAG_RE = buildMetaTagRe(); |
|
|
| function stripMetaTags(s) { |
| if (typeof s !== 'string' || !s) return s; |
| const stripped = s.replace(META_TAG_RE, '').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(); |
| |
| |
| const remaining = stripped.match(/<([a-z][-a-z_]*)[^>]*>[\s\S]*?<\/\1>/g); |
| if (remaining?.length) { |
| const tagNames = remaining.map(m => m.match(/^<([a-z][-a-z_]*)/)?.[1]).filter(Boolean); |
| const unknown = tagNames.filter(t => !META_TAG_NAMES.has(t)); |
| if (unknown.length) { |
| console.error(`[META_TAG_AUDIT] Unknown XML tags in user message: ${[...new Set(unknown)].join(', ')}`); |
| } |
| } |
| return stripped; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function normalizeSystemPromptForHash(s) { |
| let out = String(s || ''); |
| |
| out = out |
| |
| .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g, '<ts>') |
| |
| .replace(/\b(Today(?:'s)?\s+(?:date|is)(?:\s+is)?\s*[:\-]?\s*)\d{4}-\d{2}-\d{2}/gi, '$1<date>') |
| |
| .replace(/(?<!\d)\d{4}-\d{2}-\d{2}(?!\d|T)/g, '<date>') |
| |
| .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<uuid>') |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| .replace(/(^[ \t]*[-β’]?\s*(?:Working\s+directory|Current\s+working\s+directory|cwd|CWD)\s*[:οΌ])[^\n]*/gim, '$1 <cwd>') |
| |
| .replace(/(^[ \t]*[-β’]?\s*(?:Current\s+(?:date|time)|Time)\s*[:οΌ])[^\n]*/gim, '$1 <time>') |
| |
| .replace(/(^[ \t]*[-β’]?\s*(?:Session\s*ID|sessionId|session_id)\s*[:οΌ])[^\n]*/gim, '$1 <sessionid>') |
| |
| |
| |
| |
| |
| .replace(/(?<![\d.])(?:1[7-9]|20)\d{8}(?:\d{3})?(?![\d.])/g, '<epoch>'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const NEXT_HEADING = '(?:Status|Recent commits|Recent files|gitStatus|Current branch|Main branch|Git user)\\s*:'; |
| const blockEnd = `(?=^[ \\t]*${NEXT_HEADING}|^\\s*$|$(?![\\s\\S]))`; |
| out = out.replace( |
| new RegExp(`^([ \\t]*Status\\s*:)[ \\t]*\\n[\\s\\S]*?${blockEnd}`, 'gim'), |
| '$1\n<git-status>\n', |
| ); |
| out = out.replace( |
| new RegExp(`^([ \\t]*Recent commits\\s*:)[ \\t]*\\n[\\s\\S]*?${blockEnd}`, 'gim'), |
| '$1\n<recent-commits>\n', |
| ); |
| out = out.replace( |
| new RegExp(`^([ \\t]*Recent files\\s*:)[ \\t]*\\n[\\s\\S]*?${blockEnd}`, 'gim'), |
| '$1\n<recent-files>\n', |
| ); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| out = out.replace(/(?<![`'"\w])(?=[a-f0-9]*\d)(?=[a-f0-9]*[a-f])[a-f0-9]{7,12}(?![`'"\w])/gi, '<gitsha>'); |
|
|
| return out; |
| } |
|
|
| |
| |
| |
| function stableStringify(v) { |
| if (v === null || typeof v !== 'object') return JSON.stringify(v); |
| if (Array.isArray(v)) return '[' + v.map(stableStringify).join(',') + ']'; |
| const keys = Object.keys(v).sort(); |
| return '{' + keys.map(k => JSON.stringify(k) + ':' + stableStringify(v[k])).join(',') + '}'; |
| } |
|
|
| |
| |
| |
| |
| |
| function canonicalContentBlock(part) { |
| if (typeof part?.text === 'string') return { type: 'text', text: stripMetaTags(part.text) }; |
| if (typeof part === 'string') return { type: 'text', text: stripMetaTags(part) }; |
| const type = String(part?.type || '').toLowerCase(); |
| |
| if (type === 'image_url' || type === 'image' || type === 'input_image') { |
| const url = part?.image_url?.url || part?.url || ''; |
| if (typeof url === 'string' && url.startsWith('data:')) { |
| const comma = url.indexOf(','); |
| const meta = comma > 0 ? url.slice(5, comma) : ''; |
| const data = comma > 0 ? url.slice(comma + 1) : url; |
| return { type: 'image', meta, hash: shortDigest(data, 16) }; |
| } |
| if (typeof url === 'string' && url) return { type: 'image', url }; |
| if (typeof part?.source === 'object') { |
| const src = part.source; |
| if (src.type === 'base64' && typeof src.data === 'string') { |
| return { type: 'image', meta: src.media_type || '', hash: shortDigest(src.data, 16) }; |
| } |
| if (src.type === 'url' && typeof src.url === 'string') { |
| return { type: 'image', url: src.url }; |
| } |
| if (typeof src.file_id === 'string') return { type: 'image', file_id: src.file_id }; |
| } |
| return { type: 'image', unhashable: true }; |
| } |
| |
| if (type === 'document' || type === 'file' || type === 'input_file') { |
| const fileId = part?.file_id || part?.source?.file_id; |
| if (typeof fileId === 'string') return { type: 'file', file_id: fileId }; |
| if (part?.source?.type === 'base64' && typeof part.source.data === 'string') { |
| return { type: 'file', meta: part.source.media_type || '', hash: shortDigest(part.source.data, 16) }; |
| } |
| if (typeof part?.source?.url === 'string') return { type: 'file', url: part.source.url }; |
| return { type: 'file', unhashable: true }; |
| } |
| |
| |
| return { type: type || 'unknown', json: stableStringify(part ?? '') }; |
| } |
|
|
| function canonicaliseContent(content) { |
| if (typeof content === 'string') return [{ type: 'text', text: stripMetaTags(content) }]; |
| if (!Array.isArray(content)) return [{ type: 'json', json: stableStringify(content ?? '') }]; |
| return content.map(canonicalContentBlock); |
| } |
|
|
| function hasUnhashableMedia(blocks) { |
| return Array.isArray(blocks) && blocks.some(b => b?.unhashable === true); |
| } |
|
|
| |
| |
| |
| |
| function projectAssistantToolCalls(m) { |
| const calls = []; |
| if (Array.isArray(m?.tool_calls)) { |
| for (const tc of m.tool_calls) { |
| const name = tc?.function?.name || tc?.name || ''; |
| const args = tc?.function?.arguments; |
| let argsCanonical; |
| if (typeof args === 'string') { |
| try { argsCanonical = stableStringify(JSON.parse(args)); } |
| catch { argsCanonical = args; } |
| } else if (args !== undefined) { |
| argsCanonical = stableStringify(args); |
| } else if (tc?.input !== undefined) { |
| argsCanonical = stableStringify(tc.input); |
| } else { |
| argsCanonical = ''; |
| } |
| calls.push({ name, args: argsCanonical }); |
| } |
| } |
| if (Array.isArray(m?.content)) { |
| for (const part of m.content) { |
| if (part?.type === 'tool_use') { |
| calls.push({ name: part.name || '', args: stableStringify(part.input ?? null) }); |
| } |
| } |
| } |
| return calls; |
| } |
|
|
| function projectMessage(m) { |
| const role = m?.role; |
| if (role === 'system') { |
| const blocks = canonicaliseContent(m.content); |
| return { role: 'system', content: blocks }; |
| } |
| if (role === 'user') { |
| const blocks = canonicaliseContent(m.content); |
| return { role: 'user', content: blocks }; |
| } |
| if (role === 'tool') { |
| return { |
| role: 'tool_result', |
| tool_call_id: typeof m?.tool_call_id === 'string' ? m.tool_call_id : '', |
| content: canonicaliseContent(m.content), |
| }; |
| } |
| if (role === 'assistant') { |
| |
| |
| const blocks = canonicaliseContent(m.content); |
| const text = blocks |
| .filter(b => b.type === 'text') |
| .map(b => (b.text || '').replace(/\s+/g, ' ').trim()) |
| .join('\n') |
| .trim(); |
| const toolCalls = projectAssistantToolCalls(m); |
| return { role: 'assistant', text, tool_calls: toolCalls }; |
| } |
| |
| |
| return { role: String(role || 'unknown'), content: canonicaliseContent(m?.content) }; |
| } |
|
|
| function systemDigest(messages) { |
| |
| |
| |
| |
| |
| if (process.env.CASCADE_REUSE_HASH_SYSTEM === '0') return ''; |
| const sys = messages.filter(m => m?.role === 'system'); |
| if (!sys.length) return ''; |
| |
| |
| |
| const normalized = sys.map(m => { |
| const projected = projectMessage(m); |
| if (Array.isArray(projected.content)) { |
| projected.content = projected.content.map(b => { |
| if (b?.type === 'text' && typeof b.text === 'string') { |
| return { ...b, text: normalizeSystemPromptForHash(b.text) }; |
| } |
| return b; |
| }); |
| } |
| return projected; |
| }); |
| return shortDigest(stableStringify(normalized), 32); |
| } |
|
|
| function toolContextDigest(opts = {}) { |
| if (!opts.emulateTools) return ''; |
| |
| |
| |
| const tools = (Array.isArray(opts.tools) ? opts.tools.map(t => { |
| const fn = t?.function || t; |
| return { |
| name: fn?.name || '', |
| description: fn?.description || '', |
| parameters: fn?.parameters ?? fn?.input_schema ?? null, |
| }; |
| }) : []).sort((a, b) => (a.name || '').localeCompare(b.name || '')); |
| return shortDigest(stableStringify({ |
| tools, |
| tool_choice: opts.toolChoice ?? null, |
| preambleTier: opts.preambleTier ?? null, |
| toolPreambleHash: opts.toolPreamble ? shortDigest(opts.toolPreamble, 16) : '', |
| }), 32); |
| } |
|
|
| |
| |
| |
| function priorTurnsForBefore(messages) { |
| if (!Array.isArray(messages)) return null; |
| |
| let newestStable = -1; |
| for (let i = messages.length - 1; i >= 0; i--) { |
| const r = messages[i]?.role; |
| if (r === 'user' || r === 'tool') { newestStable = i; break; } |
| } |
| if (newestStable < 0) return null; |
| |
| if (newestStable === 0) return null; |
| return messages.slice(0, newestStable); |
| } |
|
|
| function projectTurns(turns) { |
| if (!Array.isArray(turns)) return null; |
| const projected = []; |
| for (const m of turns) { |
| if (m?.role === 'system') continue; |
| const p = projectMessage(m); |
| if (Array.isArray(p.content) && hasUnhashableMedia(p.content)) return { unhashable: true }; |
| projected.push(p); |
| } |
| return { turns: projected }; |
| } |
|
|
| function buildKeyPayload({ messages, modelKey, callerKey, opts, scope }) { |
| const sys = systemDigest(messages); |
| const tools = toolContextDigest(opts); |
| const turnSlice = scope === 'after' ? messages : priorTurnsForBefore(messages); |
| if (!turnSlice) return null; |
| const projection = projectTurns(turnSlice); |
| if (!projection) return null; |
| if (projection.unhashable) return null; |
| return stableStringify({ |
| v: KEY_VERSION, |
| caller: String(callerKey || ''), |
| model: String(modelKey || ''), |
| route: opts?.route || 'chat', |
| sys, |
| tools, |
| turns: projection.turns, |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function fingerprintBefore(messages, modelKey = '', callerKey = '', opts = {}) { |
| const payload = buildKeyPayload({ messages, modelKey, callerKey, opts, scope: 'before' }); |
| if (!payload) return null; |
| return sha256(payload); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function fingerprintAfter(messages, modelKey = '', callerKey = '', opts = {}) { |
| if (!Array.isArray(messages) || !messages.length) return null; |
| |
| |
| const sys = systemDigest(messages); |
| const tools = toolContextDigest(opts); |
| const projection = projectTurns(messages.filter(m => m?.role !== 'system')); |
| if (!projection || projection.unhashable) return null; |
| return sha256(stableStringify({ |
| v: KEY_VERSION, |
| caller: String(callerKey || ''), |
| model: String(modelKey || ''), |
| route: opts?.route || 'chat', |
| sys, |
| tools, |
| turns: projection.turns, |
| })); |
| } |
|
|
| function effectiveTtl(entry) { |
| const hint = Number(entry?.ttlHintMs); |
| return Number.isFinite(hint) && hint > 0 ? hint : POOL_TTL_MS; |
| } |
|
|
| function prune(now) { |
| for (const [fp, e] of _pool) { |
| if (now - e.lastAccess > effectiveTtl(e)) { _pool.delete(fp); stats.expired++; } |
| } |
| if (_pool.size <= POOL_MAX) return; |
| const entries = [..._pool.entries()].sort((a, b) => a[1].lastAccess - b[1].lastAccess); |
| const toDrop = entries.length - POOL_MAX; |
| for (let i = 0; i < toDrop; i++) { |
| _pool.delete(entries[i][0]); |
| stats.evictions++; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function checkout(fingerprint, callerKey = '', expected = null) { |
| if (!fingerprint) { stats.misses++; return null; } |
| const entry = _pool.get(fingerprint); |
| if (!entry) { stats.misses++; return null; } |
|
|
| |
| |
| |
| |
| |
| |
| |
| if (entry.callerKey && callerKey && entry.callerKey !== callerKey) { |
| stats.misses++; |
| return null; |
| } |
| if (Date.now() - entry.lastAccess > effectiveTtl(entry)) { |
| _pool.delete(fingerprint); |
| stats.expired++; |
| stats.misses++; |
| return null; |
| } |
| if (expected) { |
| if (expected.apiKey && entry.apiKey && expected.apiKey !== entry.apiKey) { stats.misses++; return null; } |
| if (expected.lsPort && entry.lsPort && expected.lsPort !== entry.lsPort) { stats.misses++; return null; } |
| if (expected.lsGeneration != null && entry.lsGeneration != null && expected.lsGeneration !== entry.lsGeneration) { |
| stats.misses++; |
| return null; |
| } |
| } |
|
|
| |
| _pool.delete(fingerprint); |
| stats.hits++; |
| return entry; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function checkin(fingerprint, entry, callerKey = '', ttlHintMs) { |
| if (!entry) return; |
| const fingerprints = Array.isArray(fingerprint) |
| ? fingerprint.filter((fp) => typeof fp === 'string' && fp) |
| : (fingerprint ? [fingerprint] : []); |
| if (!fingerprints.length) return; |
| const now = Date.now(); |
| let resolvedHint; |
| if (ttlHintMs === undefined) { |
| resolvedHint = entry.ttlHintMs; |
| } else if (ttlHintMs === null || !Number.isFinite(ttlHintMs) || ttlHintMs <= 0) { |
| resolvedHint = undefined; |
| } else { |
| resolvedHint = ttlHintMs; |
| } |
| |
| |
| |
| |
| for (const fp of fingerprints) { |
| _pool.set(fp, { |
| cascadeId: entry.cascadeId, |
| sessionId: entry.sessionId, |
| lsPort: entry.lsPort, |
| lsGeneration: entry.lsGeneration, |
| apiKey: entry.apiKey, |
| callerKey: callerKey || entry.callerKey || '', |
| stepOffset: Number.isFinite(entry.stepOffset) ? entry.stepOffset : 0, |
| generatorOffset: Number.isFinite(entry.generatorOffset) ? entry.generatorOffset : 0, |
| historyCoverage: entry.historyCoverage || null, |
| createdAt: entry.createdAt || now, |
| lastAccess: now, |
| ...(Number.isFinite(resolvedHint) && resolvedHint > 0 ? { ttlHintMs: resolvedHint } : {}), |
| }); |
| } |
| |
| |
| |
| |
| stats.stores++; |
| if (fingerprints.length > 1) stats.aliasWrites = (stats.aliasWrites || 0) + (fingerprints.length - 1); |
| prune(now); |
| } |
|
|
| |
| |
| |
| |
| |
| export function invalidateFor({ apiKey, lsPort, lsGeneration } = {}) { |
| let dropped = 0; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const targetCascadeIds = new Set(); |
| for (const [, e] of _pool) { |
| let hit = false; |
| if (apiKey && e.apiKey === apiKey) hit = true; |
| if (!hit && lsPort && e.lsPort === lsPort) { |
| if (lsGeneration == null || e.lsGeneration == null || e.lsGeneration === lsGeneration) hit = true; |
| } |
| if (hit && e.cascadeId) targetCascadeIds.add(e.cascadeId); |
| } |
| for (const [fp, e] of _pool) { |
| let drop = false; |
| if (apiKey && e.apiKey === apiKey) drop = true; |
| if (!drop && lsPort && e.lsPort === lsPort) { |
| if (lsGeneration == null || e.lsGeneration == null || e.lsGeneration === lsGeneration) drop = true; |
| } |
| |
| |
| |
| |
| if (!drop && e.cascadeId && targetCascadeIds.has(e.cascadeId)) drop = true; |
| if (drop) { |
| _pool.delete(fp); |
| dropped++; |
| } |
| } |
| return dropped; |
| } |
|
|
| export function poolStats() { |
| return { |
| size: _pool.size, |
| maxSize: POOL_MAX, |
| ttlMs: POOL_TTL_MS, |
| ...stats, |
| hitRate: stats.hits + stats.misses > 0 |
| ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(1) |
| : '0.0', |
| }; |
| } |
|
|
| export function poolClear() { |
| const n = _pool.size; |
| _pool.clear(); |
| return n; |
| } |
|
|
| |
| |
| |
| setInterval(() => prune(Date.now()), 5 * 60 * 1000).unref(); |
|
|