feat(FN-940): allow terminated agents to restart as active or running

- Remove blanket block on terminated→any state transitions in agent-store
- Add active and running as valid transitions from terminated state in types
- Update tests to verify terminated→active and terminated→running succeed
- Add tests for invalid terminated transitions (idle, paused) still throwing
This commit is contained in:
gsxdsm
2026-04-04 17:40:31 -07:00
parent 9a8d68fce8
commit e5497db55a
3 changed files with 24 additions and 10 deletions

View File

@@ -405,17 +405,35 @@ describe("AgentStore", () => {
).rejects.toThrow("Invalid state transition: idle -> terminated");
});
it("transition from terminated to any state throws", async () => {
it("transition from terminated to invalid states throws", async () => {
const agent = await createReadyAgent(store, "Terminated");
await store.updateAgentState(agent.id, "active");
await store.updateAgentState(agent.id, "terminated");
await expect(
store.updateAgentState(agent.id, "active")
).rejects.toThrow("Cannot transition from terminated");
await expect(
store.updateAgentState(agent.id, "idle")
).rejects.toThrow("Cannot transition from terminated");
).rejects.toThrow("Invalid state transition: terminated -> idle");
await expect(
store.updateAgentState(agent.id, "paused")
).rejects.toThrow("Invalid state transition: terminated -> paused");
});
it("terminated → active transition succeeds", async () => {
const agent = await createReadyAgent(store, "RestartActive");
await store.updateAgentState(agent.id, "active");
await store.updateAgentState(agent.id, "terminated");
const updated = await store.updateAgentState(agent.id, "active");
expect(updated.state).toBe("active");
});
it("terminated → running transition succeeds", async () => {
const agent = await createReadyAgent(store, "RestartRunning");
await store.updateAgentState(agent.id, "active");
await store.updateAgentState(agent.id, "terminated");
const updated = await store.updateAgentState(agent.id, "running");
expect(updated.state).toBe("running");
});
it("emits both 'agent:stateChanged' and 'agent:updated' events", async () => {

View File

@@ -244,10 +244,6 @@ export class AgentStore extends EventEmitter {
return agent; // No change needed
}
if (currentState === "terminated") {
throw new Error(`Cannot transition from terminated state to ${newState}`);
}
const validTransitions = AGENT_VALID_TRANSITIONS[currentState];
if (!validTransitions.includes(newState)) {
throw new Error(

View File

@@ -1292,7 +1292,7 @@ export const AGENT_VALID_TRANSITIONS: Record<AgentState, AgentState[]> = {
running: ["active", "paused", "error", "terminated"],
paused: ["active", "terminated"],
error: ["active", "terminated"],
terminated: [], // Terminal state - no exits
terminated: ["active", "running"], // Can be restarted
};
/** Single heartbeat event recorded for an agent */