38 lines
2.6 KiB
Markdown
38 lines
2.6 KiB
Markdown
# RFC: Use one surface manager per session
|
|
|
|
Status: implemented
|
|
|
|
English | [中文](2026-07-19-use-one-session-surface-manager.zh.md)
|
|
|
|
## Problem
|
|
|
|
`Session` maintained two `SurfaceManager` instances over the same append-only event log. One validated seed and append candidates, while a second lazy instance independently folded committed events for `session.surface`, derived messages, compaction, and workspace context. Once the public surface had been read, every later event advanced duplicate node and replacement-generation state without creating a separate authority or failure boundary.
|
|
|
|
## Decision
|
|
|
|
Each `Session` owns one eagerly constructed `SurfaceManager`. Seed and append acceptance call `validateNext()` on that manager before committing an event, and `session.surface` returns the same object through this readonly contract:
|
|
|
|
```ts
|
|
export interface SessionSurface {
|
|
readonly nodes: readonly number[]
|
|
readonly replaceGeneration: number
|
|
}
|
|
```
|
|
|
|
Candidate validation remains atomic. `validateNext()` may synchronize committed log entries, but it only plans the uncommitted candidate. The candidate enters manager state after `log.push()` and the next delta synchronization, so surface validation failures and pre-commit `internal/dispatch` vetoes leave no phantom node or replacement generation.
|
|
|
|
`foldSurface()` remains the detached full-log replay function for offline validation and reconstruction. It uses the same transitions and agrees with the live manager for every committed prefix without sharing mutable state.
|
|
|
|
## Alternatives considered
|
|
|
|
**Keep acceptance and projection state separate.** Separate instances appeared to isolate public reads from validation, but callers already receive borrowed surface state and the declared readonly contract prevents ordinary mutation. Duplicating the manager was not a runtime trust boundary.
|
|
|
|
**Recompute the public surface from the full log on every access.** This removed duplicate cached state but gave up incremental derivation and made repeated request construction scale with complete session history.
|
|
|
|
## Consequences
|
|
|
|
- Acceptance, `session.surface`, derived messages, compaction, and workspace context observe one incremental state.
|
|
- `Session.surface` exposes no validation method, while its object identity and borrowed readonly node array remain stable.
|
|
- A hostile cast can still corrupt borrowed state; JavaScript callers that deliberately bypass the readonly contract remain outside the supported same-process boundary.
|
|
- Surface, seed, dispatch-veto, request-reconstruction, compaction, and workspace-context tests exercise the shared manager and detached replay paths.
|