fix: bind stdio to its exact fresh identity

This commit is contained in:
Tianyi Cui
2026-07-14 09:54:34 +08:00
parent 43812841a5
commit 6b782b0189
10 changed files with 88 additions and 62 deletions
@@ -93,6 +93,19 @@ describe('dsh-stdio-agent app', () => {
await ctx.fiber.dispose()
})
it('normalizes an empty resume id to a fresh exact app identity', async () => {
const ctx = await mount({
model: 'mock',
resumeSessionId: '',
persistenceRoot: '/tmp/dsh-stdio-agent-spec-empty-resume',
skills: await isolatedSkillsConfig(),
})
const agent = ctx.get('agents')?.list()[0]
expect(agent?.id).toMatch(/^main-session-[0-9a-f-]{36}$/)
expect(agent?.id).toBe(agent?.session.id)
await ctx.fiber.dispose()
})
it('defaults persistenceRoot and welcome when omitted', async () => {
// Direct apply (NOT via ctx.plugin, which validates+defaults the config
// first) so the runtime `?? './.sessions'` / `?? 'ready.'` fallbacks on
@@ -74,7 +74,7 @@ function chunkEvent(chunk: StreamChunk): SessionEvent {
return { type: 'assistant/chunk', seq: 0, time: 0, data: { turn: 1, step: 0, chunk } }
}
const CONFIG: Config = { welcome: 'hi there', resumeSessionId: 'main' }
const CONFIG: Config = { welcome: 'hi there', sessionId: 'main' }
async function setup(config: Config = CONFIG, runtimeOver: Partial<StdioRuntime> = {}) {
const ctx = new Context()
@@ -199,7 +199,7 @@ describe('createStdioChat rendering', () => {
})
it('accepts a lineage-bearing configured agent created after the UI installs', async () => {
const { ctx, input } = await setup({ welcome: 'hi there', resumeSessionId: 'resumed' })
const { ctx, input } = await setup({ welcome: 'hi there', sessionId: 'resumed' })
const unrelated = makeAgent('unrelated')
ctx.agents.register(unrelated)
const resumed = makeAgent('resumed')
@@ -247,33 +247,21 @@ describe('createStdioChat rendering', () => {
expect(out.text()).toContain('[main turn 1] ')
})
it('retargets a surviving root when HMR publishes it before disposing the old root', async () => {
const { ctx, input } = await setup({ welcome: 'hi there' })
const oldRoot = makeAgent('main-session-old')
const child = makeAgent('child')
;(child.session.header as { parentSession?: string }).parentSession = oldRoot.id
const replacement = makeAgent('main-session-replacement')
const lateChild = makeAgent('late-child')
it('retargets only the exact identity after loop HMR recreation', async () => {
const { ctx, input } = await setup({ welcome: 'hi there', sessionId: 'main-session-fixed' })
const oldRoot = makeAgent('main-session-fixed')
const prefixCollision = makeAgent('main-session-unrelated')
const disposeOld = ctx.agents.register(oldRoot)
const disposeChild = ctx.agents.enter(child, oldRoot)
ctx.agents.announce(child)
ctx.agents.register(replacement)
const disposeLateChild = ctx.agents.enter(lateChild, replacement)
ctx.agents.announce(lateChild)
// The replacement's created edge arrived while oldRoot was still targeted.
// A replacement-owned child then arrived even later. Once oldRoot is
// removed, runtime ownership still identifies replacement as the only
// surviving root instead of selecting either newer child by insertion order.
ctx.agents.register(prefixCollision)
disposeOld()
const replacement = makeAgent('main-session-fixed')
ctx.agents.register(replacement)
input.feed('after hmr')
await new Promise(resolve => setImmediate(resolve))
expect(child.sent).toEqual([])
expect(lateChild.sent).toEqual([])
expect(prefixCollision.sent).toEqual([])
expect(replacement.sent).toEqual([[{ type: 'text', text: 'after hmr' }]])
disposeLateChild()
disposeChild()
})
it('does not retarget stdin to an unrelated root after the configured agent is disposed', async () => {
@@ -740,7 +728,7 @@ describe('createStdioChat input', () => {
})
it('drives the exact app-configured resumed session', async () => {
const { ctx, input } = await setup({ welcome: 'w', resumeSessionId: 'worker' })
const { ctx, input } = await setup({ welcome: 'w', sessionId: 'worker' })
const agent = makeAgent('worker')
ctx.agents.register(agent)
input.feed('hi')
@@ -748,14 +736,6 @@ describe('createStdioChat input', () => {
expect(agent.sent).toHaveLength(1)
})
it('treats an empty resume session id as a fresh configured identity', async () => {
const { ctx, input } = await setup({ welcome: 'w', resumeSessionId: '' })
const agent = makeAgent('main-session-fresh')
ctx.agents.register(agent)
input.feed('hi')
await new Promise(r => setImmediate(r))
expect(agent.sent).toHaveLength(1)
})
})
describe('createStdioChat EOF exit', () => {