diff --git a/src/core/services/checkinService.ts b/src/core/services/checkinService.ts index 5aab6bc..b6c41b2 100644 --- a/src/core/services/checkinService.ts +++ b/src/core/services/checkinService.ts @@ -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 + // `:`, 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(); + 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); } diff --git a/tests/checkinService.test.ts b/tests/checkinService.test.ts index e790eb3..a8fc35e 100644 --- a/tests/checkinService.test.ts +++ b/tests/checkinService.test.ts @@ -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); + }); +});