fix: address PR #1802 review round

- listChildren reads the session store via strict ctx.get (property proxy
  is caller-scoped), orders candidates branchlessly, narrows the cold-read
  return type, and pins the cost model and store/registry composition gaps
  with tests; per-file coverage restored
- acp-agent and headless-agent compositions mount session-projection; a
  keyless snapshot pins the descriptor-less diagnostic row
- api-proxy cold spec pins header-origin ownership and the legacy
  descriptor-only opt-out
- design note ships as implemented with its English pairing; companion
  notes and core-data-structures pages synced
This commit is contained in:
imccyu
2026-08-06 20:41:53 +08:00
parent a328fd34d5
commit 0b0b9e4707
29 changed files with 637 additions and 105 deletions

View File

@@ -80,6 +80,12 @@
maxTokens: 8192
compactionRetries: 1
# Projection registry: subagent catalog identity (mode/label) folds through
# its registered units; the catalog surfaces (`list_agents`, subagent listing)
# fail loud without the capability.
- id: session-projection
name: '@deepseek-ai/dsh-session-projection'
# Expose fresh-child `spawn` and completed-prefix `fork` through separate tool
# names so multi-child scenarios exercise both transports. These leaves follow
# the app because it provides `ctx.agents` and `ctx.tools`.

View File

@@ -71,6 +71,11 @@
maxTokens: 8192
compactionRetries: 1
# Projection registry: durable subagent identity (mode/label) folds through
# its registered units; subagent catalog reads fail loud without the capability.
- id: session-projection
name: '@deepseek-ai/dsh-session-projection'
# Expose fresh-child `spawn` and completed-prefix `fork` through independent
# in-process backends.
- id: subagent

View File

@@ -0,0 +1,44 @@
# Keyless real-Loader composition for the descriptor-less cold-child
# diagnostic snapshot. The seeded parent owns one session-backed child whose
# log carries `origin: 'subagent'` but no descriptor event, so the projection
# fold produces no identity and `list_agents` must surface the child as a
# `[diagnostic: corrupt]` row instead of silently dropping it.
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: none
# file/override both default to their DSH_SNAPSHOT_* env vars.
- id: replay
name: '@deepseek-ai/dsh-llm-replay'
# This scenario probes the subagent catalog only, so the bash/filesystem
# stacks are absent; the bundle must opt out of the tools that would wait
# forever for executors this tree never mounts.
- id: agent
name: '@deepseek-ai/dsh-agent-spine-demo'
config:
agents: []
workspaceContext: false
skills:
enabled: false
toolBash: false
toolTasks: false
goals: false
# Projection registry: the cold child's identity fold runs through it; the
# catalog read fails loud when the capability is absent.
- id: session-projection
name: '@deepseek-ai/dsh-session-projection'
- id: subagent
name: '@deepseek-ai/dsh-subagent'
- id: tool-subagent-list-agents
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
# Await the persisted resume before the headless driver inspects root agents.
- id: resumed-agent
name: './tests/fixtures/subagent-diagnostic-agent.ts'

View File

@@ -0,0 +1,26 @@
/**
* Loader fixture that resumes the seeded diagnostic-scenario parent before
* CLI dispatch, so `list_agents` runs against its pre-seeded cold child.
* @module subagent-diagnostic-agent
*/
import type { Context } from 'cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
/** Fixture plugin name. */
export const name = 'subagent-diagnostic-agent'
/** Services that must exist before the fixture resumes its agent. */
export const inject = ['agents', 'agentLoop', 'sessionPersistence']
/**
* Resume the seeded session and bind its exact handle to this fixture's lifetime.
* @param ctx - settled agent and persistence services from the Loader tree.
* @returns after the resumed agent is published.
*/
export async function apply(ctx: Context): Promise<void> {
const handle = await ctx.agents.resume({
resumeSessionId: 'subagent-diagnostic-parent' as SessionId,
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
})
ctx.effect(() => () => handle.dispose(), 'subagent-diagnostic-agent.handle')
}

View File

@@ -0,0 +1,31 @@
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1}}
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Start a background task."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"turn/end","seq":2,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"session/end-seed","seq":3,"time":0,"data":{}}
{"type":"agent/inbox/spliced","seq":4,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
{"type":"turn/start","seq":5,"time":0,"data":{"turn":2}}
{"type":"agent/inbox/spliced","seq":6,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":7,"time":0,"data":{"turn":2,"step":1}}
{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":9,"time":0,"data":{"title":"Start a background task.","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"list-once","name":"list_agents","argumentsDelta":"{}"}}}
{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}}}}
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":17,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"}
{"type":"tool/call","seq":18,"time":0,"data":{"turn":2,"step":1,"callId":"list-once","name":"list_agents","arguments":"{}"}}
{"type":"tool/result","seq":19,"time":0,"data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"list-once"},"content":[{"type":"tool-result","toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [diagnostic: corrupt]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"}
{"type":"step/end","seq":20,"time":0,"data":{"turn":2,"step":1}}
{"type":"step/start","seq":21,"time":0,"data":{"turn":2,"step":2}}
{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"The stored subagent is unreadable. PARENT_DONE"}}}
{"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The stored subagent is unreadable. PARENT_DONE"}}}}
{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":27,"time":0,"data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The stored subagent is unreadable. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[22,23,24,25,26],"surfaceOp":"append"}
{"type":"step/end","seq":28,"time":0,"data":{"turn":2,"step":2}}
{"type":"turn/end","seq":29,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}

