From 4b01ab6065ea86ad9f9afd64a9d935ddec44a7f8 Mon Sep 17 00:00:00 2001 From: nicolascukas Date: Tue, 9 Jun 2026 15:38:50 +0200 Subject: [PATCH 1/2] fix(adapter): honor per-engine timeout in dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slow coding-plan engines (e.g. zai-coding-plan-glm-5.1) declare their own EngineDefinition.timeout (180s), but dispatch only used the generic command default (brainstorm/tribunal/council = 120s), so a slow engine got cut off mid-answer and lost its slot ("Failed to respond: Request timed out"). Raise options.timeout to the engine's declared timeout when it's higher, at the single dispatch chokepoint — so all exec orchestration honors it. Precedent: forge stages.kern:106 already reads engine.timeout. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/adapter-cli/src/generated/adapter.ts | 4 ++++ packages/adapter-cli/src/kern/adapter.kern | 3 +++ 2 files changed, 7 insertions(+) diff --git a/packages/adapter-cli/src/generated/adapter.ts b/packages/adapter-cli/src/generated/adapter.ts index 3d3807c44..3e162e8d9 100644 --- a/packages/adapter-cli/src/generated/adapter.ts +++ b/packages/adapter-cli/src/generated/adapter.ts @@ -25,6 +25,10 @@ export class CliAdapter implements EngineAdapter { } async dispatch(options: DispatchOptions): Promise { + // Honor the engine's own declared timeout (slow coding-plan engines like zai set timeout=180) when it exceeds the generic command default, so orchestration (brainstorm/tribunal/council, default 120s) does not cut a slow engine off mid-answer and lose its slot. + if (options.engine && Number((options.engine as any).timeout ?? 0) > Number(options.timeout ?? 0)) { + options.timeout = Number((options.engine as any).timeout); + } // Prefer CLI binary when available — API is fallback for binary-less engines const binaryPath = options.engine.binary ? this.registry.findBinary(options.engine) : null; if (!binaryPath) { diff --git a/packages/adapter-cli/src/kern/adapter.kern b/packages/adapter-cli/src/kern/adapter.kern index 29eff2d11..a95a4d8bd 100644 --- a/packages/adapter-cli/src/kern/adapter.kern +++ b/packages/adapter-cli/src/kern/adapter.kern @@ -19,6 +19,9 @@ service name=CliAdapter implements=EngineAdapter method name=dispatch params="options:DispatchOptions" returns="Promise" async=true handler lang="kern" + comment raw="// Honor the engine's own declared timeout (slow coding-plan engines like zai set timeout=180) when it exceeds the generic command default, so orchestration (brainstorm/tribunal/council, default 120s) does not cut a slow engine off mid-answer and lose its slot." + if cond="options.engine && Number((options.engine as any).timeout ?? 0) > Number(options.timeout ?? 0)" + assign target="options.timeout" value="Number((options.engine as any).timeout)" comment raw="// Prefer CLI binary when available — API is fallback for binary-less engines" let name=binaryPath value="options.engine.binary ? this.registry.findBinary(options.engine) : null" if cond="!binaryPath" From ad86ac73258e55ef8e71f66ee7f91c2b01bb1313 Mon Sep 17 00:00:00 2001 From: nicolascukas Date: Tue, 9 Jun 2026 16:08:50 +0200 Subject: [PATCH 2/2] fix(adapter): extend per-engine timeout to all 4 dispatch paths, no mutation Addresses agon review of the prior commit (4/4 reviewers): - Coverage: the timeout override was only in dispatch(); dispatchStream, dispatchAgent, dispatchAgentStream still cut slow engines at the command default. Extract a withEngineTimeout() helper and call it from all four. - No mutation: helper returns a NEW options object ({...options, timeout}) instead of mutating the caller's DispatchOptions (safe under reuse/retry). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/adapter-cli/src/generated/adapter.ts | 17 +++++++++++++---- packages/adapter-cli/src/kern/adapter.kern | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/adapter-cli/src/generated/adapter.ts b/packages/adapter-cli/src/generated/adapter.ts index 3e162e8d9..0771a1751 100644 --- a/packages/adapter-cli/src/generated/adapter.ts +++ b/packages/adapter-cli/src/generated/adapter.ts @@ -24,11 +24,17 @@ export class CliAdapter implements EngineAdapter { this.getVersion = this.getVersion.bind(this); } - async dispatch(options: DispatchOptions): Promise { - // Honor the engine's own declared timeout (slow coding-plan engines like zai set timeout=180) when it exceeds the generic command default, so orchestration (brainstorm/tribunal/council, default 120s) does not cut a slow engine off mid-answer and lose its slot. - if (options.engine && Number((options.engine as any).timeout ?? 0) > Number(options.timeout ?? 0)) { - options.timeout = Number((options.engine as any).timeout); + private withEngineTimeout(options: DispatchOptions): DispatchOptions { + // Honor the engine's declared timeout (slow coding-plan engines like zai set timeout=300) over a lower generic command default (orchestration uses 120s), so a slow engine isn't cut off mid-answer and lose its slot. Returns a NEW options object — never mutates the caller's, since callers may reuse/retry the same DispatchOptions. + const declared = Number((options.engine as any)?.timeout ?? 0); + if (declared > Number(options.timeout ?? 0)) { + return { ...options, timeout: declared }; } + return options; + } + + async dispatch(options: DispatchOptions): Promise { + options = this.withEngineTimeout(options); // Prefer CLI binary when available — API is fallback for binary-less engines const binaryPath = options.engine.binary ? this.registry.findBinary(options.engine) : null; if (!binaryPath) { @@ -123,6 +129,7 @@ export class CliAdapter implements EngineAdapter { } async *dispatchStream(options: DispatchOptions): AsyncGenerator { + options = this.withEngineTimeout(options); // workspace-pure isolation, resolved once for every dispatch path below. const iso = computeEngineIsolation(options.engine, { isolation: options.isolation, cwd: options.cwd }); // Subscription pty path for claude — yields response deltas, returns final result. @@ -219,6 +226,7 @@ export class CliAdapter implements EngineAdapter { } async dispatchAgent(options: DispatchOptions): Promise { + options = this.withEngineTimeout(options); // workspace-pure isolation, resolved once for every agent dispatch path below. const iso = computeEngineIsolation(options.engine, { isolation: options.isolation, cwd: options.cwd }); // Subscription pty path for claude (agent mode: tools + bypassed perms). @@ -375,6 +383,7 @@ export class CliAdapter implements EngineAdapter { } async *dispatchAgentStream(options: DispatchOptions): AsyncGenerator { + options = this.withEngineTimeout(options); // workspace-pure isolation, resolved once for every agent-stream path below. const iso = computeEngineIsolation(options.engine, { isolation: options.isolation, cwd: options.cwd }); // Subscription pty path for claude (agent mode). Same diff capture as dispatchAgent. diff --git a/packages/adapter-cli/src/kern/adapter.kern b/packages/adapter-cli/src/kern/adapter.kern index a95a4d8bd..3176da2d9 100644 --- a/packages/adapter-cli/src/kern/adapter.kern +++ b/packages/adapter-cli/src/kern/adapter.kern @@ -17,11 +17,17 @@ service name=CliAdapter implements=EngineAdapter assign target="this.isAvailable" value="this.isAvailable.bind(this)" assign target="this.getVersion" value="this.getVersion.bind(this)" + method name=withEngineTimeout params="options:DispatchOptions" returns="DispatchOptions" private=true + handler lang="kern" + comment raw="// Honor the engine's declared timeout (slow coding-plan engines like zai set timeout=300) over a lower generic command default (orchestration uses 120s), so a slow engine isn't cut off mid-answer and lose its slot. Returns a NEW options object — never mutates the caller's, since callers may reuse/retry the same DispatchOptions." + let name=declared value="Number((options.engine as any)?.timeout ?? 0)" + if cond="declared > Number(options.timeout ?? 0)" + return value="{ ...options, timeout: declared }" + return value="options" + method name=dispatch params="options:DispatchOptions" returns="Promise" async=true handler lang="kern" - comment raw="// Honor the engine's own declared timeout (slow coding-plan engines like zai set timeout=180) when it exceeds the generic command default, so orchestration (brainstorm/tribunal/council, default 120s) does not cut a slow engine off mid-answer and lose its slot." - if cond="options.engine && Number((options.engine as any).timeout ?? 0) > Number(options.timeout ?? 0)" - assign target="options.timeout" value="Number((options.engine as any).timeout)" + assign target="options" value="this.withEngineTimeout(options)" comment raw="// Prefer CLI binary when available — API is fallback for binary-less engines" let name=binaryPath value="options.engine.binary ? this.registry.findBinary(options.engine) : null" if cond="!binaryPath" @@ -104,6 +110,7 @@ service name=CliAdapter implements=EngineAdapter method name=dispatchStream params="options:DispatchOptions" returns="string, DispatchResult, void" async=true stream=true handler <<< + options = this.withEngineTimeout(options); // workspace-pure isolation, resolved once for every dispatch path below. const iso = computeEngineIsolation(options.engine, { isolation: options.isolation, cwd: options.cwd }); // Subscription pty path for claude — yields response deltas, returns final result. @@ -201,6 +208,7 @@ service name=CliAdapter implements=EngineAdapter method name=dispatchAgent params="options:DispatchOptions" returns="Promise" async=true handler lang="kern" + assign target="options" value="this.withEngineTimeout(options)" comment raw="// workspace-pure isolation, resolved once for every agent dispatch path below." let name=iso value="computeEngineIsolation(options.engine, { isolation: options.isolation, cwd: options.cwd })" comment raw="// Subscription pty path for claude (agent mode: tools + bypassed perms)." @@ -336,6 +344,7 @@ service name=CliAdapter implements=EngineAdapter method name=dispatchAgentStream params="options:DispatchOptions" returns="string, AgentDispatchResult, void" async=true stream=true handler <<< + options = this.withEngineTimeout(options); // workspace-pure isolation, resolved once for every agent-stream path below. const iso = computeEngineIsolation(options.engine, { isolation: options.isolation, cwd: options.cwd }); // Subscription pty path for claude (agent mode). Same diff capture as dispatchAgent.