fix(hub): delivered-Nachrichten bleiben im Check-in sichtbar

`delivered` heißt nur "einmal ausgeliefert", nicht "gelesen" — abgeschlossen
ist eine Nachricht erst mit read/acked. Der Check-in filterte aber auf
`unread` und ließ damit jede Nachricht fallen, die irgendwo schon einmal
aufgetaucht war (etwa beim Session-Start), ohne dass der Agent sie je
beantwortet hätte.

Real aufgefallen an einer Architekten-Korrektur an kimi: Status `delivered`,
im pending-Block unsichtbar — die Korrektur wäre still verlorengegangen.
Genau der Fehlermodus, gegen den der Kanal gebaut wurde.

Tests: 292 → 294.

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

View File

@ -135,7 +135,15 @@ export function resolvePending(
} }
} }
const unread = listMessages(cwd).filter((m) => m.status === 'unread' && isMine(m.to)); // `unread` UND `delivered`: `delivered` heißt nur "einmal ausgeliefert", nicht
// "gelesen" — erst read/acked schließt eine Nachricht ab. Filterte man hier
// auf `unread`, verschwände jede Nachricht aus dem Rückkanal, sobald sie
// einmal irgendwo aufgetaucht ist (z. B. beim Session-Start), ohne dass der
// Agent sie je beantwortet hat. Real passiert bei einer Architekten-Korrektur
// an kimi: Status `delivered`, im Check-in unsichtbar.
const unread = listMessages(cwd).filter(
(m) => (m.status === 'unread' || m.status === 'delivered') && isMine(m.to),
);
pending.unreadCount = unread.length; pending.unreadCount = unread.length;
pending.messages = unread.slice(-MAX_MESSAGE_PREVIEWS).map((m) => ({ pending.messages = unread.slice(-MAX_MESSAGE_PREVIEWS).map((m) => ({
id: m.id, id: m.id,
@ -145,7 +153,7 @@ export function resolvePending(
const parts: string[] = []; const parts: string[] = [];
if (pending.interrupted) parts.push(pending.interrupted.action); 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.unreadCount > 0) parts.push(`${pending.unreadCount} unbeantwortete Nachricht(en) — agenthub_inbox lesen, beantworten und mit agenthub_message quittieren.`);
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.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 (pending.waitingTasks.length > 0) parts.push(`Offen für dich: ${pending.waitingTasks.join(', ')}.`);
if (parts.length > 0) pending.note = parts.join(' '); if (parts.length > 0) pending.note = parts.join(' ');

View File

@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { createTask, claimTask, reviewTask, reopenTask, cancelTask, assignTask } from '../src/core/services/taskService.js'; import { createTask, claimTask, reviewTask, reopenTask, cancelTask, assignTask } from '../src/core/services/taskService.js';
import { createMessage } from '../src/core/services/messageService.js'; import { createMessage, markMessageDelivered } from '../src/core/services/messageService.js';
import { resolvePending, hasPending } from '../src/core/services/checkinService.js'; import { resolvePending, hasPending } from '../src/core/services/checkinService.js';
import { createAsk, answerAsk } from '../src/core/services/askService.js'; import { createAsk, answerAsk } from '../src/core/services/askService.js';
@ -98,7 +98,7 @@ describe('resolvePending — Nachrichten und Zuweisungen', () => {
expect(pending.unreadCount).toBe(1); expect(pending.unreadCount).toBe(1);
expect(pending.messages[0]?.from).toBe('claude'); expect(pending.messages[0]?.from).toBe('claude');
expect(pending.messages[0]?.preview).toContain('Reihenfolge'); expect(pending.messages[0]?.preview).toContain('Reihenfolge');
expect(pending.note).toMatch(/ungelesene/i); expect(pending.note).toMatch(/unbeantwortete/i);
}); });
it('nennt offene, an den Agenten adressierte Tasks', () => { it('nennt offene, an den Agenten adressierte Tasks', () => {
@ -156,3 +156,17 @@ describe('resolvePending — beantwortete Fragen (Ask-Timeout-Lücke)', () => {
expect(resolvePending(cwd, 'kimi').answeredAsks).toHaveLength(0); expect(resolvePending(cwd, 'kimi').answeredAsks).toHaveLength(0);
}); });
}); });
describe('resolvePending — `delivered` zaehlt weiter', () => {
it('zeigt eine bereits ausgelieferte, aber unbeantwortete Nachricht weiter an', () => {
const msg = createMessage(cwd, { from: 'claude', to: 'kimi', text: 'Korrektur: nimm doch Variante A.' });
markMessageDelivered(cwd, msg.id);
const pending = resolvePending(cwd, 'kimi');
// `delivered` heisst nur "einmal aufgetaucht". Filterte der Check-in auf
// `unread`, ginge eine Architekten-Korrektur still verloren.
expect(pending.unreadCount).toBe(1);
expect(hasPending(pending)).toBe(true);
});
});