diff --git a/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts b/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts index db635d0fcf..b983d2d004 100644 --- a/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts +++ b/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts @@ -29,6 +29,7 @@ import { historyCompactCheckpointToModelMessage, historyCompactCheckpointToRuntimeEvent, isProviderHistoryCompactCheckpoint, + isSupersededHistoryCompactCheckpoint, matchHistoryCompactCheckpointPrefix, validateHistoryCompactCheckpointShape, } from '../history-compact-checkpoint.js'; @@ -594,6 +595,38 @@ describe('history compact checkpoint', () => { assert.equal(validateHistoryCompactCheckpointShape(unmarked, 'session-1'), false); }); + test('an unmarked 0.1.x checkpoint reads as superseded, a damaged one does not', () => { + // Diagnostics have to tell "this Session predates the current summary + // contract" apart from "this record is broken". Both fail shape validation, + // so the only separator is whether restoring the stamp would make it valid. + const stamped = buildHistoryCompactCheckpoint({ + sessionId: 'session-1', + coveredRuntimeEvents: [textEvent(0)], + summary: STRUCTURED_SUMMARY, + }); + const { summaryFormat: _summaryFormat, ...unmarked } = stamped; + + assert.equal(isSupersededHistoryCompactCheckpoint(unmarked, 'session-1'), true); + assert.equal(isSupersededHistoryCompactCheckpoint(stamped, 'session-1'), false); + assert.equal( + isSupersededHistoryCompactCheckpoint( + { ...stamped, source: { ...stamped.source, policyVersion: 'maka.older_policy.v0' } }, + 'session-1', + ), + true, + ); + assert.equal( + isSupersededHistoryCompactCheckpoint({ ...unmarked, coverage: undefined }, 'session-1'), + false, + ); + assert.equal( + isSupersededHistoryCompactCheckpoint({ ...unmarked, summary: '' }, 'session-1'), + false, + ); + // A record from another Session is not this Session's history. + assert.equal(isSupersededHistoryCompactCheckpoint(unmarked, 'session-2'), false); + }); + test('a marked checkpoint is held to the complete predicate at load', async () => { // A section-less summary written through a seam that bypassed the write // gates (direct recorder, older copy) but carrying the sectioned marker diff --git a/packages/runtime/src/execution-inspect.ts b/packages/runtime/src/execution-inspect.ts index 22b8eeb72d..335bd5b2b7 100644 --- a/packages/runtime/src/execution-inspect.ts +++ b/packages/runtime/src/execution-inspect.ts @@ -295,18 +295,18 @@ function inspectCompactionCheckpoints( if (event.type !== 'history_compact_checkpoint_recorded') continue; const checkpoint = event.data?.checkpoint; if (!validateHistoryCompactCheckpointShape(checkpoint, invocation.sessionId)) { - // A checkpoint recorded under an older source policy is expected history, - // not corruption: the ledger keeps every checkpoint it ever wrote, and - // every consumer fails open on it. Reporting it as an error would drown - // out the records that really are damaged. - const superseded = isSupersededHistoryCompactCheckpoint(checkpoint); + // A checkpoint recorded under an older contract is expected history, not + // corruption: the ledger keeps every checkpoint it ever wrote, and every + // consumer fails open on it. Reporting it as an error would drown out the + // records that really are damaged. + const superseded = isSupersededHistoryCompactCheckpoint(checkpoint, invocation.sessionId); diagnostics.push( diagnostic( invocation, superseded ? 'compaction_checkpoint_superseded' : 'compaction_checkpoint_invalid', superseded ? 'info' : 'error', superseded - ? 'AgentRun contains a durable Compaction checkpoint from a superseded source policy; it is ignored and re-created on demand.' + ? 'AgentRun contains a durable Compaction checkpoint from a superseded summary contract; it is ignored and re-created on demand.' : 'AgentRun contains an invalid durable Compaction checkpoint record.', event.id, ), diff --git a/packages/runtime/src/history-compact-checkpoint.ts b/packages/runtime/src/history-compact-checkpoint.ts index e19f64d4f2..24cd9f3873 100644 --- a/packages/runtime/src/history-compact-checkpoint.ts +++ b/packages/runtime/src/history-compact-checkpoint.ts @@ -491,24 +491,45 @@ export function validateHistoryCompactCheckpointShape( ); } -/** Accept forward progress, or a compare-and-swap rewrite of the exact same source coverage. */ /** * A recorded checkpoint that this Runtime no longer holds to its own contract - * because it was minted under an older source policy — not a corrupt record. + * because it was minted under an older contract — not a corrupt record. * Every consumer already fails open on it (compaction re-summarizes, copy * drops it); diagnostics use this to say so instead of crying corruption. + * + * Two generations qualify. A checkpoint carrying an older source policy names + * itself. A v0.1.x text checkpoint cannot: it predates both the source policy + * and the summary format stamp, so the only thing that separates it from a + * damaged record is that restoring the stamp would make it valid. */ -export function isSupersededHistoryCompactCheckpoint(value: unknown): boolean { +export function isSupersededHistoryCompactCheckpoint(value: unknown, sessionId?: string): boolean { if (!value || typeof value !== 'object') return false; const checkpoint = value as Partial; + if (checkpoint.kind !== 'maka.history_compact_checkpoint') return false; const policyVersion = checkpoint.source?.policyVersion as unknown; - return ( - checkpoint.kind === 'maka.history_compact_checkpoint' && + if ( typeof policyVersion === 'string' && policyVersion !== HISTORY_COMPACT_SOURCE_POLICY_VERSION + ) { + return true; + } + return isRetiredFreeformTextCheckpoint(checkpoint, sessionId); +} + +/** The unmarked v0.1.x summary contract this Runtime stopped admitting. */ +function isRetiredFreeformTextCheckpoint( + checkpoint: Partial, + sessionId?: string, +): boolean { + const text = checkpoint as Partial; + if (checkpoint.version !== 2 || text.summaryFormat !== undefined) return false; + return validateHistoryCompactCheckpointShape( + { ...checkpoint, summaryFormat: SECTIONED_SUMMARY_FORMAT }, + sessionId, ); } +/** Accept forward progress, or a compare-and-swap rewrite of the exact same source coverage. */ export function canReplaceHistoryCompactCheckpoint( current: HistoryCompactCheckpoint | undefined, candidate: HistoryCompactCheckpoint,