From e5bd28488df8191710b80e2dad3312f4f39283af Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Wed, 29 Jul 2026 20:57:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(work):=20workTimeoutSec=20pro=20Agent=20?= =?UTF-8?q?=E2=80=94=20codex=20und=20kimi=20auf=20dasselbe=20Niveau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Work-Loop blockierte fuer ALLE Agenten 50s. Der Wert stammt von Kimi-Code, dessen MCP-Client Requests nach gut einer Minute abbricht (-32001). Codex vertraegt Minuten — musste aber mit demselben Minimum leben und kehrte dadurch sechsmal so oft leer zurueck wie noetig. Jede Rueckkehr ist eine Gelegenheit, den Turn zu beenden und aus dem Loop zu fallen; genau daran unterschied sich codex' Zuverlaessigkeit von kimis. Ein globaler Default zwingt alle auf die Grenze des schwaechsten Clients. Die Grenze gehoert aber zum Agenten, nicht zum Hub — deshalb jetzt `agents[].workTimeoutSec` im Roster, ausgeliefert ueber /agents/:agent/identity. Aufloesung: explizites Argument > Roster > globaler Default. Konfiguriert: codex 240s, kimi 50s. Co-Authored-By: Claude Opus 5 (1M context) --- src/cli/remoteClient.ts | 4 ++-- src/core/schema.ts | 11 +++++++++++ src/mcp/server.ts | 14 +++++++++++++- src/server/routes.ts | 9 ++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/cli/remoteClient.ts b/src/cli/remoteClient.ts index c33d427..30f5aad 100644 --- a/src/cli/remoteClient.ts +++ b/src/cli/remoteClient.ts @@ -43,8 +43,8 @@ export const remoteClient = { return request(baseUrl, 'GET', '/health'); }, - async getAgentIdentity(baseUrl: string, agent: string): Promise<{ canonical: string; names: string[] }> { - return request<{ canonical: string; names: string[] }>( + async getAgentIdentity(baseUrl: string, agent: string): Promise<{ canonical: string; names: string[]; workTimeoutSec?: number }> { + return request<{ canonical: string; names: string[]; workTimeoutSec?: number }>( baseUrl, 'GET', `/agents/${encodeURIComponent(agent)}/identity`, diff --git a/src/core/schema.ts b/src/core/schema.ts index eb45195..7fa6368 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -162,6 +162,17 @@ export const AgentConfigSchema = z.object({ dispatch: AgentDispatch.default('loop'), /** Previous/alternate names that resolve to this canonical roster key. */ aliases: z.array(z.string().min(1)).default([]), + /** + * Wie lange `agenthub_work` fuer DIESEN Agenten blockieren darf. + * + * Der Wert haengt am MCP-CLIENT, nicht am Hub: Kimi-Code bricht Requests nach + * gut einer Minute ab (-32001), Codex vertraegt mehrere Minuten. Ein globaler + * Default zwingt beide auf das Minimum des schwaechsten Clients — der + * robustere Agent kehrt dann unnoetig oft leer zurueck, und JEDE Rueckkehr + * ist eine Gelegenheit, aus dem Loop zu fallen. Genau daran unterschied sich + * codex' Zuverlaessigkeit von kimis. + */ + workTimeoutSec: z.number().int().positive().max(600).optional(), /** Display model, e.g. "Opus 4.8", "Sonnet 4.6", "Kimi K2", "GPT-5 Codex". */ model: z.string().optional(), /** Provider/company for the logo: "anthropic" | "openai" | "moonshot" | … */ diff --git a/src/mcp/server.ts b/src/mcp/server.ts index ad86acd..7f54841 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -301,7 +301,19 @@ export async function startMcpServer(cwd: string): Promise { ? `No task in review for ${agent}, and no hub server to wait on.` : `No open task addressed to ${agent}, and no hub server to wait on.`; if (!remote) return asText(emptyMsg); - const effectiveTimeout = timeoutSec ?? DEFAULT_WORK_TIMEOUT_SEC; + // Timeout-Aufloesung: explizites Argument > Roster-Einstellung des Agenten + // > globaler Default. Der Roster-Wert existiert, weil die Grenze am + // MCP-CLIENT haengt: Kimi-Code bricht nach gut einer Minute ab, Codex + // vertraegt Minuten. Ein globaler Default zwingt alle auf das Minimum des + // schwaechsten Clients — der robustere Agent kehrt dann unnoetig oft leer + // zurueck, und jede Rueckkehr ist eine Gelegenheit, aus dem Loop zu fallen. + let rosterTimeout: number | undefined; + if (timeoutSec === undefined) { + try { + rosterTimeout = (await remoteClient.getAgentIdentity(serverUrl!, agent)).workTimeoutSec; + } catch { /* Roster nicht erreichbar — Default greift */ } + } + const effectiveTimeout = timeoutSec ?? rosterTimeout ?? DEFAULT_WORK_TIMEOUT_SEC; let result; try { result = await waitForTask(serverUrl!, finder, effectiveTimeout); diff --git a/src/server/routes.ts b/src/server/routes.ts index db00d61..c2f6878 100644 --- a/src/server/routes.ts +++ b/src/server/routes.ts @@ -148,7 +148,14 @@ export async function registerRoutes(app: FastifyInstance, cwd: string): Promise app.get('/agents/:agent/identity', async (request, reply) => { try { const identity = agentIdentity(cwd, (request.params as { agent: string }).agent); - return { canonical: identity.canonical, names: [...identity.names] }; + // workTimeoutSec gehoert fachlich zur Agenten-Identitaet: "wer bin ich und + // wie lange darf ich blockieren". Der MCP-Server holt es sich hier, statt + // dass jeder Client es raten muss. + let workTimeoutSec: number | undefined; + try { + workTimeoutSec = loadConfig(cwd).agents?.[identity.canonical]?.workTimeoutSec; + } catch { /* ohne Config bleibt der Default */ } + return { canonical: identity.canonical, names: [...identity.names], workTimeoutSec }; } catch (err) { return badRequest(reply, err instanceof Error ? err.message : String(err)); }