fix(FN-4249): complete Step 2 — rollback running agents on overlap requeue
Fusion-Task-Id: FN-4249 Fusion-Task-Lineage: e0052fae-38e1-4d66-8240-43d26f5790bb
This commit is contained in:
@@ -79,6 +79,11 @@ function createAgentStore(agents: MutableAgent[]): AgentStore {
|
||||
|
||||
return {
|
||||
getAgent: vi.fn(async (id: string) => byId.get(id) ?? null),
|
||||
listAgents: vi.fn(async (filters?: { state?: Agent["state"] }) => {
|
||||
const agents = Array.from(byId.values());
|
||||
if (filters?.state) return agents.filter((agent) => agent.state === filters.state);
|
||||
return agents;
|
||||
}),
|
||||
updateAgentState: vi.fn(async (id: string, state: Agent["state"]) => {
|
||||
const existing = byId.get(id);
|
||||
if (!existing) return;
|
||||
@@ -127,9 +132,10 @@ describe("scheduler overlap requeue agent-state invariant (FN-4249)", () => {
|
||||
|
||||
await workerManager.onTaskStart(queuedCandidate);
|
||||
|
||||
const scheduler = new Scheduler(taskStore);
|
||||
const scheduler = new Scheduler(taskStore, { agentStore });
|
||||
(scheduler as { running: boolean }).running = true;
|
||||
await scheduler.schedule();
|
||||
await scheduler.schedule();
|
||||
|
||||
const agent = await agentStore.getAgent("agent-assigned") as MutableAgent;
|
||||
const task = await taskStore.getTask("FN-100");
|
||||
@@ -138,5 +144,8 @@ describe("scheduler overlap requeue agent-state invariant (FN-4249)", () => {
|
||||
expect(task?.status).toBe("queued");
|
||||
expect(agent.state).toBe("active");
|
||||
expect(agent.executionTaskId ?? null).toBeNull();
|
||||
expect(agentStore.updateAgentState).toHaveBeenCalledWith("agent-assigned", "active");
|
||||
expect(agentStore.updateAgentState).toHaveBeenCalledWith("agent-assigned", "running");
|
||||
expect(agentStore.updateAgentState).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,6 +301,7 @@ export class InProcessRuntime
|
||||
maxConcurrent: this.config.maxConcurrent,
|
||||
maxWorktrees: this.config.maxWorktrees,
|
||||
semaphore: this.globalSemaphore,
|
||||
agentStore: this.agentStore,
|
||||
missionStore,
|
||||
missionAutopilot,
|
||||
missionExecutionLoop,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type MissionStore,
|
||||
type MissionFeature,
|
||||
type PrInfo,
|
||||
type AgentStore,
|
||||
} from "@fusion/core";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
@@ -108,6 +109,8 @@ export interface SchedulerOptions {
|
||||
* agents that also hold slots).
|
||||
*/
|
||||
semaphore?: AgentSemaphore;
|
||||
/** Optional AgentStore for durable-agent state rollback during overlap requeue. */
|
||||
agentStore?: AgentStore;
|
||||
/** Called when scheduler starts a task */
|
||||
onSchedule?: (task: Task) => void;
|
||||
/** Called when a task is blocked by deps */
|
||||
@@ -485,6 +488,20 @@ export class Scheduler {
|
||||
await this.store.logEntry(taskId, reason);
|
||||
}
|
||||
|
||||
private async rollbackRunningAgentsForQueuedTodoTask(taskId: string): Promise<void> {
|
||||
const agentStore = this.options.agentStore;
|
||||
if (!agentStore) return;
|
||||
|
||||
const runningAgents = await agentStore.listAgents({ state: "running", includeEphemeral: true });
|
||||
const linkedAgents = runningAgents.filter((agent) => agent.executionTaskId === taskId);
|
||||
|
||||
for (const agent of linkedAgents) {
|
||||
await agentStore.updateAgentState(agent.id, "active");
|
||||
await agentStore.syncExecutionTaskLink(agent.id, null);
|
||||
schedulerLog.log(`Rolled back running agent ${agent.id} after overlap requeue of ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `newIntervalMs` differs from the currently active timer, restart
|
||||
* the `setInterval` so the new cadence takes effect immediately.
|
||||
@@ -847,6 +864,7 @@ export class Scheduler {
|
||||
if (task.status !== "queued" || task.blockedBy !== targetBlockedBy) {
|
||||
await this.store.updateTask(task.id, { status: "queued", blockedBy: targetBlockedBy });
|
||||
}
|
||||
await this.rollbackRunningAgentsForQueuedTodoTask(task.id);
|
||||
await this.logDispatchQueuedReason(task.id, `queued — file scope overlap with ${overlappingTaskId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user