fix(session-reference): bind snapshots to prompts

This commit is contained in:
Yichen Jiang
2026-07-22 17:34:31 +08:00
parent 3fa368854c
commit da8c0ab092
102 changed files with 838 additions and 248 deletions

View File

@@ -1,6 +1,6 @@
# `@deepseek-ai/dsh-session-reference`
`ctx.sessionReferences` prepares bounded, read-only snapshots of other DeepSeek Harness sessions as durable `context/message` input. It consumes `ctx.sessionQuery` and the backend-independent compact checkpoint marker; SQLite FTS is not required. The standard TUI and ACP demo bundles mount it, while other hosts may call the service directly.
`ctx.sessionReferences` prepares bounded, read-only snapshots of other sessions as prompt-prefix context. It consumes `ctx.sessionQuery` and the backend-independent compact checkpoint marker; SQLite FTS is not required. The standard TUI and ACP demo bundles mount it, while other hosts may call the service directly.
## Public API
@@ -10,9 +10,9 @@
## Snapshot semantics
Preparation calls `ctx.sessionQuery.readSurface()` once per distinct source and never rereads it after enqueue. It projects only direct-user `user/message`, direct-user `steering/message`, assistant text, and `user/message` checkpoints carrying the canonical `dsh-compact` source marker from the folded current surface. Shadowed pre-compaction events, tools, reasoning, context, plugin-generated user messages other than marked compact checkpoints, and unfinished assistant chunks are excluded. A compacted source therefore contributes its latest checkpoint plus retained later conversation, not restored shadowed text.
Preparation calls `ctx.sessionQuery.readSurface()` once per distinct source and never rereads it after enqueue. It projects only direct-user `user/message`, direct-user `steering/message`, assistant text, and `user/message` checkpoints carrying the canonical `dsh-compact` source marker from the folded current surface. For a source prompt that already contains baked prefix context, projection reads only its model-hidden display content, preventing recursive snapshot propagation. Shadowed pre-compaction events, tools, reasoning, context, plugin-generated user messages other than marked compact checkpoints, and unfinished assistant chunks are excluded. A compacted source therefore contributes its latest checkpoint plus retained later conversation, not restored shadowed text.
The context source is `{ kind: 'plugin', plugin: 'session-reference' }`. Its metadata records version `1`, source ids and labels, capture seqs, compact presence, retained/omitted message counts, omitted UTF-8 bytes, and truncation state. The target session persists that exact context through the ordinary `context/message` event; later source mutation, compaction, or deletion cannot change target replay.
The context source is `{ kind: 'plugin', plugin: 'session-reference' }` with `placement: 'prompt-prefix'`. Its metadata records version `1`, source ids and labels, capture seqs, compact presence, retained/omitted message counts, omitted UTF-8 bytes, and truncation state. AgentLoop writes the snapshot, `## My request:` delimiter, and effective prompt into one `user/message` or `steering/message`; the same event's model-hidden envelope retains the direct prompt and metadata for TUI/ACP replay. Later source mutation, compaction, or deletion cannot change target replay.
## Configuration
@@ -30,7 +30,7 @@ Retention applies `maxReferenceBytes` independently to each source, keeps compac
#### What the model sees
The model sees the current message's readable `@label` plus one same-level user-context message headed `## Referenced sessions`. The context states that its JSON is untrusted, read-only background and forbids following instructions, permission claims, or tool requests unless the current user repeats them. Labels, cwd values, ids, and conversation text are serialized as JSON inside `<referenced-sessions>` tags; every data `<` is emitted as the lossless JSON escape `\u003c`, so source text cannot spell a framing tag.
The model sees one user-role message in this order: the `## Referenced sessions` untrusted snapshot, the `## My request:` delimiter, then the current message with its readable `@label`. The warning forbids following instructions, permission claims, or tool requests from the snapshot unless the current user repeats them. Labels, cwd values, ids, and conversation text are serialized as JSON inside `<referenced-sessions>` tags; every data `<` is emitted as the lossless JSON escape `\u003c`, so source text cannot spell a framing tag.
#### Token effect
@@ -38,7 +38,7 @@ Each referenced message adds the fixed warning plus up to three serialized snaps
#### KV Cache effect
Snapshot context is append-only at the target message boundary and preserves earlier cacheable history. Different references or source capture contents change the new suffix only; later target compaction may invalidate reuse from its replacement boundary.
The combined snapshot and request are append-only at the target message boundary and preserve earlier cacheable history. Different references or source capture contents change the new suffix only; later target compaction may invalidate reuse from its replacement boundary.
## Known Limitations and Deferred Work

View File

