mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
The persistence seam is now create/open/stat/list returning per-session SessionHandles (read/append/flush/close); every log read and write flows through the owning handle. The seam package exports only the service and handle contracts, consumer-visible errors, and pure durable-data validation helpers; each backend owns its complete storage runtime, and the shared contract suites pin equivalent observable behavior. The backend routes published sessions' live events by id into the active write handle; agent-loop only acquires, seeds, and closes the handle. Resume appends interruptedTurnClosers through its write handle; session-query owns the revision-keyed cold cache. Legacy-only surfaces are removed in the same swap: locate/readRaw/supportsRawArtifacts, the legacy event-shape read migration, zstd torn-frame salvage, DSH_SESSION_JSONL, and hook transcript_path population; a torn final zstd frame is discarded whole; the session-list cold blank probe returns on stat metadata (eventCount derived from the last physical row, sizeBytes). The WebUI ZIP export serializes the logical log from a read handle, so both backends export identically. Refs #3245
70 lines
3.1 KiB
TypeScript
70 lines
3.1 KiB
TypeScript
/** Root-fiber shutdown drains buffered session events durably (both mount orders). */
|
|
|
|
import { describe, expect, it, afterEach } from 'vitest'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
import LlmRuntime from '@deepseek-ai/dsh-llm'
|
|
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
|
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
|
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
|
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
|
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
|
import { MockAdapter, textResponse } from './mock-adapter.ts'
|
|
|
|
const dirs: string[] = []
|
|
afterEach(async () => { for (const d of dirs.splice(0)) await rm(d, { recursive: true, force: true }) })
|
|
|
|
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
|
if (subject === agent && status === 'idle') { dispose(); resolve() }
|
|
})
|
|
})
|
|
}
|
|
|
|
async function mount(order: 'backend-first' | 'loop-first'): Promise<{ ctx: Context; root: string }> {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-shutdown-drain-'))
|
|
dirs.push(root)
|
|
const ctx = new Context()
|
|
await ctx.plugin(LlmRuntime)
|
|
await ctx.plugin(SessionStore)
|
|
await ctx.plugin(SessionProjectionRegistry)
|
|
await ctx.plugin(SystemPrompt)
|
|
await ctx.plugin(ToolRuntime)
|
|
await ctx.plugin(AgentRegistry)
|
|
if (order === 'backend-first') {
|
|
await ctx.plugin(JsonlSessionPersistence, { root })
|
|
await ctx.plugin(AgentLoop, { agents: [] })
|
|
} else {
|
|
await ctx.plugin(AgentLoop, { agents: [] })
|
|
await ctx.plugin(JsonlSessionPersistence, { root })
|
|
}
|
|
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('done')]))
|
|
return { ctx, root }
|
|
}
|
|
|
|
describe.each(['backend-first', 'loop-first'] as const)('root shutdown drain (%s)', (order) => {
|
|
it('persists buffered turn events without an explicit flush before dispose', async () => {
|
|
const { ctx, root } = await mount(order)
|
|
const sessionId = SessionId('shutdown-drain')
|
|
const handle = await ctx.agents.create({ sessionId, agentOptions: { provider: 'mock', model: 'mock' } })
|
|
handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }))
|
|
await waitForIdle(ctx, handle.agent)
|
|
// No explicit flush and no agent dispose: root teardown must drain.
|
|
await ctx.fiber.dispose()
|
|
|
|
const verify = new Context()
|
|
await verify.plugin(JsonlSessionPersistence, { root })
|
|
const reader = await verify.sessionPersistence.open(sessionId, 'read')
|
|
const events = await reader.read()
|
|
await reader.close()
|
|
expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
|
|
await verify.fiber.dispose()
|
|
})
|
|
})
|