refactor(agents): remove terminated AgentState; collapse to paused/error

Drops "terminated" from AGENT_STATES. The agent lifecycle now runs through
idle | active | running | paused | error. paused (carrying a pauseReason)
absorbs every former terminated use case — manual stop, heartbeat run
termination, spawned-child cleanup. Run status (agentRuns.status) is
unchanged: "terminated" stays a valid run-status value.

AGENT_VALID_TRANSITIONS allows direct any→idle transitions so resetAgent
no longer needs the intermediate hop.

Stack-wide:
- core/agent-store: lastError clearing + resetAgent simplified.
- engine/agent-heartbeat, executor, in-process-runtime: terminated state
  writes → paused; halt-state listener fires on paused/error.
- dashboard: AgentsView/AgentListModal/AgentDetailView lose the Terminated
  badge/option/state-block; agent pickers no longer filter terminated;
  agentHealth drops the Terminated branch; routes/state cast widened to
  the new AgentState union.

Tests across core and engine updated to assert paused for AgentState and
left "terminated" intact for run-status assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-05 19:30:42 -07:00
parent ea7c533be0
commit 202f88b80f
21 changed files with 81 additions and 157 deletions

View File

@@ -1173,7 +1173,9 @@ export class AgentStore extends EventEmitter {
state: newState,
updatedAt: new Date().toISOString(),
// Clear lastError when transitioning away from terminated
...(currentState === "terminated" && newState !== "terminated" && { lastError: undefined }),
// Clear lastError when an agent re-enters an actionable state so
// a resumed agent does not carry stale "Error" badges.
...((newState === "active" || newState === "running") && { lastError: undefined }),
};
await this.writeAgent(updated);
@@ -1432,11 +1434,9 @@ export class AgentStore extends EventEmitter {
await this.endHeartbeatRun(activeRun.id, "terminated");
}
// Normalize to terminated first when idle is not directly reachable.
if (agent.state !== "idle" && agent.state !== "terminated") {
agent = await this.updateAgentState(agentId, "terminated");
}
// Any non-idle state can transition directly to idle in the new
// lifecycle (see AGENT_VALID_TRANSITIONS in types.ts), so no
// intermediate hop is required.
if (agent.state !== "idle") {
agent = await this.updateAgentState(agentId, "idle");
}