fix(work): bereits gehaltene Task wird genannt statt verschwiegen
Vom CEO gefunden: TSK-0293 stand auf in_progress bei codex — und codex tat nichts. Ursache: `findAddressedOpenTask` sucht ausschliesslich Tasks mit Status `open`. Haelt ein Agent bereits eine in_progress-Task (typisch nach einem Session-Neustart), greift der Busy-Guard und der Loop liefert NICHTS zurueck. Der Agent wartet dann auf neue Arbeit, die nie kommt, waehrend seine eigene Task unbearbeitet liegt — und von aussen sieht es aus, als sei er tot. `resolvePending` liefert jetzt `heldTask`, und alle drei Wege sagen es dem Agenten deutlich: - `agenthub start <agent>`: "DU HAELTST BEREITS <id>" ganz oben - `agenthub work` (CLI): nennt die Task und beendet sich, statt zu warten - `agenthub_work` (MCP): gibt Task samt Body zurueck statt zu blockieren Tests: 295 → 297. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8a1728cbce
commit
908a043ac5
@ -201,6 +201,11 @@ export async function startAgentSession(options: StartAgentOptions): Promise<str
|
|||||||
? ((await remoteClient.getPending(serverUrl, agent)) as ReturnType<typeof resolvePending>)
|
? ((await remoteClient.getPending(serverUrl, agent)) as ReturnType<typeof resolvePending>)
|
||||||
: resolvePending(projectCwd, agent);
|
: resolvePending(projectCwd, agent);
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
// Zuerst und am deutlichsten: eine bereits gehaltene Task. Sonst wartet der
|
||||||
|
// Agent nach einem Neustart auf neue Arbeit, waehrend seine eigene liegt.
|
||||||
|
if (pending.heldTask) {
|
||||||
|
lines.push(`⚠️ DU HAELTST BEREITS ${pending.heldTask} — arbeite DORT weiter und reiche sie ein, bevor du Neues anfaengst.`);
|
||||||
|
}
|
||||||
if (pending.waitingTasks.length) lines.push(`Offen für dich: ${pending.waitingTasks.join(', ')}`);
|
if (pending.waitingTasks.length) lines.push(`Offen für dich: ${pending.waitingTasks.join(', ')}`);
|
||||||
if (pending.awaitingReview.length) lines.push(`Wartet auf Architekten-Review: ${pending.awaitingReview.join(', ')} — du bist so lange gebunden.`);
|
if (pending.awaitingReview.length) lines.push(`Wartet auf Architekten-Review: ${pending.awaitingReview.join(', ')} — du bist so lange gebunden.`);
|
||||||
if (pending.unreadCount) lines.push(`Ungelesene Nachrichten: ${pending.unreadCount}`);
|
if (pending.unreadCount) lines.push(`Ungelesene Nachrichten: ${pending.unreadCount}`);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { parseSSEBuffer } from './watch.js';
|
import { parseSSEBuffer } from './watch.js';
|
||||||
import { announceAgent, findAddressedOpenTask, claimAndPrintTask, type AgentContext } from './start.js';
|
import { announceAgent, findAddressedOpenTask, claimAndPrintTask, type AgentContext } from './start.js';
|
||||||
|
import { resolvePending } from '../../core/services/checkinService.js';
|
||||||
import { discoverServer as discoverHubServer } from '../../discovery.js';
|
import { discoverServer as discoverHubServer } from '../../discovery.js';
|
||||||
import { remoteClient } from '../remoteClient.js';
|
import { remoteClient } from '../remoteClient.js';
|
||||||
|
|
||||||
@ -61,6 +62,21 @@ async function drainAgentMessages(ctx: WorkAgentContext): Promise<number> {
|
|||||||
export async function workAgent(ctx: WorkAgentContext): Promise<void> {
|
export async function workAgent(ctx: WorkAgentContext): Promise<void> {
|
||||||
await announceAgent(ctx.serverUrl, ctx.agent, ctx.role);
|
await announceAgent(ctx.serverUrl, ctx.agent, ctx.role);
|
||||||
|
|
||||||
|
// Haelt der Agent schon eine Task? Der Finder unten sucht nur `open` — nach
|
||||||
|
// einem Session-Neustart wartet der Worker deshalb auf neue Arbeit, waehrend
|
||||||
|
// die eigene, bereits geclaimte Task unbearbeitet liegt. Das war ein echter
|
||||||
|
// Stillstand: Task auf in_progress, Agent tut nichts, niemand sieht warum.
|
||||||
|
try {
|
||||||
|
const held = resolvePending(ctx.projectCwd, ctx.agent).heldTask;
|
||||||
|
if (held) {
|
||||||
|
console.log(
|
||||||
|
`AgentHub: du haeltst bereits ${held}. Arbeite dort weiter und reiche sie mit `
|
||||||
|
+ `\`agenthub task review ${held}\` ein — erst danach kommt Neues.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch { /* ohne Projekt-Kontext weiter wie bisher */ }
|
||||||
|
|
||||||
// Already-waiting task?
|
// Already-waiting task?
|
||||||
const found = await findAddressedOpenTask(ctx);
|
const found = await findAddressedOpenTask(ctx);
|
||||||
if (found) {
|
if (found) {
|
||||||
|
|||||||
@ -49,6 +49,18 @@ export interface PendingForAgent {
|
|||||||
};
|
};
|
||||||
/** Offene Tasks, die an den Agenten adressiert sind (inkl. Reopens). */
|
/** Offene Tasks, die an den Agenten adressiert sind (inkl. Reopens). */
|
||||||
waitingTasks: string[];
|
waitingTasks: string[];
|
||||||
|
/**
|
||||||
|
* Die Task, die der Agent GERADE HAELT (in_progress, von ihm geclaimt).
|
||||||
|
*
|
||||||
|
* Klingt trivial, war aber eine echte Sackgasse: der Work-Loop sucht nur
|
||||||
|
* Tasks mit Status `open`. Haelt ein Agent bereits eine in_progress-Task —
|
||||||
|
* etwa nach einem Session-Neustart —, greift der Busy-Guard und der Loop
|
||||||
|
* liefert NICHTS zurueck. Der Agent wartet dann auf neue Arbeit, die nie
|
||||||
|
* kommt, waehrend seine eigene Task unbearbeitet liegt. Deshalb muss der
|
||||||
|
* gehaltene Task ueberall auftauchen, wo ein Agent nach "was ist zu tun"
|
||||||
|
* fragt.
|
||||||
|
*/
|
||||||
|
heldTask?: string;
|
||||||
/** Vom Agenten eingereichte Tasks, die auf das Architekten-Review warten. */
|
/** Vom Agenten eingereichte Tasks, die auf das Architekten-Review warten. */
|
||||||
awaitingReview: string[];
|
awaitingReview: string[];
|
||||||
/**
|
/**
|
||||||
@ -130,7 +142,8 @@ export function resolvePending(
|
|||||||
isMine(task.assignedTo);
|
isMine(task.assignedTo);
|
||||||
|
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
if (task.status === 'open' && addressedToMe(task)) pending.waitingTasks.push(task.id);
|
if (task.status === 'in_progress' && isMine(task.claimedBy ?? task.assignedTo)) pending.heldTask = task.id;
|
||||||
|
else 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);
|
else if (task.status === 'review' && isMine(task.claimedBy ?? task.assignedTo)) pending.awaitingReview.push(task.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +187,9 @@ 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.heldTask && !pending.interrupted) {
|
||||||
|
parts.push(`Du haeltst bereits ${pending.heldTask} — arbeite dort weiter und reiche sie ein, bevor du Neues anfaengst.`);
|
||||||
|
}
|
||||||
if (pending.unreadCount > 0) parts.push(`${pending.unreadCount} unbeantwortete Nachricht(en) — agenthub_inbox lesen, beantworten und mit agenthub_message quittieren.`);
|
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(', ')}.`);
|
||||||
@ -186,6 +202,7 @@ export function resolvePending(
|
|||||||
export function hasPending(pending: PendingForAgent): boolean {
|
export function hasPending(pending: PendingForAgent): boolean {
|
||||||
return (
|
return (
|
||||||
pending.interrupted !== undefined ||
|
pending.interrupted !== undefined ||
|
||||||
|
pending.heldTask !== undefined ||
|
||||||
pending.unreadCount > 0 ||
|
pending.unreadCount > 0 ||
|
||||||
pending.answeredAsks.length > 0 ||
|
pending.answeredAsks.length > 0 ||
|
||||||
pending.waitingTasks.length > 0
|
pending.waitingTasks.length > 0
|
||||||
|
|||||||
@ -311,6 +311,21 @@ export async function startMcpServer(cwd: string, options: { agent?: string } =
|
|||||||
LOOP += ' UNATTENDED MODE: never pause for human input — when you need a decision, ' +
|
LOOP += ' UNATTENDED MODE: never pause for human input — when you need a decision, ' +
|
||||||
'call agenthub_ask (it routes to the architect) and await the answer instead of stalling.';
|
'call agenthub_ask (it routes to the architect) and await the answer instead of stalling.';
|
||||||
}
|
}
|
||||||
|
// Haelt der Agent bereits eine Task, wartet der Finder ins Leere (er sucht
|
||||||
|
// nur `open`). Das nennen wir jetzt beim Namen, statt ihn blockieren zu
|
||||||
|
// lassen — sonst wartet er auf Arbeit, die er laengst hat.
|
||||||
|
if (!reviewer) {
|
||||||
|
const held = resolvePending(root, agent).heldTask;
|
||||||
|
if (held) {
|
||||||
|
const detail = remote ? await remoteClient.getTask(serverUrl!, held) : getTask(root, held);
|
||||||
|
return asText({
|
||||||
|
held,
|
||||||
|
body: (detail as { body?: string }).body,
|
||||||
|
note: `Du haeltst bereits ${held}. Arbeite dort weiter und reiche sie mit agenthub_task_review ein — erst danach bekommst du Neues.`,
|
||||||
|
loop: LOOP,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const immediate = await finder();
|
const immediate = await finder();
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
await leave('work delivered');
|
await leave('work delivered');
|
||||||
|
|||||||
@ -188,3 +188,24 @@ describe('resolvePending — Adressierung wie im Work-Loop', () => {
|
|||||||
expect(resolvePending(cwd, 'codex').waitingTasks).toHaveLength(0);
|
expect(resolvePending(cwd, 'codex').waitingTasks).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resolvePending — bereits gehaltene Task', () => {
|
||||||
|
it('nennt die in_progress-Task, die der Agent haelt', () => {
|
||||||
|
const id = openTaskFor('codex');
|
||||||
|
claimTask(cwd, id, 'codex');
|
||||||
|
|
||||||
|
const pending = resolvePending(cwd, 'codex');
|
||||||
|
|
||||||
|
// Der Work-Loop sucht nur `open` — ohne diesen Hinweis wartet ein Agent
|
||||||
|
// nach dem Neustart auf neue Arbeit, waehrend seine eigene liegt.
|
||||||
|
expect(pending.heldTask).toBe(id);
|
||||||
|
expect(pending.note).toMatch(/haeltst bereits/i);
|
||||||
|
expect(hasPending(pending)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('meldet keine fremde in_progress-Task als gehalten', () => {
|
||||||
|
const id = openTaskFor('kimi');
|
||||||
|
claimTask(cwd, id, 'kimi');
|
||||||
|
expect(resolvePending(cwd, 'codex').heldTask).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user