/** * daemon/ask.ts — put a question to a project's session and wait for its answer. * * Sibling to `dispatch`, for a caller with no session and no mailbox: a plain * CLI poller run by launchd that needs to know whether the session it handed * work to is still alive. * * NEVER spawns. A probe that creates the thing it is probing for turns a dead * session into a fresh one and reports health. * * ── Why there is a fourth outcome ─────────────────────────────────────────── * * The requested contract had three: replied / not running / no reply in time. * Implementing it revealed those cannot be told apart from the case that * matters most. Claude Code QUEUES typed input while it is mid-turn and only * reads it when the current turn ends, which can be many minutes. So a healthy * session busy doing exactly the work it was given produces the same silence as * a wedged one, and a short timeout reports it as "no reply". * * That is not an edge case for this caller — the scheduler probes at expected * duration x 1.5, i.e. precisely when the session is most likely still working. * Every probe of a slow-but-fine task would read as stuck. * * So `ask` checks liveness BEFORE injecting anything: a session whose screen is * changing is working, which is the answer the caller wanted, and it is * returned without sending a question at all. That also removes the probe's * token cost in the common case, which is the pressure PAI asked the contract * to apply. * * replied — it answered; `reply` holds its words * busy — mid-turn and progressing. ALIVE. Nothing was sent. * silent — idle, took the question, never answered. Genuinely suspicious. * absent — no live session for that project * * `busy` must NOT count toward a stuck threshold; it is positive evidence of * life. `silent` is the one that should. */ import { type PaiProject } from "./pai-projects.js"; import { type LiveSession } from "./dispatch.js"; import { type TerminalIO } from "./terminal-screen.js"; export type AskState = "replied" | "busy" | "silent" | "absent"; export interface AskResult { /** True only when the session actually answered. */ replied: boolean; /** Resolved session label, or "" when none is running. */ session: string; /** The session's answer. Present only when replied. */ reply?: string; /** Why not, when it did not answer. */ reason?: string; /** Machine-readable outcome; additive to the boolean above. */ state: AskState; } export interface AskOptions { /** Total budget for the whole probe. */ timeoutMs?: number; } export interface AskDeps { resolve: (name: string) => Promise; sessions: () => LiveSession[]; io: TerminalIO; } /** Two samples this far apart decide working-vs-idle. */ export declare const LIVENESS_SAMPLE_MS = 1500; export declare const realAskDeps: AskDeps; /** * Extract the session's answer: everything added below the echoed question and * above the input box. * * Anchored on the LAST line still matching the question, because Claude echoes * the question into the transcript and a long one wraps over several lines — * without that, the session's own echo reads back as its reply. */ export declare function extractReply(frame: string, question: string): string; export declare function ask(projectName: string, question: string, opts?: AskOptions, deps?: AskDeps): Promise; //# sourceMappingURL=ask.d.ts.map