Merge latest master human inbox controls into V3 integration

Preserve the V3 PTC scenario while enabling the current subagent human-steering fixture environment. All product changes are inherited from master; no V3 migration semantics are altered.
This commit is contained in:
Tianyi Cui
2026-09-07 15:44:17 +08:00
70 changed files with 2352 additions and 1630 deletions
@@ -4,14 +4,20 @@ import type { Agent, ModelSelectionRef } from '@deepseek-ai/dsh-agent'
import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import { createAssistantMessage, createUserMessage, MessageId } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import SessionStore, {
SESSION_FORMAT_VERSION, Session, SessionId, SessionLogOffset, SessionSeq,
} from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader, UserMessage } from '@deepseek-ai/dsh-session'
import { snapshotSubagentDescriptor, SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent'
import { subagentIdentityProjectionDefinition } from '@deepseek-ai/dsh-subagent/src/projection.ts'
import { describe, expect, it, vi } from 'vitest'
import { ApiSessionAgentController } from '../src/agent.ts'
import { SessionCommandController } from '../src/commands.ts'
import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts'
async function commandHarness(): Promise<{
async function commandHarness(
childMode?: 'continuable' | 'seeded-continuable' | 'seed-only' | 'one-shot' | 'unknown' | 'corrupt',
): Promise<{
ctx: Context
controller: SessionCommandController
agent: Agent
@@ -22,9 +28,48 @@ async function commandHarness(): Promise<{
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(SessionId('commands-session'), { meta: { cwd: '/workspace' } })
installSessionReadTestServices(ctx)
ctx.sessionProjections.register(subagentIdentityProjectionDefinition)
const sessionId = SessionId('commands-session')
const ancestor = Session.create(SessionId('ancestor'))
ancestor.append('subagent/descriptor', snapshotSubagentDescriptor({
mode: 'continuable', provider: 'test', label: 'ancestor',
}))
// A seeded child inherits exactly the ancestor prefix; its own descriptor
// is appended after creation, as the continuation manager does. `seed-only`
// never appends one: the identity folds as continuable, but from the
// inherited prefix rather than this Session's own suffix.
const lineage = childMode === 'seeded-continuable' || childMode === 'seed-only'
? ancestor.snapshotEvents()
: undefined
const session = ctx.sessions.create(sessionId, {
...lineage === undefined ? {} : { seed: lineage, inheritedEventCount: SessionLogOffset(lineage.length) },
meta: {
cwd: '/workspace',
...(childMode === undefined ? {} : {
origin: 'subagent' as const,
parentSession: SessionId('offline-parent'),
}),
...lineage === undefined ? {} : { isSeeded: true },
},
})
if (childMode === 'continuable' || childMode === 'seeded-continuable') {
session.append('subagent/descriptor', snapshotSubagentDescriptor({
mode: 'continuable', provider: 'test', label: 'child',
}))
} else if (childMode === 'one-shot') {
session.append('subagent/descriptor', snapshotSubagentDescriptor({
mode: 'one-shot', provider: 'test', label: 'child',
}))
} else if (childMode === 'corrupt') {
session.append('subagent/descriptor', {
version: SUBAGENT_DESCRIPTOR_VERSION,
mode: 'continuable',
provider: 1,
} as never)
}
const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
const steer = vi.fn()
const steer = vi.fn((message: UserMessage) => { inbox.append('next-step', message) })
const cancel = vi.fn()
const agent = {
id: session.id,
@@ -52,7 +97,14 @@ async function commandHarness(): Promise<{
serializeImageAdmission: <Value>(_agent: Agent, operation: () => Promise<Value>) => operation(),
composeAgent: () => Promise.resolve({ setup: () => {} }),
} as unknown as ApiSessionAgentController
return { ctx, controller: new SessionCommandController(ctx, agents, '/workspace'), agent, inbox, steer, cancel }
return {
ctx,
controller: new SessionCommandController(ctx, agents, '/workspace'),
agent,
inbox,
steer,
cancel,
}
}
async function expectFailure(operation: Promise<unknown>, code: string): Promise<void> {
@@ -100,6 +152,9 @@ describe('Session queue commands', () => {
action: { kind: 'edit', content: [{ type: 'text', text: 'edited' }] },
})).toEqual({ accepted: true })
expect(inbox.nextTurn[0]?.content).toEqual([{ type: 'text', text: 'edited' }])
// An edit rewrites content in place, so the occurrence a client addressed
// by id stays addressable.
expect(inbox.nextTurn[0]?.id).toBe(queued.id)
expect(controller.updateQueue({
sessionId: agent.id, itemId: nextStep.id, action: { kind: 'remove' },
})).toEqual({ accepted: true })
@@ -136,6 +191,81 @@ describe('Session queue commands', () => {
expect(cancel).toHaveBeenCalledWith({ kind: 'user' }, { keepInbox: true })
await ctx.fiber.dispose()
})
it.each(['continuable', 'seeded-continuable'] as const)(
'mutates both inbox destinations of a live %s child while its parent is offline',
async (childMode) => {
const { ctx, controller, agent, inbox, steer } = await commandHarness(childMode)
const queued = createUserMessage({
content: [{ type: 'text', text: 'queued' }], source: { kind: 'user' },
})
const context = createUserMessage({
content: [{ type: 'text', text: 'context' }], source: { kind: 'plugin', plugin: 'test' },
})
inbox.append('next-turn', queued)
inbox.append('next-step', context)
expect(controller.updateQueue({
sessionId: agent.id,
itemId: context.id,
action: { kind: 'edit', content: [{ type: 'text', text: 'edited context' }] },
})).toEqual({ accepted: true })
const editedContext = inbox.nextStep[0]
expect(editedContext).toMatchObject({
content: [{ type: 'text', text: 'edited context' }],
source: context.source,
})
expect(editedContext?.id).toBe(context.id)
if (editedContext === undefined) throw new Error('missing edited context')
expect(controller.updateQueue({
sessionId: agent.id, itemId: editedContext.id, action: { kind: 'remove' },
})).toEqual({ accepted: true })
expect(controller.updateQueue({
sessionId: agent.id, itemId: queued.id, action: { kind: 'steer' },
})).toEqual({ accepted: true })
expect(steer).toHaveBeenCalledWith(queued)
await ctx.fiber.dispose()
},
)
it('removes the selected message before handing it to Agent steering', async () => {
const { ctx, controller, agent, inbox, steer } = await commandHarness('continuable')
const first = createUserMessage({
content: [{ type: 'text', text: 'first' }], source: { kind: 'user' },
})
const second = createUserMessage({
content: [{ type: 'text', text: 'second' }], source: { kind: 'user' },
})
inbox.append('next-turn', first)
inbox.append('next-turn', second)
// Stand in for the Agent's cancellation-convergence destination; the
// command must accept whichever boundary `Agent.steer()` selects.
steer.mockImplementation((message: UserMessage) => { inbox.append('next-turn', message) })
expect(controller.updateQueue({
sessionId: agent.id, itemId: first.id, action: { kind: 'steer' },
})).toEqual({ accepted: true })
expect(steer).toHaveBeenCalledWith(first)
// Ordering proves the removal happened before delivery rather than after.
expect(inbox.nextTurn).toEqual([second, first])
expect(inbox.nextStep).toEqual([])
await ctx.fiber.dispose()
})
it('keeps one-shot, seed-only, missing, and malformed child descriptors behind the ownership fence', async () => {
for (const mode of ['one-shot', 'seed-only', 'unknown', 'corrupt'] as const) {
const { ctx, controller, agent, inbox } = await commandHarness(mode)
const queued = createUserMessage({
content: [{ type: 'text', text: mode }], source: { kind: 'user' },
})
inbox.append('next-turn', queued)
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: agent.id, itemId: queued.id, action: { kind: 'remove' },
})), 'session/agent-busy')
expect(inbox.nextTurn).toEqual([queued])
await ctx.fiber.dispose()
}
})
})
function imageRef(id: string): ImageAttachmentRef {
@@ -320,6 +320,7 @@ describe('subagent catalogs', () => {
requestId: expect.any(String) as unknown as string,
parentSessionId: S1, childSessionId: S2,
mode: 'continuable',
delivery: 'queue',
content: [{ type: 'text', text: 'continue' }],
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
},
@@ -400,9 +400,11 @@ describe('prompt and cancel errors', () => {
})
await session.open()
const prompted = await session.prompt([{ type: 'text', text: '继续' }], 'queue')
const steered = await session.prompt([{ type: 'text', text: '现在处理' }], 'steer')
const cancelled = await session.cancel()
expect(prompted).toEqual({ ok: true, value: { accepted: true } })
expect(steered).toEqual({ ok: true, value: { accepted: true } })
expect(cancelled).toEqual({ ok: true, value: { accepted: true } })
expect(api.callsOf('session.follow')).toEqual([
{
@@ -419,9 +421,18 @@ describe('prompt and cancel errors', () => {
requestId: expect.any(String) as unknown as string,
parentSessionId: PARENT, childSessionId: SID,
mode: 'continuable',
delivery: 'queue',
content: [{ type: 'text', text: '继续' }],
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
},
{
requestId: expect.any(String) as unknown as string,
parentSessionId: PARENT, childSessionId: SID,
mode: 'continuable',
delivery: 'steer',
content: [{ type: 'text', text: '现在处理' }],
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
},
])
expect(api.callsOf('subagents.interruptByParent')).toEqual([
{ childSessionId: SID, parentSessionId: PARENT, mode: 'continuable' },
@@ -456,6 +467,7 @@ describe('prompt and cancel errors', () => {
requestId: expect.any(String) as unknown as string,
parentSessionId: PARENT, childSessionId: SID,
mode: 'continuable',
delivery: 'queue',
content,
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
},