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 files changed
+544 -119

No files matched your search

@@ -1,7 +1,7 @@
terminal 44x18 buffer=normal length=18 base=0 viewport=0
terminal 44x18 buffer=normal length=24 base=6 viewport=6
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=7 viewportRow=14 bufferRow=14
cursor hidden column=7 viewportRow=17 bufferRow=23
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold
@@ -13,25 +13,39 @@ buffer
3| <blank>
4| "Assistant "
style 0-8 fg=bright-magenta bold underline
5| "Model wait 0.0s "
5| <blank>
6| "You "
style 0-2 fg=bright-magenta bold underline
7| "Old prompt with a long line that exercises "
8| "wrapping and stays visible after compaction."
9| <blank>
10| "● Tool / bash / Run the coverage gate"
style 0-36 fg=green
11| "$ pnpm run test:coverage "
style 0-23 dim
12| "/workspace/project "
style 0-17 dim
13| "packages/ui/tui 100% "
style 0-19 dim
14| "… +1 lines (Ctrl+O to expand) "
style 0-28 dim
15| "1 test skipped "
style 0-13 dim
16| "coverage complete "
style 0-16 dim
17| "[exit 0] "
style 0-7 dim
18| "Model wait 0.0s "
style 0-14 dim
6| <blank>
7| "Context · workspace-context"
style 0-26 dim
8| "Additional instructions from: "
style 0-43 dim
9| "nested/AGENTS.md "
style 0-15 dim
10| " "
11| "Render workspace context XML clearly. "
style 0-36 dim
12| <blank>
13| "/workspace/project (tui-staging) deepseek-v"
19| <blank>
20| "… earlier context was compacted … "
style 0-32 dim
21| <blank>
22| "/workspace/project (tui-staging) deepseek-v"
style 0-17 fg=bright-magenta bold
style 18-31 dim
style 34-43 dim
14| " dsh > "
23| " dsh > "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse
15-17| <blank>
@@ -1,7 +1,7 @@
terminal 104x30 buffer=normal length=30 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=7 viewportRow=13 bufferRow=13
cursor hidden column=7 viewportRow=22 bufferRow=22
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold
@@ -13,25 +13,41 @@ buffer
3| <blank>
4| "Assistant "
style 0-8 fg=bright-magenta bold underline
5| "Model wait 0.0s "
5| <blank>
6| "You "
style 0-2 fg=bright-magenta bold underline
7| "Old prompt with a long line that exercises wrapping and stays visible after compaction. "
8| <blank>
9| "● Tool / bash / Run the coverage gate"
style 0-36 fg=green
10| "$ pnpm run test:coverage "
style 0-23 dim
11| "/workspace/project "
style 0-17 dim
12| "packages/ui/tui 100% "
style 0-19 dim
13| "… +1 lines (Ctrl+O to expand) "
style 0-28 dim
14| "1 test skipped "
style 0-13 dim
15| "coverage complete "
style 0-16 dim
16| "[exit 0] "
style 0-7 dim
17| "Model wait 0.0s "
style 0-14 dim
6| <blank>
7| "Context · workspace-context"
style 0-26 dim
8| "Additional instructions from: nested/AGENTS.md "
style 0-45 dim
9| " "
10| "Render workspace context XML clearly. "
style 0-36 dim
11| <blank>
12| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
18| <blank>
19| "… earlier context was compacted … "
style 0-32 dim
20| <blank>
21| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
style 0-17 fg=bright-magenta bold
style 18-31 dim
style 34-50 dim
style 53-57 dim
style 60-69 dim
13| " dsh > "
22| " dsh > "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse
14-29| <blank>
23-29| <blank>
@@ -1,7 +1,7 @@
terminal 80x24 buffer=normal length=24 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=7 viewportRow=20 bufferRow=20
cursor hidden column=7 viewportRow=21 bufferRow=21
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold
@@ -16,35 +16,36 @@ buffer
5| <blank>
6| "You "
style 0-2 fg=bright-magenta bold underline
7| "Old prompt with a long line that exercises wrapping before compaction. "
8| <blank>
9| "● Tool / bash / Run the coverage gate"
7| "Old prompt with a long line that exercises wrapping and stays visible after "
8| "compaction. "
9| <blank>
10| "● Tool / bash / Run the coverage gate"
style 0-36 fg=green
10| "$ pnpm run test:coverage "
11| "$ pnpm run test:coverage "
style 0-23 dim
11| "/workspace/project "
12| "/workspace/project "
style 0-17 dim
12| "packages/ui/tui 100% "
13| "packages/ui/tui 100% "
style 0-19 dim
13| "… +1 lines (Ctrl+O to expand) "
14| "… +1 lines (Ctrl+O to expand) "
style 0-28 dim
14| "1 test skipped "
15| "1 test skipped "
style 0-13 dim
15| "coverage complete "
16| "coverage complete "
style 0-16 dim
16| "[exit 0] "
17| "[exit 0] "
style 0-7 dim
17| "Model wait 0.0s "
18| "Model wait 0.0s "
style 0-14 dim
18| <blank>
19| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
19| <blank>
20| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
style 0-17 fg=bright-magenta bold
style 18-31 dim
style 34-50 dim
style 53-57 dim
style 60-69 dim
20| " dsh > "
21| " dsh > "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse
21-23| <blank>
22-23| <blank>
+6 -5
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,
+108 -12
View File
@@ -16,6 +16,7 @@ import { createUserMessage,
} from '@deepseek-ai/dsh-llm'
import { GOAL_CHANGE_VERSION, GoalId, renderGoalChange, type GoalSnapshotChangeMeta } from '@deepseek-ai/dsh-goal'
import CommandService, { type CommandInvocation } from '@deepseek-ai/dsh-commands'
import { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact'
import SessionStore, { SessionId, type JsonValue, type SessionEvent, type SessionHeader, type TurnEndReason } from '@deepseek-ai/dsh-session'
import type { SessionRecord } from '@deepseek-ai/dsh-session-query'
import SkillService, { type SkillDefinition, type SkillSummary } from '@deepseek-ai/dsh-skill'
@@ -4365,10 +4366,10 @@ describe('tool cards and surface replay', () => {
await dispose(result)
})
it('rebuilds after a surface replacement and hides shadowed tool calls', async () => {
it('keeps append-origin history and marks a landed compaction, live and on rebuild', async () => {
const result = await setup({ tools })
appendUser(result.session, 'old prompt')
const assistant = result.session.append('assistant/message', {
result.session.append('assistant/message', {
turn: 1,
step: 1,
message: createMessage({
@@ -4391,21 +4392,116 @@ describe('tool cards and surface replay', () => {
isError: false,
}),
}, { surfaceOp: 'append' })
const start = result.session.surface.nodes[0] as number
result.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary replacement' }],
source: { kind: 'plugin', plugin: 'compact' },
}), {
surfaceOp: { op: 'replace', start, end: toolResult.seq },
sourceEventSeqs: [start, assistant.seq, toolResult.seq],
// Result pruning rewrites one node's content in place: model-only, and no
// boundary in the conversation, so the terminal keeps the full output.
const originalResult = toolResult.data.message.content[0]
result.session.append('tool/result', {
...toolResult.data,
message: freezeMessage({
...toolResult.data.message,
content: [{ ...originalResult, content: [{ type: 'text', text: 'pruned result copy' }] }] as [typeof originalResult],
}),
}, {
surfaceOp: { op: 'replace', start: toolResult.seq, end: toolResult.seq },
sourceEventSeqs: [toolResult.seq],
})
const nodes = [...result.session.surface.nodes]
const checkpoint = result.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: '<context_checkpoint>model-only summary payload</context_checkpoint>' }],
source: COMPACT_CHECKPOINT_SOURCE,
}), {
surfaceOp: { op: 'replace', start: nodes[0] as number, end: nodes.at(-1) as number },
sourceEventSeqs: nodes,
})
// A regenerated assistant message replaces one node without summarizing
// anything, so it marks no boundary either.
const generic = result.session.append('assistant/message', {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'generic replacement copy' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'deepseek-v4-flash' },
},
}),
}, { surfaceOp: { op: 'replace', start: checkpoint.seq, end: checkpoint.seq }, sourceEventSeqs: [checkpoint.seq] })
// Only a checkpoint carrying the compaction seam's source marks a boundary:
// another plugin replacing a node is model-only.
result.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'foreign plugin replacement copy' }],
source: { kind: 'plugin', plugin: 'other' },
}), { surfaceOp: { op: 'replace', start: generic.seq, end: generic.seq }, sourceEventSeqs: [generic.seq] })
await tick()
result.terminal.resize(89)
await tick()
const lastFullRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
expect(lastFullRender).toContain('summary replacement')
expect(lastFullRender).not.toContain('old output')
const liveRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
expect(liveRender).toContain('old prompt')
// The shadowed step keeps its card: one call row, one full result, no
// second card from the pruned copy.
expect(liveRender.split('$ printf hello')).toHaveLength(2)
expect(liveRender).toContain('third')
expect(liveRender.split('[exit 0]')).toHaveLength(2)
expect(liveRender.split('… earlier context was compacted …')).toHaveLength(2)
expect(liveRender).not.toContain('model-only summary payload')
expect(liveRender).not.toContain('generic replacement copy')
expect(liveRender).not.toContain('foreign plugin replacement copy')
// Ctrl+R rebuilds the transcript from the log; the replayed projection
// matches what the live appends produced, including the shadowed assistant
// message's tool card.
result.terminal.send('\x12')
await tick()
result.terminal.resize(90)
await tick()
const replayRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
expect(replayRender).toContain('old prompt')
expect(replayRender.split('$ printf hello')).toHaveLength(2)
expect(replayRender).toContain('third')
expect(replayRender.split('[exit 0]')).toHaveLength(2)
expect(replayRender.split('… earlier context was compacted …')).toHaveLength(2)
expect(replayRender).not.toContain('model-only summary payload')
expect(replayRender).not.toContain('generic replacement copy')
expect(replayRender).not.toContain('foreign plugin replacement copy')
await dispose(result)
})
it('replays a stored compaction as preserved history plus its marker', async () => {
const result = await setup({
beforeMount(session) {
appendUser(session, 'prompt before compaction')
session.append('assistant/message', {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'reply before compaction' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'deepseek-v4-flash' },
},
}),
}, { surfaceOp: 'append' })
const nodes = [...session.surface.nodes]
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: '<context_checkpoint>stored model-only payload</context_checkpoint>' }],
source: COMPACT_CHECKPOINT_SOURCE,
}), {
surfaceOp: { op: 'replace', start: nodes[0] as number, end: nodes.at(-1) as number },
sourceEventSeqs: nodes,
})
},
})
result.terminal.resize(89)
await tick()
const mounted = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
expect(mounted).toContain('prompt before compaction')
expect(mounted).toContain('reply before compaction')
expect(mounted.split('… earlier context was compacted …')).toHaveLength(2)
expect(mounted).not.toContain('stored model-only payload')
await dispose(result)
})
})