Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fixtures/harness/antigravity_cli/matched.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
{
"name": "idle-3003",
"type": "antigravity_cli",
"status": "idle",
"status": "waiting",
"summary": "Antigravity CLI session active",
"pid": 3003,
"projectPath": "/proj/idle",
Expand Down
2 changes: 1 addition & 1 deletion fixtures/harness/codex/statuses.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
{
"name": "st-idle-4005",
"type": "codex",
"status": "idle",
"status": "waiting",
"summary": "old reply",
"pid": 4005,
"projectPath": "/proj/st-idle",
Expand Down
2 changes: 1 addition & 1 deletion fixtures/harness/grok_cli/fallback.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
{
"name": "idle-81004",
"type": "grok_cli",
"status": "idle",
"status": "waiting",
"summary": "old prompt",
"pid": 81004,
"projectPath": "/proj/idle",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("DevinAgentMapper", () => {
const agent = mapper.mapSessionToAgent(
session(),
{
lastRole: "assistant",
lastRole: "user",
lastTimeUpdated: Date.now() - 10 * 60 * 1000,
summary: "",
},
Expand All @@ -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" }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe("OpenCodeAgentMapper", () => {
{
lastRole: "assistant",
lastTimeUpdated: staleTime,
lastAssistantCompleted: true,
lastAssistantCompleted: false,
lastAssistantErrored: false,
summary: "",
},
Expand All @@ -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" });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" ||
Expand All @@ -200,6 +196,10 @@ export class CodexSessionParser {
return AgentStatus.WAITING;
}

if (isIdle(session.lastActive)) {
return AgentStatus.IDLE;
}

return AgentStatus.RUNNING;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion packages/agent-manager/src/harnesses/pi/PiSessionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
10 changes: 5 additions & 5 deletions rust/crates/devkit-harness/src/antigravity/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
19 changes: 15 additions & 4 deletions rust/crates/devkit-harness/src/codex/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
}
Expand Down Expand Up @@ -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(&[
Expand Down
10 changes: 5 additions & 5 deletions rust/crates/devkit-harness/src/copilot/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
}

Expand Down Expand Up @@ -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]
Expand Down
7 changes: 3 additions & 4 deletions rust/crates/devkit-harness/src/devin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,18 +261,17 @@ fn is_acp_backend_child(proc: &AgentProc, process_by_pid: &HashMap<i64, &AgentPr
process_by_pid.contains_key(&ppid)
}

/// `determineStatus` — idle(5min) > 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"
}
Expand Down
Loading
Loading