Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/core/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,13 @@ function demoteProtectedHeadText(head: Message[]): Message[] {
// Standing instructions survive verbatim; only the stale request prose demotes.
const demoteText = (text: string, cc?: CacheControl): ContentBlock[] | undefined => {
const { reminders, rest } = splitStandingInstructions(text);
const preview = compactPreview(rest);
// A protected opening turn above the normal native-text budget is exactly
// where a long task contract lives. Keep its existing bounded head+tail
// representation instead of reducing the whole contract to 300 chars.
const preview =
rest.length > USER_TEXT_MAX_CHARS
? verbatimTaskText(rest)
: compactPreview(rest);
if (reminders.length === 0) return preview ? [tomb(preview, cc)] : undefined;
const out: ContentBlock[] = reminders.map((r) => ({ type: 'text', text: r } as TextBlock));
if (preview) out.push(tomb(preview));
Expand Down
49 changes: 48 additions & 1 deletion tests/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1047,9 +1047,27 @@ describe('isCompressionProfitableAmortized — multi-turn horizon gate', () => {
// to a PRIOR-CONTEXT tombstone, live tail preserved verbatim.
// ---------------------------------------------------------------------------
describe('collapseHistory — opening-turn request quarantine (regression #14)', () => {
const COLLAPSE_CHUNK = 0;
const COLLAPSE_COLS = 100;
const FILLER_BODY_CHARS = 2800;
const FILLER_TURN_COUNT = 12;
const KEEP_TAIL = 1;
const MIN_COLLAPSE_PREFIX = 5;
const PROTECTED_PREFIX = 1;
const REQUIRED_API_COUNT = 30;
const SLAB_DATA = 'U0xBQg=='; // base64("SLAB") — the recognition / cache anchor
const OPENING_REQUEST = 'can you update the ux to add a sonnet button';
const LIVE_REQUEST = 'LIVE: enforce live=last-user invariant and fail closed';
const REQUIRED_API_NAMES = Array.from(
{ length: REQUIRED_API_COUNT },
(_, index) => `requiredApiFunction${index}`,
);
const REQUIRED_API_REQUIREMENTS = REQUIRED_API_NAMES.map(
(name, index) =>
`${index + 1}. MUST implement ${name}; map InvalidArgument to TypeError; ` +
'preserve boolean SELECT/expression invariants.',
);
const ENUMERATED_OPENING_REQUEST = REQUIRED_API_REQUIREMENTS.join('\n');

it('demotes the opening request to a tombstone, keeps the slab image, preserves the live tail', async () => {
const msgs: Message[] = [
Expand Down Expand Up @@ -1091,7 +1109,7 @@ describe('collapseHistory — opening-turn request quarantine (regression #14)',
expect(headText[0]!.text).toContain('PRIOR CONTEXT ONLY');
expect(headText[0]!.text).toContain('must not be acted');
expect(headText[0]!.text).toContain('<user t="0">');
expect(headText[0]!.text).toContain('Preview:'); // the ask survives only as a marked preview
expect(headText[0]!.text).toContain('Preview:'); // the ask survives inside the marked tombstone
// The bare request string never appears as a standalone clean text block anywhere.
const cleanOpeningSomewhere = out.some(
(m) =>
Expand All @@ -1118,6 +1136,7 @@ describe('collapseHistory — opening-turn request quarantine (regression #14)',
// (4) Synthetic history sits BETWEEN head and live; its recency pointer/outro
// points at the live text and never resurrects the opening request.
const synth = out[1]!;

const synthText = (synth.content as Array<Record<string, unknown>>).filter(
(c) => c.type === 'text',
) as Array<{ text: string }>;
Expand All @@ -1126,6 +1145,34 @@ describe('collapseHistory — opening-turn request quarantine (regression #14)',
expect(t.text).not.toContain(OPENING_REQUEST);
}
});

it('preserves every requirement in a protected enumerated opening task', async () => {
const msgs: Message[] = [
usr([
{ type: 'text', text: ENUMERATED_OPENING_REQUEST },
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: SLAB_DATA } },
]),
];
for (let index = 1; index <= FILLER_TURN_COUNT; index++) {
const body = `turn ${index}: ` + 'x'.repeat(FILLER_BODY_CHARS);
msgs.push(index % 2 === 1 ? asst(body) : usr(body));
}
msgs.push(usr(LIVE_REQUEST));

const { messages: out, info } = await collapseHistory(msgs, isCompressionProfitable, {
keepTail: KEEP_TAIL,
minCollapsePrefix: MIN_COLLAPSE_PREFIX,
cols: COLLAPSE_COLS,
collapseChunk: COLLAPSE_CHUNK,
protectedPrefix: PROTECTED_PREFIX,
});

expect(info.reason).toBeUndefined();
const serialized = JSON.stringify(out);
for (const requirement of REQUIRED_API_REQUIREMENTS) {
expect(serialized).toContain(requirement);
}
});
});

// ---------------------------------------------------------------------------
Expand Down