View File

@@ -0,0 +1 @@
[{"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "tool-call"}, {"type": "tool-call-delta", "index": 0, "id": "list-once", "name": "list_agents", "argumentsDelta": "{}"}, {"type": "block-end", "index": 0, "block": {"type": "tool-call", "id": "list-once", "name": "list_agents", "arguments": "{}"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "tool-calls"}}]}, {"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "text"}, {"type": "text-delta", "index": 0, "text": "The stored subagent is unreadable. PARENT_DONE"}, {"type": "block-end", "index": 0, "block": {"type": "text", "text": "The stored subagent is unreadable. PARENT_DONE"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "stop"}}]}]

View File

@@ -0,0 +1,119 @@
/**
* Assembled-app regression: a persisted `origin: 'subagent'` child whose log
* carries no descriptor event is surfaced by `list_agents` as a
* `[diagnostic: corrupt]` row instead of being silently dropped.
*/
import { readFile, readdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Context } from 'cordis'
import { normalizeSessionLog, scrubRequestHeaders, type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import { describe, expect, it } from 'vitest'
const fixtureDir = fileURLToPath(new URL('./subagent-diagnostic-snapshots/descriptorless-child', import.meta.url))
const replayOverride = join(fixtureDir, 'replay.override.json')
const parentExpected = join(fixtureDir, 'parent.expected.jsonl')
const configPath = fileURLToPath(new URL('../subagent-diagnostic.cordis.snapshot.yml', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const parentId = SessionId('subagent-diagnostic-parent')
const childId = SessionId('subagent-diagnostic-child')
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
const task = 'Call list_agents once and report what it shows.'
/**
* Seed a completed parent turn plus one cold child that durably classifies
* as a subagent (`origin`) but never appended its descriptor event — the
* publication-window death the diagnostic row exists for.
*/
async function seedDescriptorlessChild(root: string, cwd: string): Promise<void> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
const parentMeta: SessionHeader = {
version: SESSION_FORMAT_VERSION,
id: parentId,
createdAt: 1,
cwd,
delegationDepth: 0,
}
const parentEvents: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
{ type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background task.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
{ type: 'turn/end', seq: 2, time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
]
const childMeta: SessionHeader = {
version: SESSION_FORMAT_VERSION,
id: childId,
createdAt: 2,
cwd,
parentSession: parentId,
origin: 'subagent',
delegationDepth: 1,
}
const childEvents: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 20, data: { turn: 1 } },
{ type: 'turn/end', seq: 1, time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
]
try {
await ctx.sessionPersistence.create(parentMeta)
await ctx.sessionPersistence.append(parentId, parentEvents)
await ctx.sessionPersistence.create(childMeta)
await ctx.sessionPersistence.append(childId, childEvents)
} finally {
await ctx.fiber.dispose()
}
}
describe('descriptor-less cold child diagnostic snapshot', () => {
it('surfaces the unreadable child as a corrupt diagnostic through the assembled headless app', async () => {
let cwd = ''
const result = await runLoaderSmoke({
label: 'subagent diagnostic headless stream-json snapshot',
tempDirPrefix: 'dsh-subagent-diag-',
binScript,
configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', task],
tsconfigPath,
env: {
DSH_SNAPSHOT_FILE: replayOverride,
DSH_SNAPSHOT_OVERRIDE: replayOverride,
},
prepare: async (runCwd) => {
cwd = runCwd
await seedDescriptorlessChild(join(runCwd, '.sessions'), runCwd)
},
inspect: async (runCwd) => {
const sessionsDir = join(runCwd, '.sessions')
const files = (await readdir(sessionsDir, { recursive: true })).filter(file => file.endsWith('.jsonl'))
const logs = await Promise.all(files.map(async file => readFile(join(sessionsDir, file), 'utf8')))
const parent = logs.find(content => content.includes('"subagent-diagnostic-parent"'))
if (parent === undefined) throw new Error('missing persisted parent log')
// THE model-visible fact: the descriptor-less child is reported, not
// silently dropped, and its reason is the corrupt classification.
expect(parent).toContain(`${childId} [diagnostic: corrupt]`)
const context: NormalizeContext = { sessionIds: [parentId, childId], cwd }
const normalizedParent = scrubRequestHeaders(normalizeSessionLog(parent, context))
if (refreshing) {
await writeFile(parentExpected, normalizedParent)
}
expect(normalizedParent).toBe(await readFile(parentExpected, 'utf8'))
},
})
expect(result.stderr).toBe('')
const records = result.stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
expect(records.at(-1)).toMatchObject({
type: 'result',
sessionId: parentId,
output: 'The stored subagent is unreadable. PARENT_DONE',
})
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})

View File

@@ -54,6 +54,7 @@
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:*",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-session-query": "workspace:*",
"@deepseek-ai/dsh-session-query-sqlite": "workspace:*",
"@deepseek-ai/dsh-session-reference": "workspace:*",