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)); }