fix(client): refine trajectory inspection behavior

This commit is contained in:
_Kerman
2026-07-29 11:57:13 +08:00
parent 33afcc913b
commit 5d0d28cd0e
8 changed files with 147 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
// FoldAdapter: core SurfaceManager wiring + node materialization cache.
// Padding sentinels solve the paged-window seq offset (core fold asserts seq === index);
// a cross-window replace throw degrades to a lenient linear scan (foldDegraded —
// the degradation lives in one branch function in this file, zero scattered removal points).
// Padding sentinels solve the paged-window seq offset (core fold asserts seq === index).
// A replace that crosses the loaded window head uses a lenient linear scan until
// paging reaches its range; unexpected fold failures report and use the same fallback.
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
// Subpath export (package.json exports "./surface", alias added for this): all value imports
@@ -71,6 +71,17 @@ function paddingEvent(seq: number): SessionEvent {
return { type: 'noop/padding', seq, time: 0, data: {} } as unknown as SessionEvent
}
/**
* Whether a valid replacement range begins before the loaded history window.
* @param event - Candidate surface event in the current replay window.
* @param baseSeq - Sequence at the loaded window head.
* @returns True when strict folding requires an earlier page.
*/
function replacementCrossesWindowHead(event: SessionEvent, baseSeq: number): boolean {
if (!isSurfaceEvent(event) || event.surfaceOp === 'append') return false
return event.surfaceOp.start < baseSeq || event.surfaceOp.end < baseSeq
}
/** Minimal generation projection owned by the inspection adapter, not the core live surface. */
interface FoldedContext {
generation: number
@@ -183,7 +194,7 @@ function materializeNode(
}
}
/** Window fold over the core SurfaceManager (sentinel padding for the seq offset; degrades to a linear scan on cross-window replace). */
/** Window fold over the core SurfaceManager with a lenient partial-history fallback. */
export class FoldAdapter {
/** padded = [sentinel x baseSeq, ...window events]; SurfaceManager borrows this reference for lazy incremental folding. */
private padded: SessionEvent[] = []
@@ -247,7 +258,7 @@ export class FoldAdapter {
for (const event of events) this.padded.push(event)
this.surface = new SurfaceManager(this.padded)
this.nodeCache.clear()
this.degraded = false
this.degraded = events.some(event => replacementCrossesWindowHead(event, baseSeq))
this.callIdx = new Map()
this.resultViews.clear()
this.contextGeneration = 0
@@ -281,6 +292,7 @@ export class FoldAdapter {
if (this.projectContexts && (isSurfaceEvent(event) || event.type === 'request/header')) {
this.contextRev++
}
if (replacementCrossesWindowHead(event, this.baseSeq)) this.degraded = true
this.padded.push(event)
this.indexCall(event, view)
if (this.projectContexts) this.indexContextPrompt(event)

View File

@@ -162,6 +162,68 @@ describe('FoldAdapter', () => {
}
})
it('silently degrades when a replacement needs an earlier history page', () => {
const adapter = new FoldAdapter()
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
try {
adapter.reset([
at(10, {
type: 'assistant/message',
surfaceOp: { op: 'replace', start: 1, end: 3 },
sourceEventSeqs: [1, 3],
data: {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'partial summary' }],
source: { kind: 'model', provider: 'fake', model: 'fake' },
}),
},
}),
ev.user(11, 'newer message'),
], 10)
expect(adapter.nodes()).toMatchObject({
degraded: true,
nodes: [{ seq: 10 }, { seq: 11 }],
})
expect(errorSpy).not.toHaveBeenCalled()
} finally {
errorSpy.mockRestore()
}
})
it('silently degrades when a live replacement needs an earlier history page', () => {
const adapter = new FoldAdapter()
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
try {
adapter.reset([ev.user(10, 'window head')], 10)
adapter.append(at(11, {
type: 'assistant/message',
surfaceOp: { op: 'replace', start: 1, end: 1 },
sourceEventSeqs: [1],
data: {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'live summary' }],
source: { kind: 'model', provider: 'fake', model: 'fake' },
}),
},
}))
expect(adapter.nodes()).toMatchObject({
degraded: true,
nodes: [{ seq: 10 }, { seq: 11 }],
})
expect(errorSpy).not.toHaveBeenCalled()
} finally {
errorSpy.mockRestore()
}
})
it('materializes a tool-result error field when present', () => {
const adapter = new FoldAdapter()
adapter.reset([