fix(tui,host): project the human transcript from append-origin events

The terminal and history pagination both treated the model-visible surface as
the human transcript. A landed compaction replacement therefore erased the
conversation it summarized — messages the reader had already seen — and a
model-only replacement copy consumed a page's `maxMessages` quota, which could
also split a compaction's provenance from the replacement citing it.

`dsh-session` now exports the marker split `isAppendSurfaceEvent` /
`isReplacementSurfaceEvent`. The terminal replays append-origin surface events,
keeps a shadowed step's tool cards paired through its append-origin assistant
message, and renders one dim marker where a compaction landed; the checkpoint is
recognized through the compaction seam's `isCompactCheckpointSource` contract,
not the shape of the replacement. `session.history` counts only append-origin
human messages. Everything model-facing keeps reading `session.surface`.
This commit is contained in:
Hypatia May
2026-07-29 16:13:36 +08:00
parent 59ecfac776
commit d9a11dc91e
33 changed files with 544 additions and 119 deletions

View File

@@ -4,6 +4,8 @@ import {
Session,
SessionId,
foldSurface,
isAppendSurfaceEvent,
isReplacementSurfaceEvent,
isSurfaceEligibleType,
isSurfaceEvent,
} from '@deepseek-ai/dsh-session'
@@ -861,6 +863,40 @@ describe('surface type guards', () => {
expect(isSurfaceEligibleType(markerless.type)).toBe(true)
expect(isSurfaceEvent(markerless)).toBe(false)
})
it('splits surface events into append-origin and replacement by their marker', () => {
const s = surfaceSession()
s.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'checkpoint' }], source: { kind: 'plugin', plugin: 'compact' },
}), { surfaceOp: { op: 'replace', start: 1, end: 2 }, sourceEventSeqs: [1, 2] })
const appended = s.events.find(e => e.type === 'user/message')!
const replacement = s.events.at(-1)!
expect(isAppendSurfaceEvent(appended)).toBe(true)
expect(isReplacementSurfaceEvent(appended)).toBe(false)
expect(isAppendSurfaceEvent(replacement)).toBe(false)
expect(isReplacementSurfaceEvent(replacement)).toBe(true)
})
it('rejects log-only and markerless events from both marker guards', () => {
const s = surfaceSession()
const turnStart = s.events.find(e => e.type === 'turn/start')!
// A surface-eligible type whose mandatory marker is absent has no origin at
// all: it never entered the surface.
const markerless: SessionEvent = {
type: 'user/message',
seq: 0,
time: 0,
data: createUserMessage({
content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
}),
}
expect(isAppendSurfaceEvent(turnStart)).toBe(false)
expect(isReplacementSurfaceEvent(turnStart)).toBe(false)
expect(isAppendSurfaceEvent(markerless)).toBe(false)
expect(isReplacementSurfaceEvent(markerless)).toBe(false)
})
})
describe('SurfaceManager.replaceGeneration', () => {