Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
Resolve translation-pairing hashes after the merge and adapt the math-rendering e2e fixture to the simplified turn/start event shape.
This commit is contained in:
@@ -241,13 +241,14 @@ function assertToolResultRewrite(
|
||||
event: SessionEvent,
|
||||
shadowedSeqs: readonly number[],
|
||||
events: readonly SessionEvent[],
|
||||
baseSeq: number,
|
||||
): void {
|
||||
if (event.type !== 'tool/result') return
|
||||
if (shadowedSeqs.length !== 1) {
|
||||
throw new Error('tool/result surface replacement must rewrite exactly one current node')
|
||||
}
|
||||
for (const originalSeq of shadowedSeqs) {
|
||||
const original = events[originalSeq]
|
||||
const original = events[originalSeq - baseSeq]
|
||||
if (original?.type !== 'tool/result') {
|
||||
throw new Error('tool/result surface replacement must target a current tool/result')
|
||||
}
|
||||
@@ -275,6 +276,7 @@ function planSurfaceEvent(
|
||||
event: SessionEvent,
|
||||
expectedSeq: number,
|
||||
events: readonly SessionEvent[],
|
||||
baseSeq: number,
|
||||
): SurfacePlan | undefined {
|
||||
if (event.seq !== expectedSeq) {
|
||||
throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`)
|
||||
@@ -287,7 +289,7 @@ function planSurfaceEvent(
|
||||
}
|
||||
const range = replacementRange(state, surfaceOp)
|
||||
assertProvenance(event, range.shadowedSeqs)
|
||||
assertToolResultRewrite(event, range.shadowedSeqs, events)
|
||||
assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq)
|
||||
return {
|
||||
kind: 'replace',
|
||||
seq: event.seq,
|
||||
@@ -303,8 +305,9 @@ function applySurfaceEvent(
|
||||
event: SessionEvent,
|
||||
expectedSeq: number,
|
||||
events: readonly SessionEvent[],
|
||||
baseSeq: number,
|
||||
): SurfaceFoldReplacement | undefined {
|
||||
const plan = planSurfaceEvent(state, event, expectedSeq, events)
|
||||
const plan = planSurfaceEvent(state, event, expectedSeq, events, baseSeq)
|
||||
if (plan?.kind === 'append') {
|
||||
state.nodes.push(plan.seq)
|
||||
} else if (plan?.kind === 'replace') {
|
||||
@@ -330,7 +333,7 @@ export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult
|
||||
const state = createFoldState()
|
||||
const replacements: SurfaceFoldReplacement[] = []
|
||||
for (const [index, event] of events.entries()) {
|
||||
const replacement = applySurfaceEvent(state, event, index, events)
|
||||
const replacement = applySurfaceEvent(state, event, index, events, 0)
|
||||
if (replacement !== undefined) replacements.push(replacement)
|
||||
}
|
||||
return { nodes: [...state.nodes], replacements }
|
||||
@@ -340,38 +343,55 @@ export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult
|
||||
export class SurfaceManager implements SessionSurface {
|
||||
/** Shared transition state; replacement history is not retained. */
|
||||
private _state = createFoldState()
|
||||
/** Last processed seq; -1 folds a seeded log on first access. */
|
||||
private _lastProcessedSeq = -1
|
||||
/** Last processed absolute seq. */
|
||||
private _lastProcessedSeq: number
|
||||
|
||||
constructor(private log: readonly SessionEvent[]) {}
|
||||
/**
|
||||
* @param log - Contiguous complete log or loaded event window.
|
||||
* @param baseSeq - Absolute sequence of the window's first event.
|
||||
*/
|
||||
constructor(
|
||||
private log: readonly SessionEvent[],
|
||||
private readonly baseSeq = 0,
|
||||
) {
|
||||
this._lastProcessedSeq = baseSeq - 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the next candidate without mutating the committed surface.
|
||||
* @param event - candidate event that has not entered the log yet.
|
||||
*/
|
||||
validateNext(event: SessionEvent): void {
|
||||
if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
|
||||
planSurfaceEvent(this._state, event, this.log.length, this.log)
|
||||
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta()
|
||||
planSurfaceEvent(
|
||||
this._state,
|
||||
event,
|
||||
this.baseSeq + this.log.length,
|
||||
this.log,
|
||||
this.baseSeq,
|
||||
)
|
||||
}
|
||||
|
||||
/** Monotonic count of folded positional replacements. */
|
||||
get replaceGeneration(): number {
|
||||
if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
|
||||
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta()
|
||||
return this._state.replaceGeneration
|
||||
}
|
||||
|
||||
/** Surface event sequences in model-visible order. */
|
||||
get nodes(): readonly number[] {
|
||||
if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
|
||||
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta()
|
||||
return this._state.nodes
|
||||
}
|
||||
|
||||
/** Fold events appended since the previous access. */
|
||||
private _processDelta(): void {
|
||||
for (let i = this._lastProcessedSeq + 1; i < this.log.length; i++) {
|
||||
const tailSeq = this.baseSeq + this.log.length - 1
|
||||
for (let seq = this._lastProcessedSeq + 1; seq <= tailSeq; seq++) {
|
||||
const index = seq - this.baseSeq
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded by the loop condition
|
||||
applySurfaceEvent(this._state, this.log[i]!, i, this.log)
|
||||
this._lastProcessedSeq = i
|
||||
applySurfaceEvent(this._state, this.log[index]!, seq, this.log, this.baseSeq)
|
||||
this._lastProcessedSeq = seq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
isSurfaceEligibleType,
|
||||
isSurfaceEvent,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import { SurfaceManager } from '@deepseek-ai/dsh-session/surface'
|
||||
import {
|
||||
createMessage,
|
||||
createToolResultMessage,
|
||||
@@ -239,6 +240,53 @@ describe('foldSurface tool-result rewrites', () => {
|
||||
})
|
||||
|
||||
describe('SurfaceManager', () => {
|
||||
it('folds a contiguous window without materializing earlier event sequences', () => {
|
||||
const baseSeq = 400_000
|
||||
const events = [
|
||||
provenanceEvent(baseSeq, undefined),
|
||||
provenanceEvent(baseSeq + 1, undefined),
|
||||
{
|
||||
...provenanceEvent(baseSeq + 2, [baseSeq]),
|
||||
surfaceOp: { op: 'replace', start: baseSeq, end: baseSeq },
|
||||
},
|
||||
] as SessionEvent[]
|
||||
|
||||
const surface = new SurfaceManager(events, baseSeq)
|
||||
expect(surface.nodes).toEqual([baseSeq + 2, baseSeq + 1])
|
||||
expect(surface.replaceGeneration).toBe(1)
|
||||
})
|
||||
|
||||
it('validates tool-result rewrites against a nonzero window offset', () => {
|
||||
const baseSeq = 400_000
|
||||
const original = toolResultEvent(baseSeq, 'call')
|
||||
const events: SessionEvent[] = [
|
||||
original,
|
||||
{
|
||||
...original,
|
||||
seq: baseSeq + 1,
|
||||
time: baseSeq + 1,
|
||||
surfaceOp: { op: 'replace' as const, start: baseSeq, end: baseSeq },
|
||||
sourceEventSeqs: [baseSeq],
|
||||
} as SessionEvent,
|
||||
]
|
||||
|
||||
expect(new SurfaceManager(events, baseSeq).nodes).toEqual([baseSeq + 1])
|
||||
})
|
||||
|
||||
it('rejects a replacement that crosses a loaded window head', () => {
|
||||
const baseSeq = 400_000
|
||||
const events = [
|
||||
provenanceEvent(baseSeq, undefined),
|
||||
{
|
||||
...provenanceEvent(baseSeq + 1, [baseSeq - 1, baseSeq]),
|
||||
surfaceOp: { op: 'replace', start: baseSeq - 1, end: baseSeq },
|
||||
},
|
||||
] as SessionEvent[]
|
||||
|
||||
expect(() => new SurfaceManager(events, baseSeq).nodes)
|
||||
.toThrow(`surface replace: start seq ${baseSeq - 1} not found in surface`)
|
||||
})
|
||||
|
||||
it('shares ordered entries and nested replacement ranges with foldSurface', () => {
|
||||
const s = Session.create(SessionId('shared-fold'))
|
||||
s.append('user/message', createUserMessage({
|
||||
|
||||
Reference in New Issue
Block a user