Avoid retaining processed session seqs

This commit is contained in:
Hypatia May
2026-07-14 14:23:02 +08:00
parent dbe65e1d13
commit dc60fe9572
7 changed files with 27 additions and 49 deletions

View File

@@ -328,7 +328,7 @@ export class Session {
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
* circular reference, sparse array, or an exotic object such as
* Map/Set/Date/class instance), or when the candidate violates the
* canonical surface contract (marker shape and eligibility, unique known
* canonical surface contract (marker shape and eligibility, unique
* earlier provenance, positional replacement validity, and complete
* shadowed-node coverage). One recursive pass reads, validates, and
* copies each nested value once, so a stateful getter cannot supply one value

View File

@@ -85,7 +85,6 @@ export interface SurfaceFoldResult {
interface SurfaceFoldState {
nodes: SurfaceNode[]
nodeBySeq: Map<number, SurfaceNode>
knownSeqs: Set<number>
replaceGeneration: number
}
@@ -106,7 +105,6 @@ function createFoldState(replaceGeneration = 0): SurfaceFoldState {
return {
nodes: [],
nodeBySeq: new Map(),
knownSeqs: new Set(),
replaceGeneration,
}
}
@@ -163,7 +161,6 @@ function surfaceEventOf(event: SessionEvent): SurfaceEvent | undefined {
/** Validate provenance against prior log entries and the replacement range. */
function assertProvenance(
event: SurfaceEvent,
knownSeqs: ReadonlySet<number>,
shadowedSeqs: readonly number[],
): void {
const sources = event.sourceEventSeqs
@@ -178,9 +175,6 @@ function assertProvenance(
if (source >= event.seq) {
throw new Error(`sourceEventSeqs must reference earlier events: ${source} >= current seq ${event.seq}`)
}
if (!knownSeqs.has(source)) {
throw new Error(`sourceEventSeqs references unknown seq ${source}`)
}
}
const missing = shadowedSeqs.filter(seq => !sourceSet.has(seq))
if (missing.length > 0) {
@@ -213,16 +207,23 @@ function replacementRange(
}
}
/** Validate one event and prepare its atomic fold transition. */
function planSurfaceEvent(state: SurfaceFoldState, event: SessionEvent): SurfacePlan | undefined {
/** Validate one event at its replay boundary and prepare its atomic fold transition. */
function planSurfaceEvent(
state: SurfaceFoldState,
event: SessionEvent,
expectedSeq: number,
): SurfacePlan | undefined {
if (event.seq !== expectedSeq) {
throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`)
}
const surfaceEvent = surfaceEventOf(event)
if (surfaceEvent === undefined) return
if (surfaceEvent.surfaceOp === 'append') {
assertProvenance(surfaceEvent, state.knownSeqs, [])
assertProvenance(surfaceEvent, [])
return { kind: 'append', seq: event.seq }
}
const range = replacementRange(state, surfaceEvent.surfaceOp)
assertProvenance(surfaceEvent, state.knownSeqs, range.shadowedSeqs)
assertProvenance(surfaceEvent, range.shadowedSeqs)
return {
kind: 'replace',
seq: event.seq,
@@ -257,8 +258,9 @@ function replaceSurface(state: SurfaceFoldState, plan: SurfaceReplacePlan): void
function applySurfaceEvent(
state: SurfaceFoldState,
event: SessionEvent,
expectedSeq: number,
): SurfaceFoldReplacement | undefined {
const plan = planSurfaceEvent(state, event)
const plan = planSurfaceEvent(state, event, expectedSeq)
if (plan?.kind === 'append') {
const tail = state.nodes.at(-1)
const node: SurfaceNode = { seq: plan.seq, prev: tail?.seq ?? null, next: null }
@@ -268,7 +270,6 @@ function applySurfaceEvent(
} else if (plan?.kind === 'replace') {
replaceSurface(state, plan)
}
state.knownSeqs.add(event.seq)
if (plan?.kind !== 'replace') return
return {
seq: plan.seq,
@@ -287,14 +288,15 @@ function applySurfaceEvent(
* @param events - session events in contiguous seq order.
* @returns the current surface and every positional replacement.
* @throws when any event violates the unified surface contract: metadata must
* be well shaped and type-eligible, provenance must name unique known earlier
* events, and a positional replacement must name and cite its complete range.
* be well shaped and type-eligible, event seqs must be contiguous, provenance
* must name unique earlier events, and a positional replacement must name and
* cite its complete range.
*/
export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult {
const state = createFoldState()
const replacements: SurfaceFoldReplacement[] = []
for (const event of events) {
const replacement = applySurfaceEvent(state, event)
for (const [index, event] of events.entries()) {
const replacement = applySurfaceEvent(state, event, index)
if (replacement !== undefined) replacements.push(replacement)
}
return {
@@ -326,7 +328,7 @@ export class SurfaceManager {
*/
validateNext(event: SessionEvent): void {
if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
planSurfaceEvent(this._state, event)
planSurfaceEvent(this._state, event, this.log.length)
}
/**
@@ -370,7 +372,7 @@ export class SurfaceManager {
// Index is bounded by i < this.log.length — never undefined.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const event = this.log[i]!
applySurfaceEvent(this._state, event)
applySurfaceEvent(this._state, event, i)
this._lastProcessedSeq = i
}
}