From b2064cba10f3a0c19ca24472f0ee7d7f81c1816f Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:26:57 +0800 Subject: [PATCH] fix(agent-loop): reject duplicate configured identities --- packages/core/agent-loop/src/index.ts | 22 ++++++++++++++++--- .../tests/config-session-id.spec.ts | 19 ++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 325ba7b92d..7144ae8dfb 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -369,6 +369,24 @@ export interface Config { })[] } +/** Reject self-contained identity conflicts before any configured agent starts. */ +function validateConfiguredAgents(agents: Config['agents']): void { + const exactIdentities = new Map() + for (const { id, sessionId, resumeSessionId } of agents) { + const hasResumeId = resumeSessionId !== undefined && resumeSessionId !== '' + if (sessionId !== undefined && hasResumeId) { + throw new Error(`agent "${id}": sessionId and resumeSessionId are mutually exclusive`) + } + const exactIdentity = hasResumeId ? resumeSessionId : sessionId + if (exactIdentity === undefined) continue + const firstId = exactIdentities.get(exactIdentity) + if (firstId !== undefined) { + throw new Error(`agents "${firstId}" and "${id}" use duplicate exact session identity "${exactIdentity}"`) + } + exactIdentities.set(exactIdentity, id) + } +} + /** Concrete ReactLoopAgent factory and driver service. */ export class AgentLoop extends Service implements AgentFactory { static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt'] @@ -390,6 +408,7 @@ export class AgentLoop extends Service implements AgentFactory { constructor(ctx: Context, public config: Config) { super(ctx, 'agentLoop') + validateConfiguredAgents(config.agents) this.ownership = new FactoryOwnership(ctx.fiber) this.runtime = { ctx } ctx.effect(() => () => this.ownership.dispose(), 'agentLoop.transactions()') @@ -412,9 +431,6 @@ export class AgentLoop extends Service implements AgentFactory { } continue } - if (sessionId !== undefined) { - throw new Error(`agent "${id}": sessionId and resumeSessionId are mutually exclusive`) - } ctx.effect(() => { const fiber = ctx.inject(['sessionPersistence'], (childCtx: Context) => { void this.resumeWith(ctx, childCtx.sessionPersistence, { diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts index bd2a5522df..56cc0bee40 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -64,6 +64,25 @@ describe('config-driven session id', () => { await conflicting.fiber.dispose() }) + it('rejects duplicate exact ids before asynchronous configured startup', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-duplicate-')) + dirs.push(root) + const ctx = await makeCoreContext() + await ctx.plugin(SessionPersistenceJsonl, { root }) + + const outcome = await ctx.plugin(AgentLoop, { + agents: [ + { id: 'first', sessionId: SessionId('shared'), model: 'mock' }, + { id: 'second', sessionId: SessionId('shared'), model: 'mock' }, + ], + }).then(() => undefined, (error: unknown) => error) + const published = ctx.agents.get(SessionId('shared')) + await ctx.fiber.dispose() + + expect(outcome).toEqual(new Error('agents "first" and "second" use duplicate exact session identity "shared"')) + expect(published).toBeUndefined() + }) + it('restores a materialized exact id across an AgentLoop-only reload', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-reload-')) dirs.push(root)