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:
@@ -189,9 +189,9 @@ export interface SurfaceNode {
|
||||
}
|
||||
```
|
||||
|
||||
## Derived history: `deriveMessages()`
|
||||
## Derived history: `deriveMessages()` and `deriveEventMessage()`
|
||||
|
||||
`Session.deriveMessages()` projects the event log into the `Message[]` the model sees. The projection rules:
|
||||
`Session.deriveMessages()` projects the event log into the `Message[]` the model sees — cached (each surface node projected once, when first seen; a surface rewrite rebuilds) and frozen (a fresh array per call over shared, deep-frozen messages, so mutating logged history through a projection is unrepresentable). `deriveEventMessage(event)` is the per-node pure function the fold applies — public so external reconstructors and the dev invariant project a log prefix with exactly the same rules and cannot disagree with the cache. The projection rules:
|
||||
|
||||
- `user/message` → a user message.
|
||||
- `assistant/message` → an assistant message. Raw `assistant/chunk` events are replay/UI data and are **skipped** in derivation (the assembled message is authoritative). An **empty-content** `assistant/message` is also skipped — a max-tokens step cut off with no content still records an `assistant/message` to host its `usage`, but a content-less assistant turn must not enter the provider transcript.
|
||||
|
||||
Reference in New Issue
Block a user