feat(FN-4153): complete Step 3 — wire scheduler permanent-agent assignment toggle

Fusion-Task-Id: FN-4153
Fusion-Task-Lineage: 6ec09467-c7a0-4a9d-9a09-16d5ef6f7a86
This commit is contained in:
Fusion
2026-05-13 01:07:37 -07:00
committed by gsxdsm
parent 992ae3a92e
commit efd5a8a4aa
2 changed files with 54 additions and 12 deletions

View File

@@ -22,6 +22,7 @@ import { resolveEffectiveNode } from "./effective-node.js";
import { applyUnavailableNodePolicy } from "./node-routing-policy.js";
import type { NodeDispatchValidationResult } from "./node-dispatch-validation.js";
import type { MeshLeaseManager } from "./mesh-lease-manager.js";
import { selectPermanentAgentForTask } from "./agent-assignment.js";
/**
* Check whether two sets of file scope paths overlap.
@@ -179,6 +180,8 @@ export class Scheduler {
private wasNodeBlocked = new Set<string>();
/** Tracks tasks blocked by missing project-node mapping to deduplicate block log entries. */
private wasNodeDispatchValidationBlocked = new Set<string>();
/** Tracks tasks queued due to missing permanent executors when ephemeral workers are disabled. */
private wasPermanentAgentUnavailable = new Set<string>();
/** Tracks dispatch-queued reason signatures to avoid per-tick log spam. */
private wasDispatchQueuedReasonLogged = new Set<string>();
@@ -465,6 +468,7 @@ export class Scheduler {
this.failedTaskIds.clear();
this.wasNodeBlocked.clear();
this.wasNodeDispatchValidationBlocked.clear();
this.wasPermanentAgentUnavailable.clear();
this.wasDispatchQueuedReasonLogged.clear();
schedulerLog.log("Stopped");
}
@@ -772,6 +776,7 @@ export class Scheduler {
// Resolve dependency order among todo tasks
const ordered = resolveDependencyOrder(todo);
let started = 0;
let loggedMissingAgentStoreThisPass = false;
for (const taskId of ordered) {
const task = tasks.find((t) => t.id === taskId)!;
@@ -954,6 +959,42 @@ export class Scheduler {
}
}
if (latestSettings.ephemeralAgentsEnabled === false && !freshTask.assignedAgentId) {
if (!this.options.agentStore) {
if (!loggedMissingAgentStoreThisPass) {
loggedMissingAgentStoreThisPass = true;
schedulerLog.warn("ephemeralAgentsEnabled=false but scheduler has no agentStore; falling back to legacy dispatch behavior");
}
} else {
const selectedAgent = await selectPermanentAgentForTask({
task: freshTask,
agentStore: this.options.agentStore,
taskStore: this.store,
});
if (!selectedAgent) {
await this.store.updateTask(task.id, { status: "queued" });
if (!this.wasPermanentAgentUnavailable.has(task.id)) {
await this.logDispatchQueuedReason(
task.id,
"queued — no permanent executor available (ephemeral agents disabled)",
);
this.wasPermanentAgentUnavailable.add(task.id);
}
continue;
}
await this.store.updateTask(task.id, { assignedAgentId: selectedAgent.id });
await this.store.logEntry(
task.id,
`Auto-assigned to permanent agent ${selectedAgent.id} (ephemeral agents disabled)`,
);
this.wasPermanentAgentUnavailable.delete(task.id);
}
} else {
this.wasPermanentAgentUnavailable.delete(task.id);
}
// Clear status, reserve worktree path, and then move to in-progress.
// Reset mergeRetries so a fresh execution gets a fresh merge budget —
// otherwise a task whose previous run exhausted its 3 retries (e.g.
@@ -976,6 +1017,7 @@ export class Scheduler {
});
this.wasNodeBlocked.delete(task.id);
this.wasNodeDispatchValidationBlocked.delete(task.id);
this.wasPermanentAgentUnavailable.delete(task.id);
this.clearDispatchQueuedReasonMemo(task.id);
await this.store.logEntry(task.id, `Node routing resolved: ${effectiveNode.nodeId ?? "local"} (source: ${effectiveNode.source})`);
this.options.onSchedule?.(task);