feat: durable command lifecycle logging in the executor (command/run + command/done)

CommandService.execute appends the log-only pair around every resolved
handler — run before invocation, done at settlement, including thrown and
aborted handlers (kind:'error'); admission misses log nothing. commandId is
minted monotonically per instance; per-session appends serialize through a
tail queue over SessionStore.appendOutOfBand (zero-step wrap on an idle log,
direct join inside an open turn). The invariant companion now asserts the
pairing relation (unique run ids; a done requires a prior in-log run).
CommandSource is a minimal merge-extensible map (user variant only).
Dependent benches mount SessionStore; TUI/e2e snapshots re-recorded for the
executor's durable-append timing and the /status event counts.
This commit is contained in:
imccyu
2026-07-27 17:37:41 +08:00
parent e900ebd4c6
commit 4e50369eb6
15 changed files with 384 additions and 110 deletions

View File

@@ -3,7 +3,7 @@ import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { RUN_CODE_NAME, defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { createScope } from '@deepseek-ai/dsh-scope'
import UserInteractionService, { type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-interaction'
@@ -24,7 +24,9 @@ const PLAN_CONFIG = { section: TEST_PLAN_SECTION } satisfies PlanModeConfig
*/
async function agentWithSession(ctx: Context, id = 'agent-1', { active }: { active?: boolean } = {}): Promise<Agent & { session: Session }> {
const session = new Session(SessionId(id))
// A live store session when a store is mounted (the command executor logs
// lifecycle events through it); bare otherwise (fold/tool-only benches).
const session = ctx.get('sessions')?.create(SessionId(id)) ?? new Session(SessionId(id))
const agent = { id: SessionId(id), session, options: {} } as unknown as Agent & { session: Session }
let scoped!: Context
await ctx.plugin(Object.assign((inner: Context) => { scoped = createScope(inner, agent).ctx }, {
@@ -488,6 +490,7 @@ describe('/plan', () => {
expect(bare.get('commands')).toBeUndefined()
const ctx = await setup()
await ctx.plugin(SessionStore)
await ctx.plugin(CommandService)
// The `ctx.inject` child mounts asynchronously once `commands` resolves.
await new Promise(resolve => setImmediate(resolve))
@@ -526,6 +529,7 @@ describe('/plan', () => {
it('leaves active plan mode, cancels a pending entry, and treats inactive exit as idempotent', async () => {
const ctx = await setup()
await ctx.plugin(SessionStore)
await ctx.plugin(CommandService)
await new Promise(resolve => setImmediate(resolve))
const signal = new AbortController().signal
@@ -564,6 +568,7 @@ describe('/plan', () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(CommandService)
const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG)
await new Promise(resolve => setImmediate(resolve))