fix(compact): preserve replay ownership (PR2 round 2)
This commit is contained in:
@@ -13,7 +13,7 @@ This backend owns the compaction policy:
|
||||
- **Convergence** — retry head-checkpoint compaction up to `compactionRetries`; reject a summary that does not shrink its source, and throw if retries cannot return below threshold.
|
||||
- **Summarization** — a direct `llm/stream` call uses the configured model and cap without running the loop-only `agent/request` seam. The input transcript preserves non-text blocks as tagged placeholders; only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call.
|
||||
- **Framing** — the replacement user message marks established checkpoint context with `<compacted-summary>` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint.
|
||||
- **Lifecycle** — `compactRegion()` records its start, summary, replacement, and end. The serial `agent/pre-step` listener checks pressure before every step, outside an open step, so a tool-heavy turn remains compactable and the loop derives history once after mutation.
|
||||
- **Lifecycle** — `compactRegion()` requires its agent to own the exact target session and rejects mismatch before resolution or mutation; a valid call records its start, summary, replacement, and end. The serial `agent/pre-step` listener checks pressure before every step, outside an open step, so a tool-heavy turn remains compactable and the loop derives history once after mutation.
|
||||
- **Failure handling** — an unmatched `compact/start` is an inert crash marker because no replacement landed. Recoverable failure records an error end and leaves the surface unchanged.
|
||||
|
||||
`summarize()` is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on the conversation model's meter. The hook returns the summary blocks together with the call envelope it used (`{ summary, model, maxTokens? }`), which is logged on `compact/summary`.
|
||||
|
||||
@@ -159,11 +159,12 @@ export class BasicCompactService extends CompactService {
|
||||
|
||||
/**
|
||||
* Compact one inclusive positional surface range using the effective
|
||||
* conversation model for all retention and shrink pricing.
|
||||
* @param session - session whose surface is mutated.
|
||||
* conversation model for all retention and shrink pricing. Reject an agent
|
||||
* that does not own the exact target before any resolution or mutation.
|
||||
* @param session - session whose surface is mutated; must equal `agent.session`.
|
||||
* @param start - inclusive first surface-node seq.
|
||||
* @param end - inclusive last surface-node seq.
|
||||
* @param agent - agent used by the summarizer and model resolver.
|
||||
* @param agent - owner of the target session, used by the summarizer and model resolver.
|
||||
* @param signal - optional summarization cancellation signal.
|
||||
* @returns the successful durable compaction result.
|
||||
*/
|
||||
@@ -174,6 +175,9 @@ export class BasicCompactService extends CompactService {
|
||||
agent: Agent,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CompactionResult> {
|
||||
if (session !== agent.session) {
|
||||
throw new Error('compactRegion: agent.session must be the exact target session')
|
||||
}
|
||||
const model = effectiveModel(agent)
|
||||
if (model === undefined || model.length === 0) {
|
||||
throw new Error('compactRegion: no routed or configured conversation model is available for token pricing')
|
||||
|
||||
@@ -361,6 +361,26 @@ describe('pressure measurement and retention', () => {
|
||||
})
|
||||
|
||||
describe('compaction region transaction', () => {
|
||||
it('rejects an agent that does not own the exact target session before mutation', async () => {
|
||||
const compact = service()
|
||||
const target = conversation(2)
|
||||
const owner = conversation(1)
|
||||
const targetEvents = [...target.events]
|
||||
const ownerEvents = [...owner.events]
|
||||
const nodes = target.surface.nodes
|
||||
|
||||
await expect(compact.compactRegion(
|
||||
target,
|
||||
nodes[0]!.seq,
|
||||
nodes[1]!.seq,
|
||||
agent(owner),
|
||||
)).rejects.toThrow('compactRegion: agent.session must be the exact target session')
|
||||
|
||||
expect(target.events).toEqual(targetEvents)
|
||||
expect(owner.events).toEqual(ownerEvents)
|
||||
expect(compact.calls).toEqual([])
|
||||
})
|
||||
|
||||
it('lands a framed, replayable checkpoint with exact pricing provenance', async () => {
|
||||
const compact = service()
|
||||
const session = conversation(3)
|
||||
|
||||
Reference in New Issue
Block a user