import { log } from "../../shared" import { MIN_IDLE_TIME_MS } from "./constants" import type { BackgroundTask } from "./types" function getString(obj: Record, key: string): string | undefined { const value = obj[key] return typeof value === "string" ? value : undefined } export function handleSessionIdleBackgroundEvent(args: { properties: Record findBySession: (sessionID: string) => BackgroundTask | undefined idleDeferralTimers: Map> recentlyCompactedSessions?: Set onPostCompactionIdle?: (task: BackgroundTask, sessionID: string) => void validateSessionHasOutput: (sessionID: string) => Promise checkSessionTodos: (sessionID: string) => Promise tryCompleteTask: (task: BackgroundTask, source: string) => Promise emitIdleEvent: (sessionID: string) => void }): void { const { properties, findBySession, idleDeferralTimers, recentlyCompactedSessions, onPostCompactionIdle, validateSessionHasOutput, checkSessionTodos, tryCompleteTask, emitIdleEvent, } = args const sessionID = getString(properties, "sessionID") if (!sessionID) return const task = findBySession(sessionID) if (!task || task.status !== "running") return if (recentlyCompactedSessions?.has(sessionID)) { recentlyCompactedSessions.delete(sessionID) log("[background-agent] Skipping post-compaction session.idle:", { taskId: task.id, sessionID }) onPostCompactionIdle?.(task, sessionID) return } const startedAt = task.startedAt if (!startedAt) return const elapsedMs = Date.now() - startedAt.getTime() if (elapsedMs < MIN_IDLE_TIME_MS) { const remainingMs = MIN_IDLE_TIME_MS - elapsedMs if (!idleDeferralTimers.has(task.id)) { log("[background-agent] Deferring early session.idle:", { elapsedMs, remainingMs, taskId: task.id, }) const timer = setTimeout(() => { idleDeferralTimers.delete(task.id) emitIdleEvent(sessionID) }, remainingMs) idleDeferralTimers.set(task.id, timer) } else { log("[background-agent] session.idle already deferred:", { elapsedMs, taskId: task.id }) } return } // Refresh lastUpdate to prevent stale timeout from racing with this async validation. // Without this, checkAndInterruptStaleTasks can kill the task synchronously // while validateSessionHasOutput is still awaiting an API response. if (task.progress) { task.progress.lastUpdate = new Date() } validateSessionHasOutput(sessionID) .then(async (hasValidOutput) => { if (task.status !== "running") { log("[background-agent] Task status changed during validation, skipping:", { taskId: task.id, status: task.status, }) return } if (!hasValidOutput) { log("[background-agent] Session.idle but no valid output yet, waiting:", task.id) return } const hasIncompleteTodos = await checkSessionTodos(sessionID) if (task.status !== "running") { log("[background-agent] Task status changed during todo check, skipping:", { taskId: task.id, status: task.status, }) return } if (hasIncompleteTodos) { log("[background-agent] Task has incomplete todos, waiting for todo-continuation:", task.id) return } await tryCompleteTask(task, "session.idle event") }) .catch((err) => { log("[background-agent] Error in session.idle handler:", err) }) }