From a6f640242a03bb17c978c7cee302b0ad78fbf24a Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Wed, 29 Jul 2026 19:09:41 +0200 Subject: [PATCH] =?UTF-8?q?fix(hub):=20beantwortete=20Asks=20=C3=BCber=20d?= =?UTF-8?q?en=20Check-in-Kanal=20nachliefern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Realer Fall heute: kimi stellte ASK-0004 um 16:43 und blockierte in `agenthub_ask(wait:true)`. Der Wait läuft nach 300 s aus. Der Architekt brauchte für die Architekturentscheidung 18 Minuten — danach war kimi dormant, das korrekt emittierte `ask`-Event lief ins Leere, und ein Mensch musste den Agenten wecken. Dieselbe Taubheit wie in TSK-0274, nur an einer anderen Stelle: dormant = unerreichbar. Deshalb wandert die Antwort jetzt in den pending-Block — `resolvePending` liefert beantwortete eigene Fragen mit, sodass sie beim nächsten Hub-Kontakt (task_log, checkin, start) ankommt, egal wie lange der Architekt gebraucht hat. Tests: 290 → 292. Co-Authored-By: Claude Opus 5 (1M context) --- src/core/services/checkinService.ts | 19 +++++++++++++++++++ tests/checkinService.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/core/services/checkinService.ts b/src/core/services/checkinService.ts index 55184f5..6c8c7da 100644 --- a/src/core/services/checkinService.ts +++ b/src/core/services/checkinService.ts @@ -1,5 +1,6 @@ import { listTasks } from './taskService.js'; import { listMessages } from './messageService.js'; +import { listAsks } from './askService.js'; import { agentIdentity } from './identityService.js'; /** @@ -49,6 +50,16 @@ export interface PendingForAgent { waitingTasks: string[]; /** Vom Agenten eingereichte Tasks, die auf das Architekten-Review warten. */ awaitingReview: string[]; + /** + * Vom Agenten gestellte Fragen, die inzwischen beantwortet sind. + * + * Warum das hierher gehört: `agenthub_ask(wait:true)` wartet nur 300 s. Der + * Architekt braucht für eine Architekturentscheidung regelmäßig länger (real: + * 18 min bei ASK-0004). Danach ist der Agent dormant und die Antwort erreicht + * ihn nie — ein Mensch muss ihn wecken. Über den Check-in kommt sie beim + * nächsten Hub-Kontakt an. + */ + answeredAsks: Array<{ id: string; answer: string }>; unreadCount: number; messages: PendingMessagePreview[]; /** Klartext für das Modell — nur gesetzt, wenn wirklich etwas anliegt. */ @@ -87,10 +98,16 @@ export function resolvePending( const pending: PendingForAgent = { waitingTasks: [], awaitingReview: [], + answeredAsks: [], unreadCount: 0, messages: [], }; + for (const ask of listAsks(cwd, { status: 'answered' })) { + if (!isMine(ask.from)) continue; + pending.answeredAsks.push({ id: ask.id, answer: previewOf(ask.answer) }); + } + for (const task of tasks) { if (task.status === 'open' && isMine(task.assignedTo)) pending.waitingTasks.push(task.id); else if (task.status === 'review' && isMine(task.claimedBy ?? task.assignedTo)) pending.awaitingReview.push(task.id); @@ -129,6 +146,7 @@ export function resolvePending( const parts: string[] = []; if (pending.interrupted) parts.push(pending.interrupted.action); if (pending.unreadCount > 0) parts.push(`${pending.unreadCount} ungelesene Nachricht(en) — agenthub_inbox lesen und antworten.`); + if (pending.answeredAsks.length > 0) parts.push(`Deine Frage(n) ${pending.answeredAsks.map((a) => a.id).join(', ')} sind BEANTWORTET — lies die Antwort, bevor du weiterbaust.`); if (pending.waitingTasks.length > 0) parts.push(`Offen für dich: ${pending.waitingTasks.join(', ')}.`); if (parts.length > 0) pending.note = parts.join(' '); @@ -140,6 +158,7 @@ export function hasPending(pending: PendingForAgent): boolean { return ( pending.interrupted !== undefined || pending.unreadCount > 0 || + pending.answeredAsks.length > 0 || pending.waitingTasks.length > 0 ); } diff --git a/tests/checkinService.test.ts b/tests/checkinService.test.ts index 46c8c03..9cf733d 100644 --- a/tests/checkinService.test.ts +++ b/tests/checkinService.test.ts @@ -5,6 +5,7 @@ import { join } from 'node:path'; import { createTask, claimTask, reviewTask, reopenTask, cancelTask, assignTask } from '../src/core/services/taskService.js'; import { createMessage } from '../src/core/services/messageService.js'; import { resolvePending, hasPending } from '../src/core/services/checkinService.js'; +import { createAsk, answerAsk } from '../src/core/services/askService.js'; /** * TSK-0274 — der Check-in-Kanal. @@ -133,3 +134,25 @@ describe('resolvePending — Nachrichten und Zuweisungen', () => { expect(JSON.stringify(pending).length).toBeLessThan(1500); }); }); + +describe('resolvePending — beantwortete Fragen (Ask-Timeout-Lücke)', () => { + it('liefert die Antwort auf eine eigene Frage nach — auch lange nach dem 300s-Wait', () => { + const ask = createAsk(cwd, { from: 'kimi', to: 'claude', question: 'Welcher Weg?' }); + answerAsk(cwd, ask.id, 'Nimm Variante B.', 'claude'); + + const pending = resolvePending(cwd, 'kimi'); + + // agenthub_ask(wait) gibt nach 300s auf; braucht der Architekt laenger, + // erreicht die Antwort den dormanten Agenten sonst NIE. + expect(pending.answeredAsks.map((a) => a.id)).toContain(ask.id); + expect(pending.answeredAsks[0]?.answer).toContain('Variante B'); + expect(pending.note).toMatch(/BEANTWORTET/); + expect(hasPending(pending)).toBe(true); + }); + + it('zeigt fremde Antworten nicht an', () => { + const ask = createAsk(cwd, { from: 'codex', to: 'claude', question: 'X?' }); + answerAsk(cwd, ask.id, 'Y', 'claude'); + expect(resolvePending(cwd, 'kimi').answeredAsks).toHaveLength(0); + }); +});