From 1ec8c40d0dcef27e689e6d81412ae464d29c91da Mon Sep 17 00:00:00 2001 From: Hypatia May Date: Tue, 23 Jun 2026 13:26:45 +0800 Subject: [PATCH] refactor(surface): use nodeBySeq map for lookup in _replace, drop dead params --- docs/core-data-structures/session.md | 46 +++++++++++++++++++++++++++- packages/core/session/src/surface.ts | 28 ++++++++--------- scripts/type-equiv.manifest.json | 4 +++ 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/docs/core-data-structures/session.md b/docs/core-data-structures/session.md index 62f6d062a4..2618db43c9 100644 --- a/docs/core-data-structures/session.md +++ b/docs/core-data-structures/session.md @@ -66,7 +66,51 @@ type SessionEvent = { `SessionEventType = keyof SessionEventMap`. Because `SessionEventMap` is merge-extensible, switches over `SessionEvent` must NOT use `assertNever` — a plugin-added variant is a valid unknown value; handle the known cases and fall through `default`. -The five message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`) additionally carry two optional surface fields: `surfaceOp` (how the event enters the derived surface linked list — `'append'` or a `{ op: 'replace', start, end }` shadow) and `sourceEventSeqs` (provenance). See the [session surface RFC](../rfc/implemented/architecture/2026-06-18-session-surface.md). +## Surface types + +The five message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`) carry surface metadata declaring how they join the derived surface linked list. See the [session surface RFC](../rfc/implemented/architecture/2026-06-18-session-surface.md). + +### `SurfaceEventType` — the message-producing subset of event types + +```ts type-equiv +export type SurfaceEventType = + | 'user/message' + | 'assistant/message' + | 'tool/result' + | 'context/message' + | 'steering/message' +``` + +### `SurfaceOp` — how an event entered the surface + +```ts type-equiv +export type SurfaceOp = + | 'append' + | { op: 'replace'; start: number; end: number } +``` + +`'append'` is the normal tail-append path. `replace` shadows surface nodes from `start` through `end` inclusive (both must be valid surface node seqs; `start === end` replaces a single node) and inserts the new node in their place. + +### `SurfaceIntent` — the parameter to `session.append()` + +```ts type-equiv +export interface SurfaceIntent { + surfaceOp: SurfaceOp + sourceEventSeqs?: number[] +} +``` + +Required for `SurfaceEventType` events — every message-producing event must declare how it joins the surface, the sole source of derived history. Non-surface types reject it at compile time. + +### `SurfaceNode` — a node in the surface linked list + +```ts type-equiv +export interface SurfaceNode { + seq: number + prev: number | null + next: number | null +} +``` ## Derived history: `deriveMessages()` diff --git a/packages/core/session/src/surface.ts b/packages/core/session/src/surface.ts index 7ed1743af3..3615668674 100644 --- a/packages/core/session/src/surface.ts +++ b/packages/core/session/src/surface.ts @@ -55,7 +55,7 @@ export interface SurfaceNode { export class SurfaceManager { /** Surface nodes in linked-list order (head to tail). Empty until first access. */ private _nodes: SurfaceNode[] = [] - /** Map from event seq → node for O(1) lookup during replacements. */ + /** Map from event seq → node. */ private _nodeBySeq = new Map() /** The last processed seq. -1 forces a full rebuild on first access. */ private _lastProcessedSeq = -1 @@ -100,7 +100,7 @@ export class SurfaceManager { this._nodes.push(node) this._nodeBySeq.set(event.seq, node) } else { - this._replace(this._nodes, this._nodeBySeq, event.seq, event.surfaceOp) + this._replace(event.seq, event.surfaceOp) } } this._lastProcessedSeq = this.log.length - 1 @@ -108,31 +108,31 @@ export class SurfaceManager { /** Apply a replace operation to the in-progress surface. */ private _replace( - nodes: SurfaceNode[], - nodeBySeq: Map, newSeq: number, op: Extract, ): void { - const startIdx = nodes.findIndex(n => n.seq === op.start) - if (startIdx === -1) { + const startNode = this._nodeBySeq.get(op.start) + if (!startNode) { throw new Error(`surface replace: start seq ${op.start} not found in surface`) } - const endIdx = nodes.findIndex(n => n.seq === op.end) - if (endIdx === -1) { + const endNode = this._nodeBySeq.get(op.end) + if (!endNode) { throw new Error(`surface replace: end seq ${op.end} not found in surface`) } + const startIdx = this._nodes.indexOf(startNode) + const endIdx = this._nodes.indexOf(endNode) if (startIdx > endIdx) { throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`) } // Remove shadowed nodes from `[startIdx, endIdx]` inclusive. const count = endIdx - startIdx + 1 - const removed = nodes.splice(startIdx, count) - for (const r of removed) nodeBySeq.delete(r.seq) + const removed = this._nodes.splice(startIdx, count) + for (const r of removed) this._nodeBySeq.delete(r.seq) // Insert the new node where the removed range was. - const prevNode = startIdx > 0 ? nodes[startIdx - 1] : undefined - const nextNode = startIdx < nodes.length ? nodes[startIdx] : undefined + const prevNode = startIdx > 0 ? this._nodes[startIdx - 1] : undefined + const nextNode = startIdx < this._nodes.length ? this._nodes[startIdx] : undefined const newNode: SurfaceNode = { seq: newSeq, @@ -141,7 +141,7 @@ export class SurfaceManager { } if (prevNode) prevNode.next = newSeq if (nextNode) nextNode.prev = newSeq - nodes.splice(startIdx, 0, newNode) - nodeBySeq.set(newSeq, newNode) + this._nodes.splice(startIdx, 0, newNode) + this._nodeBySeq.set(newSeq, newNode) } } diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index f46c4fca6b..30165e2388 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -19,6 +19,10 @@ { "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/session.md", "symbol": "TurnTriggerMap", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/session.md", "symbol": "TurnEndReasonMap", "source": "packages/core/session/src/types.ts" }, + { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceEventType", "source": "packages/core/session/src/types.ts" }, + { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceOp", "source": "packages/core/session/src/types.ts" }, + { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceIntent", "source": "packages/core/session/src/types.ts" }, + { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceNode", "source": "packages/core/session/src/surface.ts" }, { "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/core/session/src/types.ts" },