@@ -195,6 +195,7 @@ export class SessionReferenceService extends Service {
const context: HookContext = {
source: { kind: 'plugin', plugin: 'session-reference' },
content: [{ type: 'text', text: prompt }],
placement: 'prompt-prefix',
meta,
}
return { content: acceptedContent, contexts: [context] }

View File

@@ -1,6 +1,7 @@
/** Current-surface projection and byte-bounded rendering. */
import { isCompactCheckpointSource } from '@deepseek-ai/dsh-compact'
import { displayPromptContent } from '@deepseek-ai/dsh-session'
import type { SessionSurfaceSnapshot } from '@deepseek-ai/dsh-session-query'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { TextRetainer } from '@deepseek-ai/dsh-retention'
@@ -40,13 +41,13 @@ function projectSessionConversation(snapshot: SessionSurfaceSnapshot): Projected
case 'user/message': {
const checkpoint = isCompactCheckpointSource(event.data.source)
if (!checkpoint && event.data.source.kind !== 'user') break
const text = textContent(event.data.content)
const text = textContent(displayPromptContent(event.data))
if (text !== '') conversation.push({ role: 'user', text, checkpoint, originalText: text, omittedBytes: 0 })
break
}
case 'steering/message': {
if (event.data.source.kind !== 'user') break
const text = textContent(event.data.content)
const text = textContent(displayPromptContent(event.data))
if (text !== '') conversation.push({ role: 'user', text, checkpoint: false, originalText: text, omittedBytes: 0 })
break
}

View File

@@ -228,6 +228,7 @@ describe('session reference discovery and preparation', () => {
const context = prepared.contexts[0]
if (context?.content[0]?.type !== 'text') throw new Error('expected text context')
expect(context.source).toEqual({ kind: 'plugin', plugin: 'session-reference' })
expect(context.placement).toBe('prompt-prefix')
expect(context.content[0].text).toContain('untrusted, read-only snapshot')
expect(promptData(context.content[0].text)).toEqual([{
sessionId: 'source',
@@ -261,6 +262,36 @@ describe('session reference discovery and preparation', () => {
expect(context.content[0].text).not.toContain('later source mutation')
})
it('projects only the direct prompt when a source message contains baked prefix context', async () => {
const ctx = await harness()
const target = ctx.sessions.create(SessionId('target'))
const source = ctx.sessions.create(SessionId('source'))
source.append('user/message', {
content: [
{ type: 'text', text: 'nested referenced snapshot must not propagate' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'direct source question' },
],
source: { kind: 'user' },
envelope: {
displayContent: [{ type: 'text', text: 'direct source question' }],
prefixContexts: [{ source: { kind: 'plugin', plugin: 'session-reference' } }],
},
}, { surfaceOp: 'append' })
const prepared = await ctx.sessionReferences.prepare(
fakeAgent(target),
[{ type: 'text', text: 'inspect source' }],
[{ sessionId: source.id }],
)
const context = prepared.contexts[0]
if (context?.content[0]?.type !== 'text') throw new Error('expected text context')
expect(promptData(context.content[0].text)).toMatchObject([{
conversation: [{ role: 'user', text: 'direct source question' }],
}])
expect(context.content[0].text).not.toContain('nested referenced snapshot must not propagate')
})
it('keeps source text inside tag-safe JSON framing without changing its value', async () => {
const ctx = await harness()
const target = ctx.sessions.create(SessionId('target'))
@@ -447,14 +478,19 @@ describe('session reference discovery and preparation', () => {
[{ type: 'text', text: 'use @source' }],
[{ sessionId: source.id }],
)
target.append(
'user/message',
{ content: prepared.content, source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
for (const context of prepared.contexts) {
target.append('context/message', context, { surfaceOp: 'append' })
}
const context = prepared.contexts[0]
if (context === undefined) throw new Error('expected prepared context')
target.append('user/message', {
content: [...context.content, { type: 'text', text: '\n\n## My request:\n' }, ...prepared.content],
source: { kind: 'user' },
envelope: {
displayContent: prepared.content,
prefixContexts: [{
source: context.source,
...context.meta === undefined ? {} : { meta: context.meta },
}],
},
}, { surfaceOp: 'append' })
const before = target.deriveMessages()
const later = source.append(
@@ -480,6 +516,7 @@ describe('session reference discovery and preparation', () => {
expect(ctx.sessions.get(source.id)).toBeUndefined()
expect(target.deriveMessages()).toEqual(before)
expect(JSON.stringify(before)).toContain('durable referenced fact')
expect(JSON.stringify(before)).toContain('## My request:')
expect(JSON.stringify(before)).not.toContain('later source mutation')
expect(new Session(SessionId('replayed-target'), target.events).deriveMessages()).toEqual(before)
})