feat(FN-3630): document lastError recovery semantics in agents.md
Documents `lastError` recovery semantics in the agents reference, clarifying how the system handles and recovers from error states. Fusion-Task-Id: FN-3630
This commit is contained in:
@@ -49,7 +49,7 @@ These fields are managed by the engine and cannot be directly edited:
|
|||||||
- `taskId` — Current working task (managed by scheduler)
|
- `taskId` — Current working task (managed by scheduler)
|
||||||
- `totalInputTokens` / `totalOutputTokens` — Token usage totals (managed by engine)
|
- `totalInputTokens` / `totalOutputTokens` — Token usage totals (managed by engine)
|
||||||
- `createdAt` / `updatedAt` / `lastHeartbeatAt` — Timestamps (managed by system)
|
- `createdAt` / `updatedAt` / `lastHeartbeatAt` — Timestamps (managed by system)
|
||||||
- `lastError` — Last error message (managed by engine)
|
- `lastError` — Last error message (managed by engine; cleared after successful recovery runs)
|
||||||
- `pauseReason` — Reason for paused state (managed by engine)
|
- `pauseReason` — Reason for paused state (managed by engine)
|
||||||
|
|
||||||
### Stale Task Link Sanitization
|
### Stale Task Link Sanitization
|
||||||
@@ -80,7 +80,7 @@ The `taskId` field is suppressed in API responses when the linked task is in a t
|
|||||||
These fields can only be set during update (not on create):
|
These fields can only be set during update (not on create):
|
||||||
|
|
||||||
- `pauseReason` — Why the agent is paused
|
- `pauseReason` — Why the agent is paused
|
||||||
- `lastError` — Last error message
|
- `lastError` — Last error message (cleared when the agent successfully recovers)
|
||||||
- `totalInputTokens` — Accumulated input token count
|
- `totalInputTokens` — Accumulated input token count
|
||||||
- `totalOutputTokens` — Accumulated output token count
|
- `totalOutputTokens` — Accumulated output token count
|
||||||
|
|
||||||
|
|||||||
@@ -325,6 +325,27 @@ describe("executeHeartbeat", () => {
|
|||||||
expect(store.endHeartbeatRun).toHaveBeenCalledWith(run.id, "terminated");
|
expect(store.endHeartbeatRun).toHaveBeenCalledWith(run.id, "terminated");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears stale lastError after a subsequent successful heartbeat run", async () => {
|
||||||
|
const store = createStoreWithAgentForExec({ state: "running" });
|
||||||
|
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||||
|
|
||||||
|
const failedRun = await monitor.startRun("agent-001", { source: "on_demand" });
|
||||||
|
await monitor.completeRun("agent-001", failedRun.id, {
|
||||||
|
status: "failed",
|
||||||
|
stderrExcerpt: "Prompt failed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const successfulRun = await monitor.startRun("agent-001", { source: "on_demand" });
|
||||||
|
await monitor.completeRun("agent-001", successfulRun.id, {
|
||||||
|
status: "completed",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "error");
|
||||||
|
expect(store.updateAgent).toHaveBeenCalledWith("agent-001", { lastError: "Prompt failed" });
|
||||||
|
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "active");
|
||||||
|
expect(store.updateAgent).toHaveBeenCalledWith("agent-001", { lastError: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
it("completes as failed when agent not found in store", async () => {
|
it("completes as failed when agent not found in store", async () => {
|
||||||
const store = createStoreWithAgentForExec();
|
const store = createStoreWithAgentForExec();
|
||||||
(store.getAgent as ReturnType<typeof vi.fn>).mockResolvedValue(null);
|
(store.getAgent as ReturnType<typeof vi.fn>).mockResolvedValue(null);
|
||||||
|
|||||||
@@ -904,8 +904,9 @@ export class HeartbeatMonitor {
|
|||||||
} else if (completionResult.status === "terminated") {
|
} else if (completionResult.status === "terminated") {
|
||||||
await this.store.updateAgentState(agentId, "paused");
|
await this.store.updateAgentState(agentId, "paused");
|
||||||
} else {
|
} else {
|
||||||
// Completed successfully - back to active
|
// Completed successfully - back to active and clear any stale failure marker.
|
||||||
await this.store.updateAgentState(agentId, "active");
|
await this.store.updateAgentState(agentId, "active");
|
||||||
|
await this.store.updateAgent(agentId, { lastError: undefined });
|
||||||
}
|
}
|
||||||
} catch (stateTransErr) {
|
} catch (stateTransErr) {
|
||||||
heartbeatLog.warn(`Agent ${agentId} state transition failed: ${stateTransErr instanceof Error ? stateTransErr.message : String(stateTransErr)} — continuing`);
|
heartbeatLog.warn(`Agent ${agentId} state transition failed: ${stateTransErr instanceof Error ? stateTransErr.message : String(stateTransErr)} — continuing`);
|
||||||
|
|||||||
Reference in New Issue
Block a user