From b47e4c5276604dd1e5a400f5f60d2c16e195df10 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:45:12 +0800 Subject: [PATCH] fix: suppress teardown startup failures --- docs/architecture.md | 2 +- docs/config-catalog.md | 2 +- docs/cordis-catalog/events.md | 4 ++-- docs/cordis-catalog/services.md | 2 +- docs/event-producer-consumer.md | 2 +- packages/core/agent-loop/README.md | 2 +- packages/core/agent-loop/src/index.ts | 4 +++- packages/core/agent-loop/tests/config-session-id.spec.ts | 5 ++++- 8 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 41ab5c716b..0eb7f1fe55 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -56,7 +56,7 @@ Default loop processing remains exposed through plugin-visible services and even A **session** is an append-only event log. A **turn** drains queued input until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points. -Startup resolves one identity. No id mints `-session-`; `sessionId` resumes stored or creates; `resumeSessionId` requires history. Failures emit `agent-loop/config-start-failed(sessionId, error)`, so front doors reject buffered work. +Startup resolves identity. No id mints `-session-`; `sessionId` resumes or creates; `resumeSessionId` requires history. Active failures emit `agent-loop/config-start-failed(sessionId, error)`, so front doors reject work; teardown stays silent. ### Turn Flow diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 484ba1c632..173806572a 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -131,7 +131,7 @@ export interface Config { Depends on: [`AgentOptions`](../packages/core/agent/src/index.ts) · [`SessionId`](../packages/core/session/src/index.ts) -Source: [`packages/core/agent-loop/src/index.ts:353`](../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:354`](../packages/core/agent-loop/src/index.ts) ## `@deepseek-ai/dsh-bash-local` diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index e44df3c3b2..df3aec8b12 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -179,13 +179,13 @@ Source: [`packages/core/agent/src/types.ts:576`](../../packages/core/agent/src/t ### `agent-loop/config-start-failed` — emit -A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. +A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. Normal factory teardown suppresses failures from the cancelled startup attempt. ```ts cordis-catalog 'agent-loop/config-start-failed'(sessionId: SessionId, error: unknown): void ``` -Source: [`packages/core/agent-loop/src/index.ts:348`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:349`](../../packages/core/agent-loop/src/index.ts) ## `approval/*` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 965a640966..74496739e2 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:368`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:369`](../../packages/core/agent-loop/src/index.ts) ## `ctx.agents` — `AgentRegistry` diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 78a30bd4e1..cc155ebb16 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -7,7 +7,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:348`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:349`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) | | `agent/created` | `emit` | [`packages/core/agent/src/types.ts:304`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio-agent`](../packages/ui/stdio-agent) | | `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:319`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) | | `agent/error` | `emit` | [`packages/core/agent/src/types.ts:593`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 2593da7c47..e27e8b625a 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -41,7 +41,7 @@ interface Config { } ``` -Agents listed in config are auto-created at startup. `cwd` seeds a fresh config-created session; a materialized exact `sessionId` remount and an explicit `resumeSessionId` keep the persisted session header. A declarative lookup, resume, setup, or publication failure is contained, logged, and emitted as `agent-loop/config-start-failed(sessionId, error)` because no live `Agent` exists for an `agent/*` signal. Config agents have no per-agent persona field: they use `dsh-system-prompt`'s deployment default, while programmatic factory callers can register an agent-scoped `deployment:persona` shadow in `setup`. The plugin registers the built-in `model`/`cwd` prompt variables on `ctx.systemPrompt`, resolved per step from `assembleContextFor(agent)` — the helper couples the typed agent with its matching scope selector. These are runtime facts of the agents THIS loop drives, unlike the `harness:identity` and default `deployment:persona` sections, which live on `dsh-system-prompt` so they survive a swapped loop plugin. +Agents listed in config are auto-created at startup. `cwd` seeds a fresh config-created session; a materialized exact `sessionId` remount and an explicit `resumeSessionId` keep the persisted session header. While the factory is active, a declarative lookup, resume, setup, or publication failure is contained, logged, and emitted as `agent-loop/config-start-failed(sessionId, error)` because no live `Agent` exists for an `agent/*` signal; cancellation caused by factory teardown is silent. Config agents have no per-agent persona field: they use `dsh-system-prompt`'s deployment default, while programmatic factory callers can register an agent-scoped `deployment:persona` shadow in `setup`. The plugin registers the built-in `model`/`cwd` prompt variables on `ctx.systemPrompt`, resolved per step from `assembleContextFor(agent)` — the helper couples the typed agent with its matching scope selector. These are runtime facts of the agents THIS loop drives, unlike the `harness:identity` and default `deployment:persona` sections, which live on `dsh-system-prompt` so they survive a swapped loop plugin. ### Exported concrete class diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 3c09ef2e7f..06c108d324 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -340,7 +340,8 @@ declare module 'cordis' { /** * A declarative agent entry failed before it could publish a live agent. * Consumers that buffer work for the configured identity use this - * transient signal to reject that work instead of waiting forever. + * transient signal to reject that work instead of waiting forever. Normal + * factory teardown suppresses failures from the cancelled startup attempt. * @param sessionId - exact shared agent/session identity that failed startup. * @param error - persistence, setup, or publication failure. * @mode emit @@ -431,6 +432,7 @@ export class AgentLoop extends Service implements AgentFactory { sessionId: SessionId, error: unknown, ): void { + if (!this.ownership.isActive()) return this.ctx.logger.warn(`agent "${configId}": config-driven ${action} of "${sessionId}" failed: ${renderThrown(error)}`) const args: unknown[] = ['agent-loop/config-start-failed', sessionId, error] for (const callback of this.ctx.events.dispatch('emit', args)) { 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 b35dc71739..53782fe03c 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -172,6 +172,8 @@ describe('config-driven session id', () => { const listing = Promise.withResolvers>>() vi.spyOn(ctx.sessionPersistence, 'list').mockReturnValue(listing.promise) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined) + const failures: unknown[] = [] + ctx.on('agent-loop/config-start-failed', (_sessionId, error) => { failures.push(error) }) const loop = await ctx.plugin(AgentLoop, { agents: [{ id: 'main', sessionId: SessionId('stdio-exact-dispose'), model: 'mock' }], @@ -181,9 +183,10 @@ describe('config-driven session id', () => { await Promise.resolve() expect(disposed).toBe(false) - listing.resolve([]) + listing.reject(new Error('startup cancelled by teardown')) await disposal expect(ctx.agents.get(SessionId('stdio-exact-dispose'))).toBeUndefined() + expect(failures).toEqual([]) expect(warn).not.toHaveBeenCalled() warn.mockRestore() await ctx.fiber.dispose()