fix(hub): Check-in adressiert wie der Work-Loop (Titel-Präfix + Handoff)

Der Check-in prüfte nur `assignedTo`, `findAddressedOpenTask` dagegen
Titel-Präfix `<name>:`, Handoff-Empfänger UND `assignedTo`. Folge: `agenthub
start codex` meldete "AKTUELL: nichts offen", während TSK-0268 mit
`codex:`-Präfix offen lag — der Work-Loop hätte sie unmittelbar danach
geclaimt. Zwei Wahrheiten über dieselbe Frage.

Jetzt nutzt der Check-in dieselbe Adressierungslogik.

Tests: 293 → 295.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-07-29 19:18:02 +02:00
parent 4be0826a6f
commit 1250359e6d
2 changed files with 40 additions and 1 deletions

View File

@ -1,6 +1,7 @@
import { listTasks } from './taskService.js';
import { listMessages } from './messageService.js';
import { listAsks } from './askService.js';
import { listHandoffs } from './handoffService.js';
import { agentIdentity } from './identityService.js';
/**
@ -108,8 +109,28 @@ export function resolvePending(
pending.answeredAsks.push({ id: ask.id, answer: previewOf(ask.answer) });
}
// Adressierung EXAKT wie in findAddressedOpenTask (start.ts): Titel-Präfix
// `<name>:`, Handoff-Empfänger ODER assignedTo. Prüfte der Check-in nur
// assignedTo, meldete der Session-Start "nichts offen" — und der Work-Loop
// claimte unmittelbar danach eine Task, die er über das Titel-Präfix findet.
// Zwei Wahrheiten über dieselbe Frage sind genau das, was hier nicht passieren darf.
let addressedByHandoff = new Set<string>();
try {
addressedByHandoff = new Set(
listHandoffs(cwd)
.filter((h) => h.toAgent && isMine(h.toAgent) && h.taskId)
.map((h) => String(h.taskId)),
);
} catch {
// Handoffs nicht lesbar — Präfix/assignedTo tragen weiterhin.
}
const addressedToMe = (task: { id: string; title?: string; assignedTo?: string }) =>
[...names].some((n) => (task.title ?? '').toLowerCase().startsWith(`${n}:`)) ||
addressedByHandoff.has(task.id) ||
isMine(task.assignedTo);
for (const task of tasks) {
if (task.status === 'open' && isMine(task.assignedTo)) pending.waitingTasks.push(task.id);
if (task.status === 'open' && addressedToMe(task)) pending.waitingTasks.push(task.id);
else if (task.status === 'review' && isMine(task.claimedBy ?? task.assignedTo)) pending.awaitingReview.push(task.id);
}

View File

@ -170,3 +170,21 @@ describe('resolvePending — `delivered` zaehlt weiter', () => {
expect(hasPending(pending)).toBe(true);
});
});
describe('resolvePending — Adressierung wie im Work-Loop', () => {
it('erkennt eine Task am Titel-Praefix, auch ohne assignedTo', () => {
// Genau der Fall, der den Session-Start luegen liess: Task traegt das
// Praefix, aber kein assignedTo — der Work-Loop haette sie sofort geclaimt.
const task = createTask(cwd, { title: 'codex: Blocker-Redesign', role: 'implementer' });
const pending = resolvePending(cwd, 'codex');
expect(pending.waitingTasks).toContain(task.id);
expect(hasPending(pending)).toBe(true);
});
it('beansprucht keine fremd adressierte Task', () => {
createTask(cwd, { title: 'kimi: nicht meins', role: 'implementer' });
expect(resolvePending(cwd, 'codex').waitingTasks).toHaveLength(0);
});
});