diff --git a/src/core/transform.ts b/src/core/transform.ts index fdee258..81a900b 100644 --- a/src/core/transform.ts +++ b/src/core/transform.ts @@ -159,6 +159,20 @@ const DYNAMIC_BLOCK_TAGS = [ 'system-reminder', ] as const; +/** + * Tag-shaped blocks that DO appear in the static slab and SHOULD be baked into + * the cached image. These are part of Claude Code's built-in system prompt / + * tool documentation, not per-turn injections, so they're stable across turns. + * + * The canary in splitStaticDynamic flags any tag-shaped block in the static + * slab that isn't in DYNAMIC_BLOCK_TAGS — designed to catch a new Claude Code + * release that ships a per-turn tag we'd accidentally cache. Without this + * allowlist, known-static tags like trigger the canary on most turns + * and drown out the real signal. Add a tag here only after confirming it's + * static (appears in the cacheable part of the prompt, not rotated per turn). + */ +const KNOWN_STATIC_TAGS = ['types'] as const; + function splitStaticDynamic(text: string): { staticText: string; dynamicText: string; @@ -190,12 +204,14 @@ function splitStaticDynamic(text: string): { // collapse. Surfacing the tag name as telemetry lets us detect that // within hours of a Claude Code release. const known = new Set(DYNAMIC_BLOCK_TAGS); + const knownStatic = new Set(KNOWN_STATIC_TAGS); const sniffer = /<([a-zA-Z][a-zA-Z0-9_-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g; const unknown = new Set(); let s: RegExpExecArray | null; while ((s = sniffer.exec(staticBuf)) !== null) { const tag = s[1]!; - if (!known.has(tag) && tag.length <= 64) unknown.add(tag); + if (!known.has(tag) && !knownStatic.has(tag) && tag.length <= 64) + unknown.add(tag); } return { diff --git a/src/node.ts b/src/node.ts index 6ee2cd4..871f02b 100644 --- a/src/node.ts +++ b/src/node.ts @@ -369,7 +369,7 @@ async function main(): Promise { if (e.info?.unknownStaticTags && e.info.unknownStaticTags.length > 0) { console.warn( `[pixelpipe warn] unknown tag(s) in static slab: ${e.info.unknownStaticTags.join(', ')} ` + - `— may need to add to DYNAMIC_BLOCK_TAGS in src/core/transform.ts`, + `— may need to add to DYNAMIC_BLOCK_TAGS (per-turn) or KNOWN_STATIC_TAGS (static) in src/core/transform.ts`, ); } diff --git a/tests/render.test.ts b/tests/render.test.ts index 3259fa2..ab4891e 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -322,6 +322,23 @@ describe('transform', () => { expect(info.unknownStaticTags).not.toContain('env'); }); + it('does not flag as an unknown tag (it lives in KNOWN_STATIC_TAGS)', async () => { + const sys = + 'claude.md\n'.repeat(400) + + '\nstring\nnumber\n\n' + + '\nWorking directory: /tmp\n'; + const body = new TextEncoder().encode( + JSON.stringify({ + model: 'claude', + messages: [{ role: 'user', content: 'hi' }], + system: sys, + }), + ); + const { info } = await transformRequest(body); + // is known-static; it should NOT show up as an unknown tag. + expect(info.unknownStaticTags).toBeUndefined(); + }); + it('omits unknownStaticTags when the static slab has no tag-shaped blocks', async () => { const sys = 'claude.md\n'.repeat(400) + '\nWorking directory: /tmp\n'; const body = new TextEncoder().encode(