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

@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
import { afterAll, describe, expect, it, vi } from 'vitest'
import type { Context } from 'cordis'
import { agentEvents } from '@deepseek-ai/dsh-agent'
import { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact'
import { createUserMessage, CallId, type ContentBlock , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-llm-retry'
import { SessionId, type JsonValue, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
@@ -684,7 +685,7 @@ describe('TUI terminal-state snapshots', () => {
await disposeSnapshot(harness)
})
it('pins compaction surface replacement and narrow-to-wide reflow', async () => {
it('pins preserved history, the compaction marker, and narrow-to-wide reflow', async () => {
// Freeze the clock: the timing header hides zero-duration buckets, so a
// real-clock millisecond tick between the fixture appends and the render
// would flip `Tools 0.0s` in and out of the pinned header.
@@ -696,7 +697,7 @@ describe('TUI terminal-state snapshots', () => {
tools: ADVANCED_CARD_TOOLS,
beforeMount(session) {
const user = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'Old prompt with a long line that exercises wrapping before compaction.' }],
content: [{ type: 'text', text: 'Old prompt with a long line that exercises wrapping and stays visible after compaction.' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
const assistant = session.append('assistant/message', {
@@ -717,7 +718,7 @@ describe('TUI terminal-state snapshots', () => {
step: 1,
message: createToolResultMessage({
callId: CallId('old-tool'),
content: [{ type: 'text', text: 'obsolete output that must disappear' }],
content: [{ type: 'text', text: 'tool output that stays readable after compaction' }],
isError: false,
}),
}, { surfaceOp: 'append' })
@@ -732,9 +733,9 @@ describe('TUI terminal-state snapshots', () => {
harness.session.append('user/message', createUserMessage({
content: [{
type: 'text',
text: '<system-reminder>\nAdditional instructions from: nested/AGENTS.md\n\nRender workspace context XML clearly.\n</system-reminder>',
text: '<context_checkpoint>\nModel-only summary payload that must never reach the transcript.\n</context_checkpoint>',
}],
source: { kind: 'plugin', plugin: 'workspace-context' },
source: COMPACT_CHECKPOINT_SOURCE,
}), {
surfaceOp: { op: 'replace', start: replacementStart, end: replacementEnd },
sourceEventSeqs: replacementSources,