From 208c644f9cc2b98c30510237f29266202e17c1aa Mon Sep 17 00:00:00 2001 From: Steven Chong <25894545+teamchong@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:23:43 -0400 Subject: [PATCH] fix: remove sparse IDS image blocks --- src/core/factsheet.ts | 49 -------------------------------------- src/core/history.ts | 14 +---------- src/core/openai-history.ts | 7 ------ src/core/openai.ts | 24 ++++++++----------- src/core/transform.ts | 8 +++---- tests/factsheet.test.ts | 33 ------------------------- tests/openai-gpt5.test.ts | 49 +++++++++++++++++++++++++++++++++++++- 7 files changed, 62 insertions(+), 122 deletions(-) diff --git a/src/core/factsheet.ts b/src/core/factsheet.ts index e4b4137..c4581ff 100644 --- a/src/core/factsheet.ts +++ b/src/core/factsheet.ts @@ -251,52 +251,3 @@ export function factSheetText(text: string): string { const { kept } = extractFactSheetEntriesAllPages(text, FACTSHEET_PAGE_CHARS); return factSheetTextFromEntries(kept); } - -/** Shape label for an IDS-block line (visual OCR aid, not a factsheet). */ -function idsBlockLabel(tok: string): string { - if (SHAPE_HEX.test(tok)) return 'hex'; - if (SHAPE_UUID.test(tok)) return 'uuid'; - if (SHAPE_CAMEL.test(tok) && tok.length >= 8) return 'camel'; - if (tok.includes('/')) return 'path'; - if (SHAPE_NUM.test(tok) && tok.length <= 8) return 'port'; - if (SHAPE_CONST.test(tok)) return 'const'; - if (SHAPE_FLAG.test(tok)) return 'flag'; - if (SHAPE_TICKET.test(tok)) return 'code'; - return 'id'; -} - -/** - * Append a short multi-line IDS block of precision-critical tokens so each ID - * sits on its own rendered row (pure-image OCR aid at 5×8). Deterministic. - * Used on every imaged path (Claude + GPT + Grok). Grok pure-image packing - * validated 2026-07-11 (white+ids_block 7/7 4/4); other families get the same - * visual isolation as defense in depth alongside the text fact-sheet. - * Does not replace the text fact-sheet; the IDS block is rasterized into the PNG. - */ -export function appendIdsBlock(text: string, maxIds = 16): string { - if (!text) return text; - if (/\nIDS\n/.test(text) || text.startsWith('IDS\n')) return text; - const entries = - text.length <= MAX_SCAN - ? extractFactSheetEntries(text) - : extractFactSheetEntriesAllPages(text, FACTSHEET_PAGE_CHARS).kept; - // Prefer tier-0 (hex, ports, camel, flags), then tier-1 paths — same - // consequence ranking as the fact-sheet budget, but keep a few paths so - // pure-image path probes stay visible as isolated rows. - const tokens: string[] = []; - const seen = new Set(); - const push = (t: string) => { - if (seen.has(t) || tokens.length >= maxIds) return; - seen.add(t); - tokens.push(t); - }; - for (const { token } of entries) { - if (priorityTier(token) === 0) push(token); - } - for (const { token } of entries) { - if (priorityTier(token) === 1 && token.includes('/')) push(token); - } - if (tokens.length === 0) return text; - const lines = tokens.map((t) => `${idsBlockLabel(t)} ${t}`); - return `${text.replace(/\s+$/, '')}\nIDS\n${lines.join('\n')}\n`; -} diff --git a/src/core/history.ts b/src/core/history.ts index c8c455d..8eb58ff 100644 --- a/src/core/history.ts +++ b/src/core/history.ts @@ -13,7 +13,7 @@ import type { CacheControl, ContentBlock, ImageBlock, Message, TextBlock, ToolUseBlock, ToolResultBlock } from './types.js'; import { DENSE_CONTENT_CHARS_PER_IMAGE, DENSE_CONTENT_COLS, DENSE_RENDER_STYLE, neutralizeSentinel, reflow, renderTextToPngsWithCharLimit, roleSlotSegment, SLOT_MARK_ASSISTANT, SLOT_MARK_USER } from './render.js'; -import { appendIdsBlock, factSheetText } from './factsheet.js'; +import { factSheetText } from './factsheet.js'; import { bytesToBase64 } from './png.js'; /** @@ -596,18 +596,6 @@ export async function collapseHistory( chunkSlot = safeSlot; } } - // Universal pure-image IDS rows (hex/camel/path/port). Append the same - // \nIDS\n... suffix to text + slot so colorByRole wrap lines stay 1:1. - // appendIdsBlock trims trailing whitespace first, so do not slice by old length. - { - const withIds = appendIdsBlock(chunkRender); - if (withIds !== chunkRender) { - const idsAt = withIds.lastIndexOf('\nIDS\n'); - const suffix = idsAt >= 0 ? withIds.slice(idsAt) : ''; - chunkRender = withIds; - chunkSlot = chunkSlot.replace(/\s+$/, '') + suffix; - } - } // Use the dense readable profile (not full-canvas) to keep code/config legible. // colorByRole tints the structural tags so turn boundaries are scannable // in the history image; it's token-free (vision cost is by pixel dims, not PNG diff --git a/src/core/openai-history.ts b/src/core/openai-history.ts index f291e5a..a80eb6f 100644 --- a/src/core/openai-history.ts +++ b/src/core/openai-history.ts @@ -29,7 +29,6 @@ import { type RenderedImage, type RenderStyle, } from './render.js'; -import { appendIdsBlock } from './factsheet.js'; import { DEFAULT_GPT_PROFILE, GPT_MAX_HEIGHT_PX } from './gpt-model-profiles.js'; import { countTokens as o200kCountTokens } from 'gpt-tokenizer/encoding/o200k_base'; @@ -103,8 +102,6 @@ export interface GptHistoryOptions { * The returned `text` (o200k baseline + cache byte-stability) stays the * ORIGINAL, un-reflowed transcript. */ reflow: boolean; - /** Append IDS block for pure-image exact recall (isolated ID rows). Default on. */ - idsBlock: boolean; } export const GPT_HISTORY_DEFAULTS: GptHistoryOptions = { @@ -122,7 +119,6 @@ export const GPT_HISTORY_DEFAULTS: GptHistoryOptions = { style: DEFAULT_GPT_PROFILE.style, maxImages: GPT_HISTORY_MAX_IMAGES, reflow: true, - idsBlock: true, }; /** One conversation item lowered to a renderable unit. */ @@ -359,7 +355,6 @@ export async function planGptCollapse( // the chunk-snapped cache byte-stability, so it must not change shape here. const safeText = neutralizeSentinel(text); let renderText = o.reflow ? reflow(safeText) ?? safeText : text; - if (o.idsBlock) renderText = appendIdsBlock(renderText); if (!isProfitable(renderText, o.cols)) { return { ...base, reason: 'not_profitable', collapsedChars: text.length }; } @@ -438,7 +433,6 @@ export async function planGptCollapse( if (!sectionText || sectionText.length === 0) continue; const safeSection = neutralizeSentinel(sectionText); let sectionRender = o.reflow ? reflow(safeSection) ?? safeSection : sectionText; - if (o.idsBlock) sectionRender = appendIdsBlock(sectionRender); // Readable portrait strips (≤768px wide) — legible to OpenAI vision, same as // the static slab. renderTextToPngs caps each PNG at MAX_HEIGHT_PX so a tall // section pages into N images, all still well under the 10,000-patch budget. @@ -666,7 +660,6 @@ export async function planResponsesPairCollapse( const source = pairs.map((pair) => pair.text).join('\n\n'); const safe = neutralizeSentinel(source); let renderedText = o.reflow ? reflow(safe) ?? safe : safe; - if (o.idsBlock) renderedText = appendIdsBlock(renderedText); const images = await renderTextToPngs( renderedText, o.cols, o.style ?? {}, o.maxHeightPx, ); diff --git a/src/core/openai.ts b/src/core/openai.ts index db4a8d8..ce556be 100644 --- a/src/core/openai.ts +++ b/src/core/openai.ts @@ -9,6 +9,7 @@ import { renderTextToPngs, reflow, + neutralizeSentinel, shrinkColsToContent, renderCellWidth, renderCellHeight, @@ -42,7 +43,7 @@ import { type GptHistoryOptions, } from './openai-history.js'; import { HISTORY_SYNTHETIC_INTRO, HISTORY_SYNTHETIC_OUTRO } from './history.js'; -import { appendIdsBlock, factSheetText } from './factsheet.js'; +import { factSheetText } from './factsheet.js'; import { countTokens as o200kCountTokens } from 'gpt-tokenizer/encoding/o200k_base'; // Per-model GPT rendering + vision-cost profiles (portrait-strip width, image-token @@ -281,9 +282,6 @@ function gptHistoryOpts( maxHeightPx: o.gptHistory?.maxHeightPx ?? profile.maxHeightPx, style: o.gptHistory?.style ?? profile.style, maxImages: o.gptHistory?.maxImages ?? configuredHistoryMaxImages(model), - // Production path for every family: isolated IDS rows in the image plus the - // adjacent text factsheet. Opt out per request with gptHistory.idsBlock: false. - idsBlock: o.gptHistory?.idsBlock ?? true, }; } @@ -302,16 +300,14 @@ function emptyInfo(reason?: string): TransformInfo { }; } -/** Append IDS block so precision tokens get isolated pure-image rows (all models). - * Production also attaches factSheetText next to images (see slab/history below). - * IDS alone is not enough for Grok exact recall on live multi-seed. */ -function prepareImagedRenderText(text: string): string { - return appendIdsBlock(text); +function prepareImagedRenderText(text: string, reflowEnabled: boolean): string { + return maybeReflow(text.trimEnd(), reflowEnabled); } function maybeReflow(text: string, enabled: boolean): string { if (!enabled) return text; - return reflow(text) ?? text; + const safe = neutralizeSentinel(text); + return reflow(safe) ?? safe; } function isTextPart(part: unknown): part is OpenAITextPart { @@ -833,7 +829,7 @@ export async function transformOpenAIChatCompletions( const firstUser = firstUserText(req); if (firstUser) info.firstUserSha8 = await sha8(firstUser); - const combined = maybeReflow(compactSlabWhitespace(combinedRaw), o.reflow); + const combined = compactSlabWhitespace(combinedRaw).trimEnd(); if (combined.length < o.minCompressChars) { info.reason = `below_min_chars (${combined.length} < ${o.minCompressChars})`; return { body, info }; @@ -845,7 +841,7 @@ export async function transformOpenAIChatCompletions( ? ' The glyph ↵ (U+21B5) marks an original hard line break in content; treat it as a real newline.' : ''; const header = CHAT_HEADER.replace('\n====', reflowNote + '\n===='); - const renderedText = prepareImagedRenderText(header + combined); + const renderedText = prepareImagedRenderText(header + combined, o.reflow); const profile = resolveGptProfile(req.model); const maxCols = o.cols ?? profile.stripCols; const cols = Math.min( @@ -1055,7 +1051,7 @@ export async function transformOpenAIResponses( const firstUser = firstResponsesUserText(inputWasString, originalInputString, inputItems); if (firstUser) info.firstUserSha8 = await sha8(firstUser); - const combined = maybeReflow(compactSlabWhitespace(combinedRaw), o.reflow); + const combined = compactSlabWhitespace(combinedRaw).trimEnd(); if (combined.length < o.minCompressChars) { info.reason = `below_min_chars (${combined.length} < ${o.minCompressChars})`; return { body, info }; @@ -1065,7 +1061,7 @@ export async function transformOpenAIResponses( ? ' The glyph ↵ (U+21B5) marks an original hard line break in content; treat it as a real newline.' : ''; const header = RESPONSES_HEADER.replace('\n====', reflowNote + '\n===='); - const renderedText = prepareImagedRenderText(header + combined); + const renderedText = prepareImagedRenderText(header + combined, o.reflow); const profile = resolveGptProfile(req.model); const maxCols = o.cols ?? profile.stripCols; const cols = Math.min( diff --git a/src/core/transform.ts b/src/core/transform.ts index 0afbe2a..6a68967 100644 --- a/src/core/transform.ts +++ b/src/core/transform.ts @@ -35,7 +35,7 @@ import { ANTHROPIC_SLAB_COLS, renderTextToPngsWithCharLimit, } from './render.js'; -import { appendIdsBlock, factSheetText } from './factsheet.js'; +import { factSheetText } from './factsheet.js'; import { stripSchemaDescriptions, schemaHasStructure } from './schema-strip.js'; import { bytesToBase64 } from './png.js'; import { collapseHistory, HISTORY_SYNTHETIC_INTRO } from './history.js'; @@ -1358,8 +1358,7 @@ export async function textToImageBlocks( }> { // Shrink before the numCols branch so gate and renderer see the same canvas width. // If shrinkage drops below the full width, stay single-col (avoid wasting a divider column). - // IDS block: isolate precision tokens on their own rows (universal pure-image hex aid). - const renderText = appendIdsBlock(text); + const renderText = text; const effectiveCols = shrinkWidth ? shrinkColsToContent(renderText, cols) : cols; const effectiveNumCols = effectiveCols < cols ? 1 : numCols; const imgs = @@ -1705,8 +1704,7 @@ export async function transformRequest( columnNoteImg + reflowNoteImg + '\n====================== BEGIN RENDERED CONTEXT ======================\n'; - // IDS block on the imaged slab (same pure-image isolation as Grok/GPT). - const combinedWithHeader = appendIdsBlock(imageInstructionHeader + combined); + const combinedWithHeader = imageInstructionHeader + combined; // Shrink the canvas to the longest actual line in what we'll *render*, // so the gate's prediction and the renderer's output agree at the smallest // legible width. The banner above sets the natural floor — no separate diff --git a/tests/factsheet.test.ts b/tests/factsheet.test.ts index 3b542ad..c925163 100644 --- a/tests/factsheet.test.ts +++ b/tests/factsheet.test.ts @@ -4,7 +4,6 @@ import { extractFactSheetEntries, extractFactSheetEntriesAllPages, factSheetText, - appendIdsBlock, } from '../src/core/factsheet.js'; describe('factsheet extraction', () => { @@ -172,35 +171,3 @@ describe('ticket-style codes and occurrence counts', () => { }); }); - -describe('appendIdsBlock (pure-image IDS rows for all models)', () => { - it('appends an IDS block with hex, camel, path, and port labels', () => { - const text = [ - 'Done. The token cache key is a3f9c1e0b7d2. I renamed the field to tokenLedgerShard', - 'and moved the tier math into src/core/anthropic-vision.ts. Proxy stays on port 47821.', - ].join(' '); - const out = appendIdsBlock(text); - expect(out).toContain('\nIDS\n'); - expect(out).toContain('hex a3f9c1e0b7d2'); - expect(out).toContain('camel tokenLedgerShard'); - expect(out).toContain('path src/core/anthropic-vision.ts'); - expect(out).toContain('port 47821'); - // original body preserved - expect(out.startsWith(text.trimEnd()) || out.includes('token cache key is a3f9c1e0b7d2')).toBe(true); - }); - - it('is idempotent — does not double-append', () => { - const text = 'key a3f9c1e0b7d2 path src/core/x.ts port 47821'; - const once = appendIdsBlock(text); - expect(appendIdsBlock(once)).toBe(once); - }); - - it('is deterministic for cache stability', () => { - const text = 'hex a3f9c1e0b7d2 camel tokenLedgerShard path src/a/b.ts port 47821'; - expect(appendIdsBlock(text)).toBe(appendIdsBlock(text)); - }); - - it('returns the original text when nothing notable is present', () => { - expect(appendIdsBlock('the quick brown fox')).toBe('the quick brown fox'); - }); -}); diff --git a/tests/openai-gpt5.test.ts b/tests/openai-gpt5.test.ts index 5a49f31..2d5810c 100644 --- a/tests/openai-gpt5.test.ts +++ b/tests/openai-gpt5.test.ts @@ -184,6 +184,30 @@ describe('transformOpenAIChatCompletions (gpt-5.6-sol)', () => { expect(tools[0]!.function.parameters?.properties?.x?.description).toBeUndefined(); }); + it('reflows chat static context that contains a literal newline sentinel', async () => { + const system = ('first line\nsecond line quotes ↵ literally\nthird line\n').repeat(80); + const body = enc.encode(JSON.stringify({ + model: 'gpt-5.6-sol', + messages: [ + { role: 'system', content: system }, + { role: 'user', content: 'hello' }, + ], + })); + + const result = await transformOpenAIChatCompletions(body, { + charsPerToken: 1, + minCompressChars: 1, + }); + + expect(result.info.compressed).toBe(true); + const source = result.info.imageSourceText!; + expect(source).not.toContain('\n'); + expect(source).toContain('BEGIN RENDERED CONTEXT ======================↵## SYSTEM MESSAGE↵'); + expect(source).toContain('second line quotes ⏎ literally↵third line'); + expect(source).not.toMatch(/\s$/); + expect(source).not.toMatch(/↵$/); + }); + it('images GPT tool definitions even when there is no instruction context', async () => { const body = enc.encode(JSON.stringify({ model: 'gpt-5.6-sol', @@ -325,6 +349,29 @@ describe('transformOpenAIResponses (gpt-5.6-sol)', () => { expect(tools[0]!.parameters?.properties?.x?.description).toBeUndefined(); }); + it('reflows Responses static context that contains a literal newline sentinel', async () => { + const instructions = ('alpha line\nbeta line quotes ↵ literally\ngamma line\n').repeat(80); + const body = enc.encode(JSON.stringify({ + model: 'gpt-5.6-sol', + instructions, + input: [{ role: 'user', content: 'hello' }], + })); + + const result = await transformOpenAIResponses(body, { + charsPerToken: 1, + minCompressChars: 1, + }); + + expect(result.info.compressed).toBe(true); + const source = result.info.imageSourceText!; + expect(source).not.toContain('\n'); + expect(source).not.toContain('↵IDS↵'); + expect(source).toContain('BEGIN RENDERED CONTEXT ======================↵## INSTRUCTIONS↵'); + expect(source).toContain('beta line quotes ⏎ literally↵gamma line'); + expect(source).not.toMatch(/\s$/); + expect(source).not.toMatch(/↵$/); + }); + it('images developer/system items whose content is an input_text part array, not just a string', async () => { // Responses allows message content as a string OR an array of parts. The // array form for a developer/system item used to be dropped: not imaged and @@ -956,7 +1003,7 @@ describe('resolveGptProfile (Claude on Responses)', () => { describe('resolveGptProfile (Grok)', () => { it('uses pure-image 5x8 packing with shorter white pages under 768px short side', () => { - // 2026-07-11 pure-image 5x8: white AA + IDS block is the stable 4/4 recipe + // Production uses the pure-image 5x8 white AA profile. // (7/7 retest). No grid; paperGray 240 confabulates ports. Width stays 768. const p = resolveGptProfile('grok-4.5'); expect(p.stripCols).toBe(152);