fix(hub): dormant ≠ beschäftigt — arbeitende Agenten nicht mehr als tot melden

`dormantAgents` filterte auf `!inLoop`. `inLoop` ist aber immer false, solange
ein Agent einen Task AUSFÜHRT — dadurch meldete der Pulse jeden hart
arbeitenden Agenten als dormant. Live gesehen: codex mit 17 s altem Check-in
stand als dormant im Pulse. Genau das Signal, auf das sich der Architekt
verlassen soll, hätte ihn dazu gebracht, funktionierende Agenten anzustoßen.

Jetzt zwei getrennte Fälle:
  - `loopExitReason` gesetzt → der Agent hat den Loop ausdrücklich verlassen
    und hört nicht mehr zu: sofort melden (unverändert).
  - kein Exit-Grund, aber Task in Arbeit → er führt aus; erst nach
    DORMANT_AFTER_MS (5 min) ohne Check-in ist das mein Fall.

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

View File

@ -22,6 +22,15 @@ import { VERSION } from '../../version.js';
export const ACTIVE_WINDOW_MS = 2 * 60_000; export const ACTIVE_WINDOW_MS = 2 * 60_000;
/** Longer than this without any action ⇒ the agent is stale/offline. */ /** Longer than this without any action ⇒ the agent is stale/offline. */
export const STALE_WINDOW_MS = 10 * 60_000; export const STALE_WINDOW_MS = 10 * 60_000;
/**
* Ab wann gilt ein Agent als DORMANT (der Architekt muss eingreifen)?
*
* `inLoop` allein taugt dafür NICHT: es ist false, sobald ein Agent einen Task
* ausführt ein hart arbeitender Agent sähe damit genauso aus wie ein toter.
* Entscheidend ist, wie lange der letzte Check-in her ist. Alles darunter ist
* normale Arbeit und darf keinen Alarm auslösen.
*/
export const DORMANT_AFTER_MS = 5 * 60_000;
export type AgentLight = 'active' | 'busy' | 'idle' | 'stale'; export type AgentLight = 'active' | 'busy' | 'idle' | 'stale';

View File

@ -11,7 +11,7 @@ import { getStatus, updateStatus } from '../core/services/statusService.js';
import { autoDelegate, suggestDelegation } from '../core/services/delegateService.js'; import { autoDelegate, suggestDelegation } from '../core/services/delegateService.js';
import { computeBudget } from '../core/services/budgetService.js'; import { computeBudget } from '../core/services/budgetService.js';
import { getRoster } from '../core/services/rosterService.js'; import { getRoster } from '../core/services/rosterService.js';
import { computeHealth, enterLoop, leaveLoop, stampSeen } from '../core/services/presenceService.js'; import { computeHealth, enterLoop, leaveLoop, stampSeen, DORMANT_AFTER_MS } from '../core/services/presenceService.js';
import { resolvePending, hasPending } from '../core/services/checkinService.js'; import { resolvePending, hasPending } from '../core/services/checkinService.js';
import { loadConfig, saveConfig } from '../core/config.js'; import { loadConfig, saveConfig } from '../core/config.js';
import { agentIdentity } from '../core/services/identityService.js'; import { agentIdentity } from '../core/services/identityService.js';
@ -96,8 +96,23 @@ export async function registerRoutes(app: FastifyInstance, cwd: string): Promise
assignedTo: task.assignedTo, assignedTo: task.assignedTo,
updatedAt: task.updatedAt, updatedAt: task.updatedAt,
})), })),
// Zwei verschiedene Zustände, die nicht gleich behandelt werden dürfen:
//
// a) `loopExitReason` gesetzt → der Agent hat den Loop AUSDRÜCKLICH
// verlassen ("turn ended"). Er hört ab sofort nicht mehr zu; ein
// Reopen erreicht ihn nie. Sofort melden, ohne Wartezeit.
// b) kein Exit-Grund, aber ein Task in Arbeit → der Agent FÜHRT gerade
// aus. `inLoop` ist dabei immer false; ohne Zeitbedingung wäre jeder
// hart arbeitende Agent "dormant" und der Architekt würde Leute
// anstoßen, die einwandfrei laufen. Erst nach DORMANT_AFTER_MS ohne
// Check-in ist das ein Fall für mich.
dormantAgents: health.agents dormantAgents: health.agents
.filter((agent) => !agent.inLoop && (agent.taskId || agent.loopExitReason)) .filter((agent) => {
if (agent.inLoop) return false;
if (agent.loopExitReason) return true;
if (!agent.taskId) return false;
return agent.lastSeenAgoSec === undefined || agent.lastSeenAgoSec * 1000 >= DORMANT_AFTER_MS;
})
.map((agent) => ({ .map((agent) => ({
name: agent.name, name: agent.name,
taskId: agent.taskId, taskId: agent.taskId,