session: cache the derived history — one projection per node, frozen and shared
deriveMessages() no longer re-derives and re-clones the whole surface per call: each node is projected exactly once, when first seen (O(new nodes) per access), through the now-public deriveEventMessage — the per-node pure function external reconstructors and the dev invariant fold over a log prefix, so no two paths can disagree about a request's messages. A surface rewrite (replace, invalidate) rebuilds, signalled by SurfaceManager.replaceGeneration — monotonic, bumped by every folded replace and by invalidate(), never reset. Callers get a fresh array snapshot per call over SHARED deep-frozen messages: isolation of the append-only log moves from per-call cloning to unrepresentability (mutation throws), which the two isolation tests now pin directly.
This commit is contained in:
@@ -109,15 +109,17 @@ describe('Session properties', () => {
|
||||
))
|
||||
})
|
||||
|
||||
it('every derived message has a known role and decoupled content', () => {
|
||||
it('every derived message has a known role and is frozen (append-only contract)', () => {
|
||||
fc.assert(fc.property(logArb, (events) => {
|
||||
const session = build(events)
|
||||
const messages = session.deriveMessages()
|
||||
const before = structuredClone(session.events)
|
||||
for (const m of messages) {
|
||||
expect(['user', 'assistant', 'system']).toContain(m.role)
|
||||
// Mutating derived content must not touch the log (append-only).
|
||||
m.content.push({ type: 'text', text: 'mutation' })
|
||||
// Derived messages are frozen shared projections: mutation THROWS
|
||||
// (strict mode) instead of relying on per-call clones for isolation.
|
||||
expect(Object.isFrozen(m)).toBe(true)
|
||||
expect(() => { m.content.push({ type: 'text', text: 'mutation' }) }).toThrow(TypeError)
|
||||
}
|
||||
expect(session.events).toEqual(before)
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user