fix(session): harden cross-session references

This commit is contained in:
Yichen Jiang
2026-07-21 17:53:30 +08:00
parent 8394898ef5
commit ebb62c482c
19 changed files with 213 additions and 55 deletions

View File

@@ -23,7 +23,7 @@ import SessionPersistenceJsonl, {
} from '@deepseek-ai/dsh-session-persistence-jsonl'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import SessionQueryService from '@deepseek-ai/dsh-session-query'
import SessionReferenceService from '@deepseek-ai/dsh-session-reference'
import SessionReferenceService, { type Config as SessionReferenceConfig } from '@deepseek-ai/dsh-session-reference'
export const name = 'acp-demo'
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
@@ -56,6 +56,8 @@ export interface Config {
persistenceRoot?: string
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
persistenceCompression?: JsonlCompression
/** Cross-session reference discovery and snapshot byte budgets. */
sessionReferences?: SessionReferenceConfig
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
@@ -86,6 +88,7 @@ export const Config: z<Config> = z.object({
dshHome: z.string(),
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
persistenceCompression: JsonlCompressionSchema,
sessionReferences: SessionReferenceService.Config,
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
@@ -107,12 +110,16 @@ export function apply(ctx: Context, config: Config): void {
ctx.plugin(CommandService)
if (goals !== false) ctx.plugin(commandGoal)
ctx.plugin(agentCore, { ...agentCore.pickSpineConfig(config), goals })
// This front door owns the same persistence/reference cluster as the TUI;
// extracting these few calls would introduce a shared app-composition facade.
/* jscpd:ignore-start */
ctx.plugin(UserInteractionService)
ctx.plugin(SessionPersistenceJsonl, {
root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT,
...(config.persistenceCompression === undefined ? {} : { compression: config.persistenceCompression }),
})
ctx.plugin(SessionQueryService)
ctx.plugin(SessionReferenceService)
ctx.plugin(SessionReferenceService, config.sessionReferences ?? {})
/* jscpd:ignore-end */
ctx.plugin(acp, { provider: config.provider, model: config.model })
}

View File

@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { Message } from '@deepseek-ai/dsh-llm'
import * as acpAgent from '../src/index.ts'
@@ -76,6 +77,7 @@ describe('dsh-acp-demo composition', () => {
persona: 'hi',
persistenceRoot: '/tmp/dsh-acp-demo-test',
persistenceCompression: 'none',
sessionReferences: { candidateLimit: 1 },
skills: await isolatedSkillsConfig(),
workspaceContext: false,
})
@@ -90,6 +92,11 @@ describe('dsh-acp-demo composition', () => {
expect(ctx.get('tools')?.get('ask_user_question')).toBeUndefined()
expect(ctx.get('goals')).toBeDefined()
expect(ctx.get('tools')?.get('get_goal')).toBeDefined()
const target = ctx.sessions.create(SessionId('candidate-target'))
ctx.sessions.create(SessionId('candidate-one'))
ctx.sessions.create(SessionId('candidate-two'))
await expect(ctx.sessionReferences.listCandidates({ id: target.id, session: target } as Agent))
.resolves.toHaveLength(1)
// No pre-created agents — ACP session/new creates them on demand.
expect(ctx.get('agents')!.list()).toHaveLength(0)
await ctx.fiber.dispose()

View File

@@ -23,7 +23,7 @@ import SessionPersistenceJsonl, {
} from '@deepseek-ai/dsh-session-persistence-jsonl'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import SessionQueryService from '@deepseek-ai/dsh-session-query'
import SessionReferenceService from '@deepseek-ai/dsh-session-reference'
import SessionReferenceService, { type Config as SessionReferenceConfig } from '@deepseek-ai/dsh-session-reference'
import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
import * as uiTui from '@deepseek-ai/dsh-tui'
@@ -51,6 +51,8 @@ export interface Config {
persistenceRoot?: string
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
persistenceCompression?: JsonlCompression
/** Cross-session reference discovery and snapshot byte budgets. */
sessionReferences?: SessionReferenceConfig
/** TUI subtitle rendered on start. Defaults to `ready.`. */
welcome?: string
/** Full-screen TUI presentation settings. */
@@ -83,6 +85,7 @@ export const Config: z<Config> = z.object({
dshHome: z.string(),
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
persistenceCompression: JsonlCompressionSchema,
sessionReferences: SessionReferenceService.Config,
welcome: z.string().default(DEFAULT_WELCOME),
ui: uiTui.TuiConfigSchema,
skills: agentCore.SkillConfigSchema,
@@ -112,7 +115,7 @@ export function composeTuiApp(ctx: Context, config: Config): void {
...(config.persistenceCompression === undefined ? {} : { compression: config.persistenceCompression }),
})
ctx.plugin(SessionQueryService)
ctx.plugin(SessionReferenceService)
ctx.plugin(SessionReferenceService, config.sessionReferences ?? {})
ctx.plugin(UserInteractionService)
ctx.plugin(uiTui, {
...config.ui,

View File

@@ -32,6 +32,12 @@ describe('dsh-tui-demo app', () => {
dshHome: '/tmp/dsh-home',
persistenceRoot: '/tmp/tui-sessions',
persistenceCompression: 'none',
sessionReferences: {
maxReferences: 2,
candidateLimit: 7,
maxReferenceBytes: 1234,
maxTotalBytes: 2345,
},
welcome: 'TUI ready',
ui: { color: false, maxToolOutputLines: 3 },
skills: { tool: { catalogDescriptionMaxLength: 8 } },
@@ -53,6 +59,12 @@ describe('dsh-tui-demo app', () => {
])
expect(calls[0]?.config).toBeUndefined()
expect(calls[2]?.config).toEqual({ root: '/tmp/tui-sessions', compression: 'none' })
expect(calls[4]?.config).toEqual({
maxReferences: 2,
candidateLimit: 7,
maxReferenceBytes: 1234,
maxTotalBytes: 2345,
})
const tuiConfig = calls[6]?.config as { sessionId: string }
expect(tuiConfig).toMatchObject({ welcome: 'TUI ready', color: false, maxToolOutputLines: 3 })
expect(tuiConfig.sessionId).toMatch(/^main-session-[0-9a-f-]{36}$/)
@@ -90,6 +102,7 @@ describe('dsh-tui-demo app', () => {
})
expect(calls[2]?.config).toEqual({ root: './.sessions' })
expect(calls[4]?.config).toEqual({})
expect(calls[6]?.config).toEqual({ welcome: 'ready.', sessionId: 'persisted-session' })
expect((calls[7]?.config as { agents: Array<Record<string, unknown>> }).agents[0]).toMatchObject({
id: 'main',