diff --git a/packages/compact/compact-basic/src/index.ts b/packages/compact/compact-basic/src/index.ts index 86481175c9..a38bf809e3 100644 --- a/packages/compact/compact-basic/src/index.ts +++ b/packages/compact/compact-basic/src/index.ts @@ -325,11 +325,10 @@ export class BasicCompactService extends CompactService { throw new Error('summarization produced no text summary content') } - return { - summary, - model: options.model, - ...options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {}, - } + // config.maxTokens is required and validated positive, so this backend's + // envelope always carries the cap; the return type's optionality exists + // for overriding subclasses whose summarizer has none. + return { summary, model: options.model, maxTokens: this.config.maxTokens } } // ---- Core API (implements the abstract contract) ---- diff --git a/packages/core/agent-loop/tests/request-reconstruction.spec.ts b/packages/core/agent-loop/tests/request-reconstruction.spec.ts index 788b451c2d..0afaadb958 100644 --- a/packages/core/agent-loop/tests/request-reconstruction.spec.ts +++ b/packages/core/agent-loop/tests/request-reconstruction.spec.ts @@ -253,7 +253,7 @@ describe('request stability across the loop', () => { send(agent, 'go') await waitForIdle(ctx, agent) ctx.systemPrompt.section({ name: 'extra', order: 2, text: 'now with guidance' }) - ctx.on('agent/request', async (_agent, _turn, _step, config, _next) => ({ ...config, temperature: 0.5 })) + ctx.on('agent/request', async (_agent, _turn, _step, config, _next) => ({ ...config, temperature: 0.5, maxTokens: 99, stop: [''] })) send(agent, 'again') await waitForIdle(ctx, agent) diff --git a/packages/core/session/tests/surface.spec.ts b/packages/core/session/tests/surface.spec.ts index 6a37e6fcab..257828e466 100644 --- a/packages/core/session/tests/surface.spec.ts +++ b/packages/core/session/tests/surface.spec.ts @@ -334,3 +334,26 @@ describe('surface type guards', () => { expect(isSurfaceEvent(markerless)).toBe(false) }) }) + +describe('SurfaceManager.replaceGeneration', () => { + it('folds the pending log delta on access and counts replaces and invalidations', () => { + const s = new Session(SessionId('gen')) + s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + s.append('user/message', { content: [{ type: 'text', text: 'one' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + s.append('user/message', { content: [{ type: 'text', text: 'two' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + // Read the generation FIRST — before nodes — so the getter itself folds + // the pending delta rather than piggybacking on a nodes read. + expect(s.surface.replaceGeneration).toBe(0) + + const nodes = s.surface.nodes + s.append('context/message', { + content: [{ type: 'text', text: 'summary' }], source: { kind: 'plugin', plugin: 'compact' }, + }, { surfaceOp: { op: 'replace', start: nodes[0]!.seq, end: nodes[1]!.seq }, sourceEventSeqs: [nodes[0]!.seq, nodes[1]!.seq] }) + expect(s.surface.replaceGeneration).toBe(1) + + // invalidate() is a rewrite too: the generation moves forward (and the + // refold re-counts the replace), never backwards. + s.surface.invalidate() + expect(s.surface.replaceGeneration).toBeGreaterThan(1) + }) +})