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();
|
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 () => {
|
it("reuses one snapshot rebuild across concurrent no-task heartbeats", async () => {
|
||||||
const listTasks = vi.fn().mockResolvedValue([
|
const listTasks = vi.fn().mockResolvedValue([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2057,12 +2057,16 @@ export class HeartbeatMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let autoClaimCandidates: AutoClaimCandidate[] = [];
|
let autoClaimCandidates: AutoClaimCandidate[] = [];
|
||||||
|
let autoClaimSnapshotCandidateCount = 0;
|
||||||
|
let autoClaimRoleFilteredCount = 0;
|
||||||
const autoClaimEnabled = isAutoClaimRelevantTasksEnabled(agent);
|
const autoClaimEnabled = isAutoClaimRelevantTasksEnabled(agent);
|
||||||
if (!taskId && canRunNoTaskHeartbeat && autoClaimEnabled && this.snapshotManager) {
|
if (!taskId && canRunNoTaskHeartbeat && autoClaimEnabled && this.snapshotManager) {
|
||||||
try {
|
try {
|
||||||
const snapshot = await this.snapshotManager.getSnapshot();
|
const snapshot = await this.snapshotManager.getSnapshot();
|
||||||
|
autoClaimSnapshotCandidateCount = snapshot.tasks.length;
|
||||||
const roleCompatibleCandidates = snapshot.tasks.filter((candidate) => canAgentTakeImplementationTask(agent, candidate));
|
const roleCompatibleCandidates = snapshot.tasks.filter((candidate) => canAgentTakeImplementationTask(agent, candidate));
|
||||||
const skippedIncompatibleCount = snapshot.tasks.length - roleCompatibleCandidates.length;
|
const skippedIncompatibleCount = snapshot.tasks.length - roleCompatibleCandidates.length;
|
||||||
|
autoClaimRoleFilteredCount = skippedIncompatibleCount;
|
||||||
if (skippedIncompatibleCount > 0) {
|
if (skippedIncompatibleCount > 0) {
|
||||||
heartbeatLog.log(
|
heartbeatLog.log(
|
||||||
`Agent ${agentId} (role=${agent.role}) skipped auto-claim of ${skippedIncompatibleCount} implementation task(s) — only executor agents may claim implementation work`,
|
`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 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
|
const candidateLines = promptCandidateLimit > 0
|
||||||
? [
|
? [
|
||||||
"",
|
"",
|
||||||
"Open Task Candidates (auto-claim scan):",
|
"Open Task Candidates (auto-claim scan):",
|
||||||
...autoClaimCandidates
|
...(
|
||||||
.slice(0, promptCandidateLimit)
|
autoClaimCandidates.length > 0
|
||||||
.map((candidate) => `- ${candidate.id}: ${candidate.title ?? candidate.descriptionFirstLine}`),
|
? autoClaimCandidates
|
||||||
|
.slice(0, promptCandidateLimit)
|
||||||
|
.map((candidate) => `- ${candidate.id}: ${candidate.title ?? candidate.descriptionFirstLine}`)
|
||||||
|
: noRoleCompatibleCandidateLines
|
||||||
|
),
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -2741,7 +2762,7 @@ export class HeartbeatMonitor {
|
|||||||
...(wakeTriggerSourceLine ? [wakeTriggerSourceLine] : []),
|
...(wakeTriggerSourceLine ? [wakeTriggerSourceLine] : []),
|
||||||
`- pending messages: ${pendingMessages.length}`,
|
`- pending messages: ${pendingMessages.length}`,
|
||||||
`- pending room messages: ${pendingRoomMessages.total}`,
|
`- 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.",
|
"Treat this wake delta as the highest-priority change for this heartbeat.",
|
||||||
"This is an autonomous heartbeat run (manual or automatic): re-anchor on",
|
"This is an autonomous heartbeat run (manual or automatic): re-anchor on",
|
||||||
|
|||||||
Reference in New Issue
Block a user