fix(scope): close remaining ownership boundaries

This commit is contained in:
Tianyi Cui
2026-07-12 03:51:55 +08:00
parent 3dca90261c
commit 36b8370027
79 changed files with 3957 additions and 817 deletions

View File

@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
@@ -99,6 +99,22 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx.fiber.dispose()
})
it('createAgent sends raw metadata to the session validator before cloning can sanitize it', async () => {
class ExoticMeta {
readonly cwd = '/accepted'
}
const { ctx } = await persistentHarness(new MockAdapter([textResponse('hi')]))
await expect(ctx.agents.create({
agentId: AgentId('exotic-meta-agent'),
sessionId: SessionId('exotic-meta-session'),
meta: new ExoticMeta(),
})).rejects.toThrow(/session metadata is not a plain JSON record/)
expect(ctx.agents.get(AgentId('exotic-meta-agent'))).toBeUndefined()
expect(ctx.sessions.get(SessionId('exotic-meta-session'))).toBeUndefined()
await ctx.fiber.dispose()
})
it('resume of a session with no cwd carries an undefined cwd header', async () => {
// Lifecycle 1: create a no-cwd session and run a turn.
const adapter1 = new MockAdapter([textResponse('a')])
@@ -411,6 +427,54 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.fiber.dispose()
})
it('reads each loaded metadata field once before reconstructing a resumed session', async () => {
const sessionId = SessionId('resume-loaded-meta-once')
const root = await persistSession(sessionId)
const ctx = await mountPersistentHarness(root, new MockAdapter([textResponse('next')]))
const loaded = await ctx.sessionPersistence.load(sessionId)
const reads = { createdAt: 0, cwd: 0, parentSession: 0, seedLength: 0 }
const meta = Object.defineProperties({
version: loaded.meta.version,
id: loaded.meta.id,
}, {
createdAt: {
enumerable: true,
get: () => { reads.createdAt += 1; return reads.createdAt === 1 ? loaded.meta.createdAt : 1n },
},
cwd: {
enumerable: true,
get: () => { reads.cwd += 1; return reads.cwd === 1 ? '/loaded' : 'relative' },
},
parentSession: {
enumerable: true,
get: () => { reads.parentSession += 1; return reads.parentSession === 1 ? SessionId('parent') : 1n },
},
seedLength: {
enumerable: true,
get: () => { reads.seedLength += 1; return reads.seedLength === 1 ? 0 : 1n },
},
}) as unknown as SessionHeader
ctx.sessionPersistence.load = () => Promise.resolve({ meta, events: loaded.events })
const resumed = await ctx.agents.resume({
agentId: AgentId('resume-loaded-meta-once'),
resumeSessionId: sessionId,
agentOptions: { model: 'mock' },
})
expect(reads).toEqual({ createdAt: 1, cwd: 1, parentSession: 1, seedLength: 1 })
expect(resumed.agent.session.header).toEqual({
version: loaded.meta.version,
id: sessionId,
createdAt: loaded.meta.createdAt,
cwd: '/loaded',
parentSession: 'parent',
seedLength: 0,
})
await resumed.dispose()
await ctx.fiber.dispose()
})
it('an idle inject() is flushed durably on its own (survives without explicit flush/dispose)', async () => {
// Lifecycle 1: run a turn, then inject context while idle. The idle inject
// wraps its context/message in a one-shot turn AND checkpoints it (the turn-enclosure RFC)

View File

@@ -605,7 +605,7 @@ describe('P1-5: a started turn (and any open step) is always closed on a boundar
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants, { freeze: false })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
}
@@ -1014,7 +1014,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants, { freeze: false })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
// Blocking listener on the parent context (survives fiber disposal).
@@ -1071,7 +1071,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants, { freeze: false })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
const unlisten = ctx.on('system-prompt/assemble', async function (_assembly, _context, next) {
@@ -1127,7 +1127,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants, { freeze: false })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
ctx.on('agent/pre-step', async () => {
@@ -1179,7 +1179,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants, { freeze: false })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
ctx.on('agent/pre-step', async () => {
@@ -1228,7 +1228,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants, { freeze: false })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
ctx.on('system-prompt/assemble', async function (_assembly, _context, next) {

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
@@ -307,6 +307,36 @@ describe('agent scope lifecycle', () => {
await retry.dispose()
})
it('rejects an exotic seed before publishing either reserved identity', async () => {
const ctx = await harness()
const published: string[] = []
ctx.on('session/created', () => { published.push('session') })
ctx.on('agent/created', () => { published.push('agent') })
class ExoticData { readonly value = 'not durable JSON' }
const seed = [{
seq: 0,
type: 'test/exotic-seed',
data: new ExoticData(),
}] as unknown as SessionEvent[]
await expect(ctx.agents.create({
agentId: AgentId('exotic-seed'),
sessionId: SessionId('exotic-seed-session'),
agentOptions: { model: 'mock' },
seed,
})).rejects.toThrow(/seed event at index 0 is not losslessly JSON-serializable/)
expect(published).toEqual([])
expect(ctx.agents.get(AgentId('exotic-seed'))).toBeUndefined()
expect(ctx.sessions.get(SessionId('exotic-seed-session'))).toBeUndefined()
const retry = await ctx.agents.create({
agentId: AgentId('exotic-seed'),
sessionId: SessionId('exotic-seed-session'),
agentOptions: { model: 'mock' },
})
await retry.dispose()
})
it('a throwing session/created listener disposes the scope (pre-nesting rollback window)', async () => {
const ctx = await harness()
let boom = true