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 1/6] 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) From 042d752fd2ca7f8699011275b03a952935a0a54a 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 2/6] docs(repeat-guard): trim module orientation --- packages/guard/repeat-tool-guard/src/index.ts | 37 ++----------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/packages/guard/repeat-tool-guard/src/index.ts b/packages/guard/repeat-tool-guard/src/index.ts index d51df1d829..bd6c5a4404 100644 --- a/packages/guard/repeat-tool-guard/src/index.ts +++ b/packages/guard/repeat-tool-guard/src/index.ts @@ -1,37 +1,8 @@ /** - * Repeat-tool-call guard: advisory loop-breaker for agents stuck re-issuing - * the same tool call with identical arguments. - * - * Not a model-facing tool — it registers no tool, never vetoes or rewrites a - * call, and adds exactly one behavior: watch each agent's stream of tool calls - * through the `tools/post-execute` waterfall, count runs of consecutive calls - * to the same tool with identical canonicalized arguments, and at configured - * run lengths fold an escalating advisory reminder onto the decision's - * `additionalContext`. The loop appends that context as a logged - * `context/message` after the step's tool results, so the reminder is - * model-visible, source-attributed, and reconstructable from the session log - * with no new session event. Decision record: - * docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md. - * - * ```yaml - * - id: repeat-tool-guard - * name: '@deepseek-ai/dsh-repeat-tool-guard' - * config: - * thresholds: [3, 5, 8] # consecutive counts that trigger a reminder - * include: [] # tool-name patterns to track; empty = all tools - * exclude: [todo_write] # tool-name patterns transparent to the chain - * ``` - * - * Chain state is keyed by the live agent object — the tool registry is a - * context-level singleton whose waterfalls interleave every agent's calls, so - * a shared counter would let one agent's repetition trip another's reminder. - * State is in-memory only: a session resumed from persistence starts with a - * fresh chain (the guard is a heuristic nudge, not a logged invariant). - * - * Plugin export shape: named exports, NO default. The cordis Loader's - * `unwrapExports` does `exports.default ?? exports`, so a stray default would - * collapse the module to the bare `apply` (see docs/postmortem/0001). - * + * Advisory per-agent repeat-call detector. It enriches post-execute decisions + * with logged model context without vetoing or rewriting calls. Configuration + * and chain semantics live in the package README; rationale lives in the + * repeat-tool-guard RFC. * @module @deepseek-ai/dsh-repeat-tool-guard */ From 68b5c716b1265590f5f2936c33f9760b12c8d1db Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:29:08 +0800 Subject: [PATCH 3/6] docs(catalog): refresh AgentLoop source link --- docs/cordis-catalog/services.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 85e07f8191..f83f2bf616 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -19,7 +19,7 @@ async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise ``` -Source: [`packages/core/agent-loop/src/index.ts:373`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:391`](../../packages/core/agent-loop/src/index.ts) ## `ctx.agents` — `AgentRegistry` From b77cfd64a1cba70c1c2168d370aeab3108989a12 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:29:32 +0800 Subject: [PATCH 4/6] docs(catalog): refresh repeat guard source link --- docs/config-catalog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 46645b0575..29d37dd6db 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -548,7 +548,7 @@ export interface Config { } ``` -Source: [`packages/guard/repeat-tool-guard/src/index.ts:55`](../packages/guard/repeat-tool-guard/src/index.ts) +Source: [`packages/guard/repeat-tool-guard/src/index.ts:26`](../packages/guard/repeat-tool-guard/src/index.ts) ## `@deepseek-ai/dsh-sandbox-local` From 69c36e4fb5a6bceff4f44a8ba81ee8deb68fc1e8 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:31:20 +0800 Subject: [PATCH 5/6] docs(subagent): name parent session lineage --- packages/subagent/subagent/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/subagent/subagent/src/types.ts b/packages/subagent/subagent/src/types.ts index b0f228da96..e5ba8fd84d 100644 --- a/packages/subagent/subagent/src/types.ts +++ b/packages/subagent/subagent/src/types.ts @@ -135,8 +135,8 @@ export interface SubagentResult { export interface SubagentRun { /** * Parent-scoped run id. For a local run, this MUST equal the published child - * session id, whose `parentSession` records `request.parent`; a remote - * provider mints an id unique in the parent namespace. + * session id, whose `parentSession` records `request.parent.session.id`; a + * remote provider mints an id unique in the parent namespace. */ readonly id: SessionId /** From 9366baf39fd84f169e1df7f42048088b5cc0c790 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:31:26 +0800 Subject: [PATCH 6/6] docs(acp): describe session terminal rendering --- packages/ui/acp/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/acp/src/index.ts b/packages/ui/acp/src/index.ts index 8bb25df579..d9c53cebf0 100644 --- a/packages/ui/acp/src/index.ts +++ b/packages/ui/acp/src/index.ts @@ -1049,7 +1049,7 @@ function validateMcpServers(params: { mcpServers?: unknown[] }): void { * zero or more times per event (best-effort UI feed, never load-bearing). * @param presenter - resolves tool-owned render intent for tool events; * defaults to the generic-fallback {@link nullToolPresenter}. - * @param terminal - the connection's terminal-rendering context; defaults to + * @param terminal - the session's terminal-rendering context; defaults to * disabled (the plain-text console-block fallback). * @param options - `includeUserMessages` (default `true`): live streaming * passes `false` so a prompt the client just sent is not echoed back. @@ -1122,7 +1122,7 @@ export function todosToPlan(todos: TodoItem[]): Plan { } /** - * Per-connection terminal-rendering context threaded into + * Per-session terminal-rendering context threaded into * {@link streamSessionEventUpdate}: whether the client advertised the * `_meta.terminal_output` capability, and the session's workspace cwd (the * default terminal-card header when a tool doesn't supply its own). Kept out of