fix: detect agent exit for all agent types, not just idle-reporting ones (#22)
The lifecycle manager only checked isProcessRunning when detectActivity returned "idle". Agents like codex, aider, and opencode return "active" for any non-empty terminal output (including shell prompt after exit), so their exit was never detected. Now checks isProcessRunning for both "idle" and "active" states. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7ae6ab17a2
commit
1eba42097b
|
|
@ -77,7 +77,7 @@ beforeEach(() => {
|
|||
getLaunchCommand: vi.fn(),
|
||||
getEnvironment: vi.fn(),
|
||||
detectActivity: vi.fn().mockReturnValue("active" as ActivityState),
|
||||
isProcessRunning: vi.fn(),
|
||||
isProcessRunning: vi.fn().mockResolvedValue(true),
|
||||
isProcessing: vi.fn().mockResolvedValue(false),
|
||||
getSessionInfo: vi.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
|
@ -232,6 +232,33 @@ describe("check (single session)", () => {
|
|||
expect(lm.getStates().get("app-1")).toBe("killed");
|
||||
});
|
||||
|
||||
it("detects killed state when agent process exits (active terminal + dead process)", async () => {
|
||||
// Stub agents (codex, aider, opencode) return "active" for any non-empty
|
||||
// terminal output, including the shell prompt after the agent exits.
|
||||
vi.mocked(mockAgent.detectActivity).mockReturnValue("active");
|
||||
vi.mocked(mockAgent.isProcessRunning).mockResolvedValue(false);
|
||||
|
||||
const session = makeSession({ status: "working" });
|
||||
vi.mocked(mockSessionManager.get).mockResolvedValue(session);
|
||||
|
||||
writeMetadata(dataDir, "app-1", {
|
||||
worktree: "/tmp",
|
||||
branch: "main",
|
||||
status: "working",
|
||||
project: "my-app",
|
||||
});
|
||||
|
||||
const lm = createLifecycleManager({
|
||||
config,
|
||||
registry: mockRegistry,
|
||||
sessionManager: mockSessionManager,
|
||||
});
|
||||
|
||||
await lm.check("app-1");
|
||||
|
||||
expect(lm.getStates().get("app-1")).toBe("killed");
|
||||
});
|
||||
|
||||
it("stays working when agent is idle but process is still running", async () => {
|
||||
vi.mocked(mockAgent.detectActivity).mockReturnValue("idle");
|
||||
vi.mocked(mockAgent.isProcessRunning).mockResolvedValue(true);
|
||||
|
|
|
|||
|
|
@ -203,13 +203,13 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
const activity = agent.detectActivity(terminalOutput);
|
||||
if (activity === "waiting_input") return "needs_input";
|
||||
|
||||
// If the terminal looks idle, check whether the agent process is
|
||||
// still running. An idle terminal with no agent process means the
|
||||
// agent has exited (e.g. finished its task or crashed).
|
||||
if (activity === "idle") {
|
||||
const processAlive = await agent.isProcessRunning(session.runtimeHandle);
|
||||
if (!processAlive) return "killed";
|
||||
}
|
||||
// Check whether the agent process is still alive. Some agents
|
||||
// (codex, aider, opencode) return "active" for any non-empty
|
||||
// terminal output, including the shell prompt visible after exit.
|
||||
// Checking isProcessRunning for both "idle" and "active" ensures
|
||||
// exit detection works regardless of the agent's classifier.
|
||||
const processAlive = await agent.isProcessRunning(session.runtimeHandle);
|
||||
if (!processAlive) return "killed";
|
||||
}
|
||||
} catch {
|
||||
// On probe failure, preserve current stuck/needs_input state rather
|
||||
|
|
|
|||
|
|
@ -157,10 +157,12 @@ describe.skipIf(!canRun)("agent-aider (integration)", () => {
|
|||
expect(exitedRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("detectActivity → idle after agent exits", () => {
|
||||
// detectActivity is a pure terminal-text classifier; it returns "idle"
|
||||
// for empty/shell-prompt output. Process exit is detected by isProcessRunning.
|
||||
expect(exitedActivity).toBe("idle");
|
||||
it("detectActivity → idle or active after agent exits", () => {
|
||||
// Aider's stub classifier returns "active" for any non-empty output and
|
||||
// "idle" for empty output. After exit the terminal usually shows a shell
|
||||
// prompt (non-empty → "active"), but may be empty depending on timing.
|
||||
// Process exit is detected by isProcessRunning, not detectActivity.
|
||||
expect(["idle", "active"]).toContain(exitedActivity);
|
||||
});
|
||||
|
||||
it("getSessionInfo → null (not implemented for aider)", () => {
|
||||
|
|
|
|||
|
|
@ -129,10 +129,12 @@ describe.skipIf(!canRun)("agent-codex (integration)", () => {
|
|||
expect(exitedRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("detectActivity → idle after agent exits", () => {
|
||||
// detectActivity is a pure terminal-text classifier; it returns "idle"
|
||||
// for empty/shell-prompt output. Process exit is detected by isProcessRunning.
|
||||
expect(exitedActivity).toBe("idle");
|
||||
it("detectActivity → idle or active after agent exits", () => {
|
||||
// Codex's stub classifier returns "active" for any non-empty output and
|
||||
// "idle" for empty output. After exit the terminal usually shows a shell
|
||||
// prompt (non-empty → "active"), but may be empty depending on timing.
|
||||
// Process exit is detected by isProcessRunning, not detectActivity.
|
||||
expect(["idle", "active"]).toContain(exitedActivity);
|
||||
});
|
||||
|
||||
it("getSessionInfo → null (not implemented for codex)", () => {
|
||||
|
|
|
|||
|
|
@ -148,10 +148,12 @@ describe.skipIf(!canRun)("agent-opencode (integration)", () => {
|
|||
expect(exitedRunning).toBe(false);
|
||||
});
|
||||
|
||||
it("detectActivity → idle after agent exits", () => {
|
||||
// detectActivity is a pure terminal-text classifier; it returns "idle"
|
||||
// for empty/shell-prompt output. Process exit is detected by isProcessRunning.
|
||||
expect(exitedActivity).toBe("idle");
|
||||
it("detectActivity → idle or active after agent exits", () => {
|
||||
// OpenCode's stub classifier returns "active" for any non-empty output and
|
||||
// "idle" for empty output. After exit the terminal usually shows a shell
|
||||
// prompt (non-empty → "active"), but may be empty depending on timing.
|
||||
// Process exit is detected by isProcessRunning, not detectActivity.
|
||||
expect(["idle", "active"]).toContain(exitedActivity);
|
||||
});
|
||||
|
||||
it("getSessionInfo → null (not implemented for opencode)", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue