import type { BackgroundManager, BackgroundTask } from "../../features/background-agent" import { getMainSessionID, getSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" const HOOK_NAME = "unstable-agent-babysitter" const DEFAULT_TIMEOUT_MS = 120000 const COOLDOWN_MS = 5 * 60 * 1000 const THINKING_SUMMARY_MAX_CHARS = 500 type BabysittingConfig = { timeout_ms?: number } type BabysitterContext = { directory: string client: { session: { messages: (args: { path: { id: string } }) => Promise<{ data?: unknown } | unknown[]> prompt: (args: { path: { id: string } body: { parts: Array<{ type: "text"; text: string }> agent?: string model?: { providerID: string; modelID: string } } query?: { directory?: string } }) => Promise } } } type BabysitterOptions = { backgroundManager: Pick config?: BabysittingConfig } type MessageInfo = { role?: string agent?: string model?: { providerID: string; modelID: string } providerID?: string modelID?: string } type MessagePart = { type?: string text?: string thinking?: string } function hasData(value: unknown): value is { data?: unknown } { return typeof value === "object" && value !== null && "data" in value } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } function getMessageInfo(value: unknown): MessageInfo | undefined { if (!isRecord(value)) return undefined if (!isRecord(value.info)) return undefined const info = value.info const modelValue = isRecord(info.model) ? info.model : undefined const model = modelValue && typeof modelValue.providerID === "string" && typeof modelValue.modelID === "string" ? { providerID: modelValue.providerID, modelID: modelValue.modelID } : undefined return { role: typeof info.role === "string" ? info.role : undefined, agent: typeof info.agent === "string" ? info.agent : undefined, model, providerID: typeof info.providerID === "string" ? info.providerID : undefined, modelID: typeof info.modelID === "string" ? info.modelID : undefined, } } function getMessageParts(value: unknown): MessagePart[] { if (!isRecord(value)) return [] if (!Array.isArray(value.parts)) return [] return value.parts.filter(isRecord).map((part) => ({ type: typeof part.type === "string" ? part.type : undefined, text: typeof part.text === "string" ? part.text : undefined, thinking: typeof part.thinking === "string" ? part.thinking : undefined, })) } function extractMessages(value: unknown): unknown[] { if (Array.isArray(value)) { return value } if (hasData(value) && Array.isArray(value.data)) { return value.data } return [] } function isUnstableTask(task: BackgroundTask): boolean { if (task.isUnstableAgent === true) return true const modelId = task.model?.modelID?.toLowerCase() return modelId ? modelId.includes("gemini") || modelId.includes("minimax") : false } async function resolveMainSessionTarget( ctx: BabysitterContext, sessionID: string ): Promise<{ agent?: string; model?: { providerID: string; modelID: string } }> { let agent = getSessionAgent(sessionID) let model: { providerID: string; modelID: string } | undefined try { const messagesResp = await ctx.client.session.messages({ path: { id: sessionID }, }) const messages = extractMessages(messagesResp) for (let i = messages.length - 1; i >= 0; i--) { const info = getMessageInfo(messages[i]) if (info?.agent || info?.model || (info?.providerID && info?.modelID)) { agent = agent ?? info?.agent model = info?.model ?? (info?.providerID && info?.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined) break } } } catch (error) { log(`[${HOOK_NAME}] Failed to resolve main session agent`, { sessionID, error: String(error) }) } return { agent, model } } async function getThinkingSummary(ctx: BabysitterContext, sessionID: string): Promise { try { const messagesResp = await ctx.client.session.messages({ path: { id: sessionID }, }) const messages = extractMessages(messagesResp) const chunks: string[] = [] for (const message of messages) { const info = getMessageInfo(message) if (info?.role !== "assistant") continue const parts = getMessageParts(message) for (const part of parts) { if (part.type === "thinking" && part.thinking) { chunks.push(part.thinking) } if (part.type === "reasoning" && part.text) { chunks.push(part.text) } } } const combined = chunks.join("\n").trim() if (!combined) return null if (combined.length <= THINKING_SUMMARY_MAX_CHARS) return combined return combined.slice(0, THINKING_SUMMARY_MAX_CHARS) + "..." } catch (error) { log(`[${HOOK_NAME}] Failed to fetch thinking summary`, { sessionID, error: String(error) }) return null } } function buildReminder(task: BackgroundTask, summary: string | null, idleMs: number): string { const idleSeconds = Math.round(idleMs / 1000) const summaryText = summary ?? "(No thinking trace available)" return `Unstable background agent appears idle for ${idleSeconds}s. Task ID: ${task.id} Description: ${task.description} Agent: ${task.agent} Status: ${task.status} Session ID: ${task.sessionID ?? "N/A"} Thinking summary (first ${THINKING_SUMMARY_MAX_CHARS} chars): ${summaryText} Suggested actions: - background_output task_id="${task.id}" full_session=true include_thinking=true include_tool_results=true message_limit=50 - background_cancel taskId="${task.id}" This is a reminder only. No automatic action was taken.` } export function createUnstableAgentBabysitterHook(ctx: BabysitterContext, options: BabysitterOptions) { const reminderCooldowns = new Map() const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => { if (event.type !== "session.idle") return const props = event.properties as Record | undefined const sessionID = props?.sessionID as string | undefined if (!sessionID) return const mainSessionID = getMainSessionID() if (!mainSessionID || sessionID !== mainSessionID) return const tasks = options.backgroundManager.getTasksByParentSession(mainSessionID) if (tasks.length === 0) return const timeoutMs = options.config?.timeout_ms ?? DEFAULT_TIMEOUT_MS const now = Date.now() for (const task of tasks) { if (task.status !== "running") continue if (!isUnstableTask(task)) continue const lastMessageAt = task.progress?.lastMessageAt if (!lastMessageAt) continue const idleMs = now - lastMessageAt.getTime() if (idleMs < timeoutMs) continue const lastReminderAt = reminderCooldowns.get(task.id) if (lastReminderAt && now - lastReminderAt < COOLDOWN_MS) continue const summary = task.sessionID ? await getThinkingSummary(ctx, task.sessionID) : null const reminder = buildReminder(task, summary, idleMs) const { agent, model } = await resolveMainSessionTarget(ctx, mainSessionID) try { await ctx.client.session.prompt({ path: { id: mainSessionID }, body: { ...(agent ? { agent } : {}), ...(model ? { model } : {}), parts: [{ type: "text", text: reminder }], }, query: { directory: ctx.directory }, }) reminderCooldowns.set(task.id, now) log(`[${HOOK_NAME}] Reminder injected`, { taskId: task.id, sessionID: mainSessionID }) } catch (error) { log(`[${HOOK_NAME}] Reminder injection failed`, { taskId: task.id, error: String(error) }) } } } return { event: eventHandler, } }