fix: --no-auth override, workflow revision in-place fix, state-driven heartbeats

Three orthogonal fixes bundled together so they re-land as a unit after
earlier worktree-based reverts kept wiping them individually.

1. `--no-auth` flag now actually disables auth. Previously a stale
   FUSION_DAEMON_TOKEN in .env silently re-armed bearer-token auth despite
   the CLI flag. Added a `noAuth` option to ServerOptions; auth-middleware's
   isDaemonAuthActive/getDaemonToken short-circuit to false/undefined when
   set; CLI plumbs opts.noAuth through both createServer call sites.

2. Workflow review failures no longer reset every completed step. Previously
   a single CSS nit from a workflow reviewer could drag 5+ already-approved
   steps back through plan review, code review, and re-execution because
   determineRevisionResetStart fuzzy-matched feedback tokens against step
   names. handleWorkflowRevisionRequest, handleWorkflowStepFailure, and
   sendTaskBackForFix now call a new reopenLastStepForRevision helper that
   flips only the last non-pending step back to pending (with currentStep
   rewind via a newly-accepted updateTask field) — all earlier done steps
   stay done, and the agent applies the feedback as an in-place patch per
   the updated PROMPT.md instructions. determineRevisionResetStart stays
   exported as @deprecated so existing unit tests still link.

3. Heartbeat scheduling is now state-driven. Previously a non-ephemeral
   agent with a stale runtimeConfig.enabled=false on disk would never tick
   and the Pause/Resume button couldn't arm the timer without also flipping
   that hidden flag. HeartbeatTriggerScheduler's watchAgentLifecycle now
   registers on transitions into active/running and clears on transitions
   out; the tick and assignment-trigger guards key off state + ephemeral
   classification. InProcessRuntime's created/updated listeners and startup
   scan mirror the same semantics. runtimeConfig.enabled is only retained
   for ephemeral (task-worker) opt-out.

Tests updated: agent-heartbeat.test.ts — one test renamed from "skips
registration when enabled is false" (obsolete behavior) to
"registers regardless of the legacy enabled flag"; 4 assignment-watching
tests now pass a realistic `state: "active"` on mock agents. 207 heartbeat
tests + 330 executor tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-22 22:12:19 -07:00
parent 66bb9ea664
commit 21d6703b22
8 changed files with 173 additions and 134 deletions

View File

@@ -492,14 +492,18 @@ export class InProcessRuntime
);
this.triggerScheduler.start();
// Set up dynamic registration for agents created or updated after startup
// Dynamic registration follows state, not runtimeConfig.enabled.
// Active/running → timer armed; anything else → not. Ephemeral
// (task-worker) agents are never armed.
const isTickable = (agent: import("@fusion/core").Agent) =>
!isEphemeralAgent(agent) && (agent.state === "active" || agent.state === "running");
this.agentCreatedListener = (agent) => {
if (!this.triggerScheduler) return;
if (!isTickable(agent)) return;
const rc = agent.runtimeConfig;
if (rc?.enabled === false) return;
this.triggerScheduler.registerAgent(agent.id, {
heartbeatIntervalMs: rc?.heartbeatIntervalMs as number | undefined,
enabled: rc?.enabled as boolean | undefined,
maxConcurrentRuns: rc?.maxConcurrentRuns as number | undefined,
});
runtimeLog.log(`Registered new agent ${agent.id} for heartbeat triggers`);
@@ -508,18 +512,17 @@ export class InProcessRuntime
this.agentUpdatedListener = (agent) => {
if (!this.triggerScheduler) return;
const rc = agent.runtimeConfig;
if (rc?.enabled === false) {
if (!isTickable(agent)) {
this.triggerScheduler.unregisterAgent(agent.id);
runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (disabled)`);
} else {
this.triggerScheduler.registerAgent(agent.id, {
heartbeatIntervalMs: rc?.heartbeatIntervalMs as number | undefined,
enabled: rc?.enabled as boolean | undefined,
maxConcurrentRuns: rc?.maxConcurrentRuns as number | undefined,
});
runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers`);
runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (state=${agent.state})`);
return;
}
const rc = agent.runtimeConfig;
this.triggerScheduler.registerAgent(agent.id, {
heartbeatIntervalMs: rc?.heartbeatIntervalMs as number | undefined,
maxConcurrentRuns: rc?.maxConcurrentRuns as number | undefined,
});
runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers (state=${agent.state})`);
};
this.agentStore.on("agent:updated", this.agentUpdatedListener);
@@ -563,21 +566,21 @@ export class InProcessRuntime
};
this.agentStore.on("agent:stateChanged", this.ephemeralTerminationListener);
// Register existing agents with heartbeat monitoring not explicitly disabled
// Agents without explicit heartbeat config will use the default 3600-second interval (1 hour)
// Register existing agents whose current state is tickable. Agents
// in paused/idle/error/terminated stay unregistered until the user
// re-activates them; the agentUpdatedListener arms the timer the
// moment state transitions to active.
try {
const agents = await this.agentStore.listAgents();
let registeredCount = 0;
for (const agent of agents) {
if (!isTickable(agent)) continue;
const rc = agent.runtimeConfig;
if (rc?.enabled !== false) {
this.triggerScheduler.registerAgent(agent.id, {
heartbeatIntervalMs: rc?.heartbeatIntervalMs as number | undefined,
enabled: rc?.enabled as boolean | undefined,
maxConcurrentRuns: rc?.maxConcurrentRuns as number | undefined,
});
registeredCount++;
}
this.triggerScheduler.registerAgent(agent.id, {
heartbeatIntervalMs: rc?.heartbeatIntervalMs as number | undefined,
maxConcurrentRuns: rc?.maxConcurrentRuns as number | undefined,
});
registeredCount++;
}
if (agents.length > 0) {
runtimeLog.log(`Registered ${registeredCount} of ${agents.length} agents for heartbeat triggers`);