mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
fix(render): reduce dense page size
This commit is contained in:
@@ -113,47 +113,6 @@ export interface HistoryCollapseInfo {
|
||||
droppedCodepoints: Map<number, number>;
|
||||
}
|
||||
|
||||
const MAX_VERBATIM_GIT_STATUS_CHARS = 8000;
|
||||
|
||||
function toolResultTextContent(block: ToolResultBlock): string {
|
||||
const inner = block.content;
|
||||
if (typeof inner === 'string') return inner;
|
||||
if (!Array.isArray(inner)) return '';
|
||||
const parts: string[] = [];
|
||||
for (const sub of inner) {
|
||||
if (sub && (sub as TextBlock).type === 'text') parts.push((sub as TextBlock).text);
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function isGitStatusShortOutput(text: string): boolean {
|
||||
if (!text || text.length > MAX_VERBATIM_GIT_STATUS_CHARS) return false;
|
||||
const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
|
||||
if (lines.length === 0) return false;
|
||||
return lines.every((line) => /^(?:[ MADRCUT?!]{2}|[?!]{2}) .+/.test(line));
|
||||
}
|
||||
|
||||
function collectVerbatimGitStatusOutputs(
|
||||
messages: Message[],
|
||||
upToExclusive: number,
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
let used = 0;
|
||||
const limit = Math.min(upToExclusive, messages.length);
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const msg = messages[i]!;
|
||||
if (!Array.isArray(msg.content)) continue;
|
||||
for (const blk of msg.content) {
|
||||
if (!blk || (blk as { type?: string }).type !== 'tool_result') continue;
|
||||
const text = toolResultTextContent(blk as ToolResultBlock);
|
||||
if (!isGitStatusShortOutput(text)) continue;
|
||||
if (used + text.length > MAX_VERBATIM_GIT_STATUS_CHARS) return out;
|
||||
out.push(text);
|
||||
used += text.length;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the largest index `i*` in `[0..messages.length - keepTail - 1]`
|
||||
@@ -409,21 +368,9 @@ export async function collapseHistory(
|
||||
}
|
||||
}
|
||||
// Build the synthetic user message.
|
||||
const verbatimGitStatusOutputs = collectVerbatimGitStatusOutputs(messages, collapseLen);
|
||||
const verbatimBlocks: TextBlock[] = verbatimGitStatusOutputs.length > 0
|
||||
? [{
|
||||
type: 'text',
|
||||
text: [
|
||||
'[Exact git status --short output preserved as text. Use this block as the source of truth over the rendered history image.]',
|
||||
...verbatimGitStatusOutputs.map((out, i) => `--- git status --short #${i + 1} ---\n${out}`),
|
||||
'[End exact git status --short output.]',
|
||||
].join('\n\n'),
|
||||
}]
|
||||
: [];
|
||||
const syntheticContent: ContentBlock[] = [
|
||||
{ type: 'text', text: '[Earlier in this conversation:]' },
|
||||
...imageBlocks,
|
||||
...verbatimBlocks,
|
||||
{ type: 'text', text: '[End of earlier context.]' },
|
||||
];
|
||||
const syntheticUser: Message = {
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ export const READABLE_CHARS_PER_IMAGE = 50000;
|
||||
* and collapsed history). The 50k canvas-max page is token-efficient but too
|
||||
* dense for OCR on lockfiles/JSON/code. Keep those blocks paged into smaller
|
||||
* images so the model can read them reliably. */
|
||||
export const DENSE_CONTENT_CHARS_PER_IMAGE = 6000;
|
||||
export const DENSE_CONTENT_CHARS_PER_IMAGE = 5000;
|
||||
export const DENSE_CONTENT_COLS = 180;
|
||||
export const DENSE_RENDER_STYLE: RenderStyle = { cellWBonus: 2, cellHBonus: 2, aa: true };
|
||||
/** Default columns per row. 1568 px / 5 px-per-cell = 313 cells. We render
|
||||
|
||||
+15
-24
@@ -24,6 +24,7 @@ import {
|
||||
HISTORY_DEFAULTS,
|
||||
} from '../src/core/history.js';
|
||||
import { transformRequest, isCompressionProfitable } from '../src/core/transform.js';
|
||||
import { DENSE_CONTENT_CHARS_PER_IMAGE } from '../src/core/render.js';
|
||||
import type { Message } from '../src/core/types.js';
|
||||
|
||||
// A tiny helper so test fixtures are readable.
|
||||
@@ -312,23 +313,12 @@ describe('collapseHistory', () => {
|
||||
expect(out[2]).toBe(msgs[11]);
|
||||
});
|
||||
|
||||
it('preserves git status --short tool output as exact text alongside history images', async () => {
|
||||
const status = [
|
||||
'M package-lock.json',
|
||||
'M package.json',
|
||||
'M packages/capnweb-typecheck/DESIGN.md',
|
||||
'M packages/capnweb-typecheck/src/index.ts',
|
||||
' M packages/capnweb-typecheck/src/plugin.ts',
|
||||
'M tsdown.config.ts',
|
||||
'?? handoff.md',
|
||||
'?? packages/capnweb-typecheck/src/transform/',
|
||||
].join('\n');
|
||||
const msgs: Message[] = [
|
||||
usr('run git status'),
|
||||
asst([{ type: 'tool_use', id: 'bash-1', name: 'bash', input: { command: 'git status --short' } }]),
|
||||
usr([{ type: 'tool_result', tool_use_id: 'bash-1', content: status }]),
|
||||
asst('noted'),
|
||||
];
|
||||
it('splits dense collapsed history into readable image pages without plaintext side channels', async () => {
|
||||
const body = Array.from(
|
||||
{ length: 180 },
|
||||
(_, i) => `line ${i}: ${'x'.repeat(80)}`,
|
||||
).join('\n');
|
||||
const msgs: Message[] = [usr(body), asst(body), usr(body)];
|
||||
|
||||
const { messages: out, info } = await collapseHistory(msgs, () => true, {
|
||||
keepTail: 0,
|
||||
@@ -337,14 +327,15 @@ describe('collapseHistory', () => {
|
||||
});
|
||||
|
||||
expect(info.reason).toBe(undefined);
|
||||
const content = out[0]!.content as Array<Record<string, unknown>>;
|
||||
expect(content.some((c) => c.type === 'image')).toBe(true);
|
||||
const exact = content.find(
|
||||
(c) => c.type === 'text' && typeof c.text === 'string' && c.text.includes('source of truth'),
|
||||
expect(info.collapsedImages).toBeGreaterThanOrEqual(
|
||||
Math.ceil(info.collapsedChars / DENSE_CONTENT_CHARS_PER_IMAGE),
|
||||
);
|
||||
expect(exact).toBeTruthy();
|
||||
expect((exact!.text as string)).toContain(status);
|
||||
for (const line of status.split('\n')) expect((exact!.text as string)).toContain(line);
|
||||
const content = out[0]!.content as Array<Record<string, unknown>>;
|
||||
expect(content.filter((c) => c.type === 'text')).toEqual([
|
||||
{ type: 'text', text: '[Earlier in this conversation:]' },
|
||||
{ type: 'text', text: '[End of earlier context.]' },
|
||||
]);
|
||||
expect(content.filter((c) => c.type === 'image')).toHaveLength(info.collapsedImages);
|
||||
});
|
||||
|
||||
it('preserves a tool_use sequence that straddles the live-tail boundary', async () => {
|
||||
|
||||
@@ -92,6 +92,29 @@ describe('dense readable render profile', () => {
|
||||
expect(imgs[0]!.width).toBeGreaterThan(1000);
|
||||
expect(imgs[0]!.width).toBeLessThanOrEqual(1568);
|
||||
});
|
||||
|
||||
it('pages diff-shaped tool output at the dense readable budget', async () => {
|
||||
const diffish = Array.from(
|
||||
{ length: 160 },
|
||||
(_, i) => `${i % 2 === 0 ? '+' : '-'}const value${i} = ${i}; // ${'changed '.repeat(8)}`,
|
||||
).join('\n');
|
||||
expect(diffish.length).toBeGreaterThan(DENSE_CONTENT_CHARS_PER_IMAGE * 2);
|
||||
|
||||
const imgs = await renderTextToPngsWithCharLimit(
|
||||
diffish,
|
||||
DENSE_CONTENT_COLS,
|
||||
DENSE_CONTENT_CHARS_PER_IMAGE,
|
||||
DENSE_RENDER_STYLE,
|
||||
);
|
||||
|
||||
expect(imgs.length).toBeGreaterThanOrEqual(
|
||||
Math.ceil(diffish.length / DENSE_CONTENT_CHARS_PER_IMAGE),
|
||||
);
|
||||
for (const img of imgs) {
|
||||
expect(img.width).toBeLessThanOrEqual(1568);
|
||||
expect(img.height).toBeLessThanOrEqual(1568);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyContent', () => {
|
||||
|
||||
Reference in New Issue
Block a user