diff --git a/fixtures/harness/antigravity_cli/matched.json b/fixtures/harness/antigravity_cli/matched.json index 80e68c9a3..754d854b0 100644 --- a/fixtures/harness/antigravity_cli/matched.json +++ b/fixtures/harness/antigravity_cli/matched.json @@ -111,7 +111,7 @@ { "name": "idle-3003", "type": "antigravity_cli", - "status": "idle", + "status": "waiting", "summary": "Antigravity CLI session active", "pid": 3003, "projectPath": "/proj/idle", diff --git a/fixtures/harness/codex/statuses.json b/fixtures/harness/codex/statuses.json index fffde99f9..170659610 100644 --- a/fixtures/harness/codex/statuses.json +++ b/fixtures/harness/codex/statuses.json @@ -100,7 +100,7 @@ { "name": "st-idle-4005", "type": "codex", - "status": "idle", + "status": "waiting", "summary": "old reply", "pid": 4005, "projectPath": "/proj/st-idle", diff --git a/fixtures/harness/grok_cli/fallback.json b/fixtures/harness/grok_cli/fallback.json index d3f2d7f17..dad02020e 100644 --- a/fixtures/harness/grok_cli/fallback.json +++ b/fixtures/harness/grok_cli/fallback.json @@ -80,7 +80,7 @@ { "name": "idle-81004", "type": "grok_cli", - "status": "idle", + "status": "waiting", "summary": "old prompt", "pid": 81004, "projectPath": "/proj/idle", diff --git a/packages/agent-manager/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts b/packages/agent-manager/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts index f76e16f9d..c67c65643 100644 --- a/packages/agent-manager/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts +++ b/packages/agent-manager/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts @@ -56,7 +56,7 @@ describe("DevinAgentMapper", () => { const agent = mapper.mapSessionToAgent( session(), { - lastRole: "assistant", + lastRole: "user", lastTimeUpdated: Date.now() - 10 * 60 * 1000, summary: "", }, @@ -65,6 +65,19 @@ describe("DevinAgentMapper", () => { expect(agent.status).toBe(AgentStatus.IDLE); }); + it("keeps stale sessions awaiting input waiting", () => { + const agent = mapper.mapSessionToAgent( + session(), + { + lastRole: "assistant", + lastTimeUpdated: Date.now() - 10 * 60 * 1000, + summary: "", + }, + proc(), + ); + expect(agent.status).toBe(AgentStatus.WAITING); + }); + it("falls back to session title then directory for summary and project path", () => { const agent = mapper.mapSessionToAgent( session({ title: "Hello Greeting Session" }), diff --git a/packages/agent-manager/src/__tests__/harnesses/gemini/GeminiCliAdapter.test.ts b/packages/agent-manager/src/__tests__/harnesses/gemini/GeminiCliAdapter.test.ts index 994dd1c7f..5a598c73b 100644 --- a/packages/agent-manager/src/__tests__/harnesses/gemini/GeminiCliAdapter.test.ts +++ b/packages/agent-manager/src/__tests__/harnesses/gemini/GeminiCliAdapter.test.ts @@ -823,10 +823,22 @@ describe("GeminiCliAdapter", () => { summary: "", sessionStart: new Date(), lastActive: new Date(Date.now() - 10 * 60 * 1000), - lastMessageType: "gemini", + lastMessageType: "user", }; expect(parser.determineStatus(session)).toBe(AgentStatus.IDLE); }); + + it('should stay "waiting" past the idle threshold when awaiting input', () => { + const session = { + sessionId: "s", + projectPath: "", + summary: "", + sessionStart: new Date(), + lastActive: new Date(Date.now() - 10 * 60 * 1000), + lastMessageType: "gemini", + }; + expect(parser.determineStatus(session)).toBe(AgentStatus.WAITING); + }); }); describe("parseSession", () => { diff --git a/packages/agent-manager/src/__tests__/harnesses/opencode/OpenCodeAgentMapper.test.ts b/packages/agent-manager/src/__tests__/harnesses/opencode/OpenCodeAgentMapper.test.ts index 9f250d398..729059ca5 100644 --- a/packages/agent-manager/src/__tests__/harnesses/opencode/OpenCodeAgentMapper.test.ts +++ b/packages/agent-manager/src/__tests__/harnesses/opencode/OpenCodeAgentMapper.test.ts @@ -58,7 +58,7 @@ describe("OpenCodeAgentMapper", () => { { lastRole: "assistant", lastTimeUpdated: staleTime, - lastAssistantCompleted: true, + lastAssistantCompleted: false, lastAssistantErrored: false, summary: "", }, @@ -68,6 +68,24 @@ describe("OpenCodeAgentMapper", () => { expect(agent.status).toBe(AgentStatus.IDLE); }); + it("keeps stale sessions awaiting input waiting", () => { + const staleTime = Date.now() - 10 * 60 * 1000; + + const agent = mapper.mapSessionToAgent( + { sessionId: "sess-4", directory: "/old", timeCreated: staleTime }, + { + lastRole: "assistant", + lastTimeUpdated: staleTime, + lastAssistantCompleted: true, + lastAssistantErrored: false, + summary: "", + }, + { pid: 126, command: "opencode", cwd: "/old" }, + ); + + expect(agent.status).toBe(AgentStatus.WAITING); + }); + it("maps unmatched processes as running process-only agents", () => { const agent = mapper.mapProcessOnlyAgent({ pid: 500, command: "opencode", cwd: "/repo" }); diff --git a/packages/agent-manager/src/harnesses/antigravity/AntigravitySessionParser.ts b/packages/agent-manager/src/harnesses/antigravity/AntigravitySessionParser.ts index 2369c540f..d4b1651f8 100644 --- a/packages/agent-manager/src/harnesses/antigravity/AntigravitySessionParser.ts +++ b/packages/agent-manager/src/harnesses/antigravity/AntigravitySessionParser.ts @@ -130,17 +130,17 @@ export class AntigravitySessionParser { } /** + * - last turn is an assistant reply → WAITING (awaiting input) * - past the idle threshold → IDLE - * - last turn is an assistant reply → WAITING (awaiting user) * - otherwise (last turn was a user message, or unknown) → RUNNING */ determineStatus(session: AntigravitySession): AgentStatus { - if (isIdle(session.lastActive)) { - return AgentStatus.IDLE; - } if (session.lastRole === "assistant") { return AgentStatus.WAITING; } + if (isIdle(session.lastActive)) { + return AgentStatus.IDLE; + } return AgentStatus.RUNNING; } diff --git a/packages/agent-manager/src/harnesses/codex/CodexSessionParser.ts b/packages/agent-manager/src/harnesses/codex/CodexSessionParser.ts index 21e924f0d..b3b4b86b4 100644 --- a/packages/agent-manager/src/harnesses/codex/CodexSessionParser.ts +++ b/packages/agent-manager/src/harnesses/codex/CodexSessionParser.ts @@ -188,10 +188,6 @@ export class CodexSessionParser { } determineStatus(session: CodexSession): AgentStatus { - if (isIdle(session.lastActive)) { - return AgentStatus.IDLE; - } - if ( session.lastPayloadType === "agent_message" || session.lastPayloadType === "task_complete" || @@ -200,6 +196,10 @@ export class CodexSessionParser { return AgentStatus.WAITING; } + if (isIdle(session.lastActive)) { + return AgentStatus.IDLE; + } + return AgentStatus.RUNNING; } diff --git a/packages/agent-manager/src/harnesses/copilot/CopilotSessionParser.ts b/packages/agent-manager/src/harnesses/copilot/CopilotSessionParser.ts index 02bd5f400..fe1d933c7 100644 --- a/packages/agent-manager/src/harnesses/copilot/CopilotSessionParser.ts +++ b/packages/agent-manager/src/harnesses/copilot/CopilotSessionParser.ts @@ -276,14 +276,14 @@ export class CopilotSessionParser { } determineStatus(session: CopilotSession): AgentStatus { - if (isIdle(session.lastActive)) { - return AgentStatus.IDLE; - } - if (session.lastEventType !== undefined && WAITING_EVENTS.has(session.lastEventType)) { return AgentStatus.WAITING; } + if (isIdle(session.lastActive)) { + return AgentStatus.IDLE; + } + return AgentStatus.RUNNING; } diff --git a/packages/agent-manager/src/harnesses/devin/DevinAgentMapper.ts b/packages/agent-manager/src/harnesses/devin/DevinAgentMapper.ts index c11d12554..fed721ed6 100644 --- a/packages/agent-manager/src/harnesses/devin/DevinAgentMapper.ts +++ b/packages/agent-manager/src/harnesses/devin/DevinAgentMapper.ts @@ -33,7 +33,7 @@ export class DevinAgentMapper { } private determineStatus(stats: DevinSessionStats, lastActive: Date): AgentStatus { - if (isIdle(lastActive)) return AgentStatus.IDLE; - return stats.lastRole === "assistant" ? AgentStatus.WAITING : AgentStatus.RUNNING; + if (stats.lastRole === "assistant") return AgentStatus.WAITING; + return isIdle(lastActive) ? AgentStatus.IDLE : AgentStatus.RUNNING; } } diff --git a/packages/agent-manager/src/harnesses/gemini/GeminiSessionParser.ts b/packages/agent-manager/src/harnesses/gemini/GeminiSessionParser.ts index c86da05f2..62350b2cc 100644 --- a/packages/agent-manager/src/harnesses/gemini/GeminiSessionParser.ts +++ b/packages/agent-manager/src/harnesses/gemini/GeminiSessionParser.ts @@ -141,14 +141,14 @@ export class GeminiSessionParser { } determineStatus(session: GeminiSession): AgentStatus { - if (isIdle(session.lastActive)) { - return AgentStatus.IDLE; - } - if (session.lastMessageType === "gemini" || session.lastMessageType === "assistant") { return AgentStatus.WAITING; } + if (isIdle(session.lastActive)) { + return AgentStatus.IDLE; + } + return AgentStatus.RUNNING; } diff --git a/packages/agent-manager/src/harnesses/grok/GrokSessionParser.ts b/packages/agent-manager/src/harnesses/grok/GrokSessionParser.ts index 9237a33fb..c17f02bc3 100644 --- a/packages/agent-manager/src/harnesses/grok/GrokSessionParser.ts +++ b/packages/agent-manager/src/harnesses/grok/GrokSessionParser.ts @@ -119,17 +119,17 @@ export class GrokSessionParser { /** * Determine agent status from parsed session state. * + * - last transcript turn is an assistant message → WAITING (awaiting input) * - past the idle threshold → IDLE - * - last transcript turn is an assistant message → WAITING (awaiting user) * - otherwise (last turn was a user message, or unknown) → RUNNING */ determineStatus(session: GrokSession): AgentStatus { - if (isIdle(session.lastActive)) { - return AgentStatus.IDLE; - } if (session.lastRole === "assistant") { return AgentStatus.WAITING; } + if (isIdle(session.lastActive)) { + return AgentStatus.IDLE; + } return AgentStatus.RUNNING; } diff --git a/packages/agent-manager/src/harnesses/kiro/KiroSessionParser.ts b/packages/agent-manager/src/harnesses/kiro/KiroSessionParser.ts index 53f0b8118..db76baca7 100644 --- a/packages/agent-manager/src/harnesses/kiro/KiroSessionParser.ts +++ b/packages/agent-manager/src/harnesses/kiro/KiroSessionParser.ts @@ -132,15 +132,15 @@ export class KiroSessionParser { } /** - * - past the idle threshold → IDLE * - last event is an assistant reply without a tool call → WAITING + * - past the idle threshold → IDLE * - otherwise (user prompt, tool call or tool result) → RUNNING */ determineStatus(session: KiroSession): AgentStatus { - if (isIdle(session.lastActive)) return AgentStatus.IDLE; if (session.lastEventKind === "AssistantMessage" && !session.lastAssistantHasToolUse) { return AgentStatus.WAITING; } + if (isIdle(session.lastActive)) return AgentStatus.IDLE; return AgentStatus.RUNNING; } diff --git a/packages/agent-manager/src/harnesses/opencode/OpenCodeAgentMapper.ts b/packages/agent-manager/src/harnesses/opencode/OpenCodeAgentMapper.ts index 53c0acee3..e39c00ba7 100644 --- a/packages/agent-manager/src/harnesses/opencode/OpenCodeAgentMapper.ts +++ b/packages/agent-manager/src/harnesses/opencode/OpenCodeAgentMapper.ts @@ -40,12 +40,10 @@ export class OpenCodeAgentMapper { } private determineStatus(stats: OpenCodeSessionStats, lastActive: Date): AgentStatus { - if (isIdle(lastActive)) return AgentStatus.IDLE; - - if (stats.lastRole === "assistant" && !stats.lastAssistantCompleted) { - return AgentStatus.RUNNING; + if (stats.lastRole === "assistant" && stats.lastAssistantCompleted) { + return AgentStatus.WAITING; } - if (stats.lastRole === "assistant") return AgentStatus.WAITING; + if (isIdle(lastActive)) return AgentStatus.IDLE; return AgentStatus.RUNNING; } } diff --git a/packages/agent-manager/src/harnesses/pi/PiSessionParser.ts b/packages/agent-manager/src/harnesses/pi/PiSessionParser.ts index 65ab02d9f..c058bcfb7 100644 --- a/packages/agent-manager/src/harnesses/pi/PiSessionParser.ts +++ b/packages/agent-manager/src/harnesses/pi/PiSessionParser.ts @@ -174,8 +174,8 @@ export class PiSessionParser { } determineStatus(session: PiSession): AgentStatus { - if (isIdle(session.lastActive)) return AgentStatus.IDLE; if (session.lastRole === "assistant") return AgentStatus.WAITING; + if (isIdle(session.lastActive)) return AgentStatus.IDLE; return AgentStatus.RUNNING; } diff --git a/rust/crates/devkit-harness/src/antigravity/parser.rs b/rust/crates/devkit-harness/src/antigravity/parser.rs index 3b649beb2..d8bc1d557 100644 --- a/rust/crates/devkit-harness/src/antigravity/parser.rs +++ b/rust/crates/devkit-harness/src/antigravity/parser.rs @@ -54,13 +54,13 @@ pub fn read_session(conversation_id: &str, transcript_path: &str, cwd: &str) -> }) } -/// `determineStatus`: past the idle mark → idle; last turn assistant → -/// waiting; else running. +/// `determineStatus`: last turn assistant → waiting; past the idle mark → +/// idle; else running. pub fn status(session: &Session, now_ms: i64) -> &'static str { - if shared::is_idle(session.last_active_ms, now_ms) { - "idle" - } else if session.last_role == Some("assistant") { + if session.last_role == Some("assistant") { "waiting" + } else if shared::is_idle(session.last_active_ms, now_ms) { + "idle" } else { "running" } diff --git a/rust/crates/devkit-harness/src/codex/parser.rs b/rust/crates/devkit-harness/src/codex/parser.rs index 4be5d56af..57b7985b3 100644 --- a/rust/crates/devkit-harness/src/codex/parser.rs +++ b/rust/crates/devkit-harness/src/codex/parser.rs @@ -251,11 +251,9 @@ fn to_session(state: SummaryState, file_mtime_ms: impl FnOnce() -> i64) -> Optio /// `determineStatus` pub fn determine_status(session: &Session, now_ms: i64) -> &'static str { - if shared::is_idle(session.last_active_ms, now_ms) { - return "idle"; - } match session.last_payload_type.as_deref() { Some("agent_message" | "task_complete" | "turn_aborted") => "waiting", + _ if shared::is_idle(session.last_active_ms, now_ms) => "idle", _ => "running", } } @@ -321,12 +319,25 @@ mod tests { fn idle_after_five_minutes() { let p = write_tmp(&[ &meta_line("s3", "/proj", "2026-10-08T10:00:00Z"), - r#"{"timestamp":"2026-10-08T10:01:00Z","type":"event_msg","payload":{"type":"task_complete","message":"bye"}}"#, + r#"{"timestamp":"2026-10-08T10:01:00Z","type":"event_msg","payload":{"type":"user_message","message":"go"}}"#, ]); let s = read_session(&p).unwrap(); assert_eq!(determine_status(&s, s.last_active_ms + 6 * 60_000), "idle"); } + #[test] + fn waiting_survives_idle_threshold() { + let p = write_tmp(&[ + &meta_line("s3", "/proj", "2026-10-08T10:00:00Z"), + r#"{"timestamp":"2026-10-08T10:01:00Z","type":"event_msg","payload":{"type":"task_complete","message":"bye"}}"#, + ]); + let s = read_session(&p).unwrap(); + assert_eq!( + determine_status(&s, s.last_active_ms + 6 * 60_000), + "waiting" + ); + } + #[test] fn legacy_message_field_wins_summary() { let p = write_tmp(&[ diff --git a/rust/crates/devkit-harness/src/copilot/parser.rs b/rust/crates/devkit-harness/src/copilot/parser.rs index c93ecc264..fe015bba0 100644 --- a/rust/crates/devkit-harness/src/copilot/parser.rs +++ b/rust/crates/devkit-harness/src/copilot/parser.rs @@ -279,11 +279,8 @@ pub fn read_session_dir( }) } -/// `determineStatus` — idle(5min) > waiting events > running. +/// `determineStatus` — waiting events > idle(5min) > running. pub fn determine_status(session: &Session, now_ms: i64) -> &'static str { - if shared::is_idle(session.last_active_ms, now_ms) { - return "idle"; - } if session.last_event_type.as_deref().is_some_and(|t| { matches!( t, @@ -292,6 +289,9 @@ pub fn determine_status(session: &Session, now_ms: i64) -> &'static str { }) { return "waiting"; } + if shared::is_idle(session.last_active_ms, now_ms) { + return "idle"; + } "running" } @@ -331,7 +331,7 @@ mod tests { assert_eq!(s.summary, "hello"); assert_eq!(s.last_event_type.as_deref(), Some("assistant.message")); assert_eq!(determine_status(&s, s.last_active_ms + 60_000), "waiting"); - assert_eq!(determine_status(&s, s.last_active_ms + 300_001), "idle"); + assert_eq!(determine_status(&s, s.last_active_ms + 300_001), "waiting"); } #[test] diff --git a/rust/crates/devkit-harness/src/devin/mod.rs b/rust/crates/devkit-harness/src/devin/mod.rs index c91cebbdc..a7a287621 100644 --- a/rust/crates/devkit-harness/src/devin/mod.rs +++ b/rust/crates/devkit-harness/src/devin/mod.rs @@ -261,18 +261,17 @@ fn is_acp_backend_child(proc: &AgentProc, process_by_pid: &HashMap lastRole==="assistant" (waiting) > +/// `determineStatus` — lastRole==="assistant" (waiting) > idle(5min) > /// running. fn determine_status( stats: &parser::SessionStats, last_active_ms: i64, now_ms: i64, ) -> &'static str { - if shared::is_idle(last_active_ms, now_ms) { - return "idle"; - } if stats.last_role.as_deref() == Some("assistant") { "waiting" + } else if shared::is_idle(last_active_ms, now_ms) { + "idle" } else { "running" } diff --git a/rust/crates/devkit-harness/src/gemini/parser.rs b/rust/crates/devkit-harness/src/gemini/parser.rs index f61d0a9d2..516029580 100644 --- a/rust/crates/devkit-harness/src/gemini/parser.rs +++ b/rust/crates/devkit-harness/src/gemini/parser.rs @@ -80,13 +80,11 @@ fn build_session( .into() } -/// `determineStatus` — idle(5min) > waiting(last turn assistant-side) > running. +/// `determineStatus` — waiting(last turn assistant-side) > idle(5min) > running. pub fn determine_status(session: &GeminiSession, now_ms: i64) -> &'static str { - if shared::is_idle(session.last_active_ms, now_ms) { - return "idle"; - } match session.last_message_type.as_deref() { Some("gemini") | Some("assistant") => "waiting", + _ if shared::is_idle(session.last_active_ms, now_ms) => "idle", _ => "running", } } diff --git a/rust/crates/devkit-harness/src/grok/parser.rs b/rust/crates/devkit-harness/src/grok/parser.rs index 69bdbbf8d..4878f82a1 100644 --- a/rust/crates/devkit-harness/src/grok/parser.rs +++ b/rust/crates/devkit-harness/src/grok/parser.rs @@ -110,14 +110,14 @@ pub fn read_session(session_dir: &Path, default_cwd: &str) -> Option { }) } -/// `determineStatus` — idle(5min) > last turn assistant (waiting) > running. +/// `determineStatus` — last turn assistant (waiting) > idle(5min) > running. pub fn determine_status(session: &Session, now_ms: i64) -> &'static str { - if shared::is_idle(session.last_active_ms, now_ms) { - return "idle"; - } if session.last_role.as_deref() == Some("assistant") { return "waiting"; } + if shared::is_idle(session.last_active_ms, now_ms) { + return "idle"; + } "running" } diff --git a/rust/crates/devkit-harness/src/kiro/parser.rs b/rust/crates/devkit-harness/src/kiro/parser.rs index 8a1b20fd3..c32adead3 100644 --- a/rust/crates/devkit-harness/src/kiro/parser.rs +++ b/rust/crates/devkit-harness/src/kiro/parser.rs @@ -93,15 +93,15 @@ pub fn read_session( }) } -/// `determineStatus`: past the 5-min idle mark → idle; else an assistant -/// reply without a tool call → waiting; everything else → running. +/// `determineStatus`: an assistant reply without a tool call → waiting; +/// else past the 5-min idle mark → idle; everything else → running. pub fn status(session: &KiroSession, now_ms: i64) -> &'static str { - if shared::is_idle(session.last_active_ms, now_ms) { - "idle" - } else if session.last_event_kind.as_deref() == Some("AssistantMessage") + if session.last_event_kind.as_deref() == Some("AssistantMessage") && !session.last_assistant_has_tool_use { "waiting" + } else if shared::is_idle(session.last_active_ms, now_ms) { + "idle" } else { "running" } diff --git a/rust/crates/devkit-harness/src/opencode/mod.rs b/rust/crates/devkit-harness/src/opencode/mod.rs index 248afc06b..3d1800951 100644 --- a/rust/crates/devkit-harness/src/opencode/mod.rs +++ b/rust/crates/devkit-harness/src/opencode/mod.rs @@ -141,20 +141,19 @@ impl HarnessAdapter for OpenCodeAdapter { } } -/// `determineStatus` — idle(5min) short-circuits; last assistant role: -/// not-completed → running, completed → waiting; anything else running. +/// `determineStatus` — a completed last assistant message → waiting; +/// otherwise idle(5min) → idle, else running. /// (TS's `lastAssistantErrored` flag is likewise unused there.) fn determine_status( stats: &parser::SessionStats, last_active_ms: i64, now_ms: i64, ) -> &'static str { - if shared::is_idle(last_active_ms, now_ms) { - return "idle"; - } - match stats.last_role.as_deref() { - Some("assistant") if !stats.last_assistant_completed => "running", - Some("assistant") => "waiting", - _ => "running", + if stats.last_role.as_deref() == Some("assistant") && stats.last_assistant_completed { + "waiting" + } else if shared::is_idle(last_active_ms, now_ms) { + "idle" + } else { + "running" } } diff --git a/rust/crates/devkit-harness/src/pi/parser.rs b/rust/crates/devkit-harness/src/pi/parser.rs index a36e4e9a3..b0cad0edd 100644 --- a/rust/crates/devkit-harness/src/pi/parser.rs +++ b/rust/crates/devkit-harness/src/pi/parser.rs @@ -370,13 +370,11 @@ fn apply_head_line(head: &mut SessionHead, line: &[u8]) { } } -/// `determineStatus` — idle > assistant→waiting > running. +/// `determineStatus` — assistant→waiting > idle > running. pub fn determine_status(session: &Session, now_ms: i64) -> &'static str { - if shared::is_idle(session.last_active_ms, now_ms) { - return "idle"; - } match session.last_role.as_deref() { Some("assistant") => "waiting", + _ if shared::is_idle(session.last_active_ms, now_ms) => "idle", _ => "running", } }