fix(FN-096): explain role-filtered auto-claim candidates
This commit is contained in:
5
.changeset/fix-auto-claim-empty-prompt.md
Normal file
5
.changeset/fix-auto-claim-empty-prompt.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Clarify no-task heartbeat prompts when eligible Todo tasks exist but role policy filters them out of auto-claim candidates.
|
||||
@@ -986,6 +986,42 @@ describe("executeHeartbeat", () => {
|
||||
expect(store.claimTaskForAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("explains empty auto-claim prompt candidates when role policy filters eligible todo tasks", async () => {
|
||||
const store = createStoreWithAgentForExec({
|
||||
taskId: undefined,
|
||||
role: "reviewer",
|
||||
soul: "review workflows",
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
|
||||
|
||||
mockTaskStore = createMockTaskStore({
|
||||
listTasks: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "FN-CANDIDATE",
|
||||
description: "executor reliability follow-up",
|
||||
title: "Executor reliability",
|
||||
prompt: "",
|
||||
steps: [],
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
log: [],
|
||||
attachments: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as unknown as TaskDetail,
|
||||
]),
|
||||
});
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
|
||||
expect(executionPrompt).toContain("auto-claim relevant tasks: enabled (no role-compatible candidates; executor role required)");
|
||||
expect(executionPrompt).toContain("Open Task Candidates (auto-claim scan):");
|
||||
expect(executionPrompt).toContain("Snapshot found 1 eligible Todo task(s), but this agent role cannot auto-claim implementation work.");
|
||||
});
|
||||
|
||||
it("reuses one snapshot rebuild across concurrent no-task heartbeats", async () => {
|
||||
const listTasks = vi.fn().mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -2057,12 +2057,16 @@ export class HeartbeatMonitor {
|
||||
}
|
||||
|
||||
let autoClaimCandidates: AutoClaimCandidate[] = [];
|
||||
let autoClaimSnapshotCandidateCount = 0;
|
||||
let autoClaimRoleFilteredCount = 0;
|
||||
const autoClaimEnabled = isAutoClaimRelevantTasksEnabled(agent);
|
||||
if (!taskId && canRunNoTaskHeartbeat && autoClaimEnabled && this.snapshotManager) {
|
||||
try {
|
||||
const snapshot = await this.snapshotManager.getSnapshot();
|
||||
autoClaimSnapshotCandidateCount = snapshot.tasks.length;
|
||||
const roleCompatibleCandidates = snapshot.tasks.filter((candidate) => canAgentTakeImplementationTask(agent, candidate));
|
||||
const skippedIncompatibleCount = snapshot.tasks.length - roleCompatibleCandidates.length;
|
||||
autoClaimRoleFilteredCount = skippedIncompatibleCount;
|
||||
if (skippedIncompatibleCount > 0) {
|
||||
heartbeatLog.log(
|
||||
`Agent ${agentId} (role=${agent.role}) skipped auto-claim of ${skippedIncompatibleCount} implementation task(s) — only executor agents may claim implementation work`,
|
||||
@@ -2717,13 +2721,30 @@ export class HeartbeatMonitor {
|
||||
}
|
||||
|
||||
const promptCandidateLimit = resolveAutoClaimCandidatesInPromptLimit(agent, heartbeatModelSettings);
|
||||
const autoClaimStatus = autoClaimEnabled
|
||||
? (promptCandidateLimit === 0
|
||||
? "disabled (prompt-suppressed)"
|
||||
: (autoClaimCandidates.length === 0 && autoClaimSnapshotCandidateCount > 0 && autoClaimRoleFilteredCount > 0
|
||||
? "enabled (no role-compatible candidates; executor role required)"
|
||||
: "enabled"))
|
||||
: "disabled";
|
||||
const noRoleCompatibleCandidateLines = autoClaimCandidates.length === 0 && autoClaimSnapshotCandidateCount > 0 && autoClaimRoleFilteredCount > 0
|
||||
? [
|
||||
`- Snapshot found ${autoClaimSnapshotCandidateCount} eligible Todo task(s), but this agent role cannot auto-claim implementation work.`,
|
||||
"- Backlog auto-claim is restricted to executor-role agents; use delegation or create coordination follow-up instead of assuming the board is empty.",
|
||||
]
|
||||
: [];
|
||||
const candidateLines = promptCandidateLimit > 0
|
||||
? [
|
||||
"",
|
||||
"Open Task Candidates (auto-claim scan):",
|
||||
...autoClaimCandidates
|
||||
.slice(0, promptCandidateLimit)
|
||||
.map((candidate) => `- ${candidate.id}: ${candidate.title ?? candidate.descriptionFirstLine}`),
|
||||
...(
|
||||
autoClaimCandidates.length > 0
|
||||
? autoClaimCandidates
|
||||
.slice(0, promptCandidateLimit)
|
||||
.map((candidate) => `- ${candidate.id}: ${candidate.title ?? candidate.descriptionFirstLine}`)
|
||||
: noRoleCompatibleCandidateLines
|
||||
),
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -2741,7 +2762,7 @@ export class HeartbeatMonitor {
|
||||
...(wakeTriggerSourceLine ? [wakeTriggerSourceLine] : []),
|
||||
`- pending messages: ${pendingMessages.length}`,
|
||||
`- pending room messages: ${pendingRoomMessages.total}`,
|
||||
`- auto-claim relevant tasks: ${autoClaimEnabled ? (promptCandidateLimit === 0 ? "disabled (prompt-suppressed)" : "enabled") : "disabled"}`,
|
||||
`- auto-claim relevant tasks: ${autoClaimStatus}`,
|
||||
"",
|
||||
"Treat this wake delta as the highest-priority change for this heartbeat.",
|
||||
"This is an autonomous heartbeat run (manual or automatic): re-anchor on",
|
||||
|
||||
Reference in New Issue
Block a user