fix(hub): beantwortete Asks über den Check-in-Kanal nachliefern

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) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-07-29 19:09:41 +02:00
parent e2bbae2044
commit a6f640242a
2 changed files with 42 additions and 0 deletions

View File

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

View File

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