/** * In-Flight Turn Tracker * * Remembers, per agent session, the messages that were written into the * agent's PTY (or handed to an in-process runtime) and whether the agent has * finished the turn they started. * * Why this exists: the persistent message queue only protects messages that * have not been delivered yet. Once a message is in the PTY it is off the * queue, so a restart during the agent's turn used to lose the turn with no * trace — the owner DM'd Ella at 01:38:30, a restart at 01:39:31 killed her * mid-turn, and nothing picked it back up (2026-09-24). The drain and the * interrupted-turn resume both read from here. * * Turn completion is decided by a {@link TurnProbe}, not by events alone: * - the ActivityMonitor `agent:idle` event is a *prompt* to re-probe, never a * verdict — it fires on a 30s poll and is force-emitted after 15 minutes of * continuous output even when the agent is still working; * - a reply (report-status / reply-slack / chat) is not a verdict either — an * agent says "on it" and keeps working, which is exactly the Ella case; * - in-process (crewly-agent) turns end precisely when their promise settles, * so the caller reports that directly via {@link completeMessage}. * * @module services/restart/in-flight-turn-tracker */ /** What the probe says about a session right now. */ export type TurnProbeResult = 'busy' | 'idle' | 'gone'; /** Answers whether an agent session is currently mid-turn. `since` is the newest open delivery, when there is one. */ export type TurnProbe = (sessionName: string, context?: { since?: number; }) => TurnProbeResult; /** How the message reached the agent. */ export type DeliveryRuntime = 'pty' | 'in-process'; /** Queue metadata attached by the message-queue processor after delivery. */ export interface DeliveryMeta { /** Message-queue id */ messageId?: string; /** Message source (slack, web_chat, system_event, …) */ source?: string; /** Conversation id used for reply routing */ conversationId?: string; /** Message body before the queue processor added its [CHAT:…] prefix */ originalContent?: string; /** JSON-safe subset of the source metadata (channelId, threadTs, …) */ sourceMetadata?: Record; /** True for scheduler / status pings wrapped in [SYSTEM] markers */ systemEvent?: boolean; } /** One message delivered into an agent's current turn. */ export interface DeliveredMessage extends DeliveryMeta { /** Epoch ms of the write */ deliveredAt: number; /** Exactly what was written to the PTY / runtime */ text: string; /** Single-line preview of `text` */ preview: string; } /** A session whose last delivered message(s) may still be in progress. */ export interface InFlightTurn { /** Agent session name */ sessionName: string; /** How the messages were delivered */ runtime: DeliveryRuntime; /** Epoch ms of the oldest open delivery */ since: number; /** Open deliveries, oldest first (empty for a turn the runtime started itself) */ messages: DeliveredMessage[]; /** 'runtime': no tracked delivery, the runtime reports the agent busy (self-started turn, background work); unset = delivery */ origin?: 'delivery' | 'runtime'; /** A tool call, subagent or background task is still running (the drain waits longer) */ longRunning?: boolean; /** For a runtime turn: the last message delivered to the session (what it is most likely working on) */ lastDelivered?: string; } /** A session the runtime itself reports busy (hooks / transcript). */ export interface RuntimeBusySession { /** Agent session name */ sessionName: string; /** Epoch ms the turn / background work began, when known */ since: number | null; /** A tool call, subagent or background task is open */ longRunning: boolean; } /** Lists the sessions whose runtime reports them busy right now. */ export type RuntimeBusySource = (now: number) => RuntimeBusySession[]; /** Minimal event-bus surface the tracker listens to. */ export interface TurnEventSource { on(event: 'eventPublished', listener: (event: { type?: string; sessionName?: string; }) => void): unknown; off?(event: 'eventPublished', listener: (event: { type?: string; sessionName?: string; }) => void): unknown; removeListener?(event: 'eventPublished', listener: (event: { type?: string; sessionName?: string; }) => void): unknown; } /** * Build a one-line preview of a delivered message. * * @param text - Delivered text * @returns Whitespace-collapsed text, cut to SAFE_RESTART.PREVIEW_CHARS */ export declare function previewOf(text: string): string; /** * Tracks in-flight agent turns. Singleton; all state is in memory. */ export declare class InFlightTurnTracker { private static instance; private readonly logger; private readonly turns; private probe; private runtimeBusySource; /** Last non-[SYSTEM] message delivered per session, kept after its turn settles */ private readonly lastDelivered; private constructor(); /** * Get the singleton instance. * * @returns The tracker */ static getInstance(): InFlightTurnTracker; /** * Reset the singleton (tests only). */ static resetInstance(): void; /** * Install the probe used to decide whether a PTY session is still mid-turn. * * @param probe - Probe function, or null to remove */ setProbe(probe: TurnProbe | null): void; /** * Install the source of runtime-reported busy sessions. Turns the runtime * started on its own (a background subagent finishing) and background * work after a turn are mid-turn too, though no delivery started them * (2026-10-02, Eve). * * @param source - Source, or null to remove */ setRuntimeBusySource(source: RuntimeBusySource | null): void; /** * Record a message that was just written into an agent's PTY or handed to * its in-process runtime. * * Callers should {@link settle} the session *before* writing: if the agent * is resting, whatever was still open belonged to a finished turn and is * dropped, so a restart never resumes a message that was already answered. * (After the write the echo makes the PTY look busy, so settling then * proves nothing.) * * @param sessionName - Agent session * @param text - Exactly what was delivered * @param runtime - How it was delivered * @param now - Current time (ms) * @returns The recorded message (pass it to completeMessage for in-process turns) */ recordDelivery(sessionName: string, text: string, runtime?: DeliveryRuntime, now?: number): DeliveredMessage; /** * Attach queue metadata to the most recent open delivery whose text matches. * Called by the queue processor after a successful delivery so the resume * path can re-enqueue with the original source (Slack thread etc.). * * @param sessionName - Agent session * @param text - The delivered text (as passed to sendMessageToAgent) * @param meta - Queue metadata * @returns True if a matching delivery was found */ annotate(sessionName: string, text: string, meta: DeliveryMeta): boolean; /** * Mark one delivery as finished (in-process runtimes report this exactly). * * @param sessionName - Agent session * @param message - The object returned by recordDelivery */ completeMessage(sessionName: string, message: DeliveredMessage): void; /** * Drop every open delivery for a session (turn over, or session gone). * * @param sessionName - Agent session * @param reason - Why, for the debug log */ markTurnComplete(sessionName: string, reason: string): void; /** * Whether a delivery to the session is still tracked as in flight. Read * only: unlike {@link settle} it never probes and never drops anything * (crewly#1015 follow-up L3). * * @param sessionName - Agent session * @returns True while an open delivery is tracked */ hasOpenTurn(sessionName: string): boolean; /** * Re-check one session with the probe and drop its open deliveries if the * agent is resting or its session is gone. In-process turns are left alone: * only their promise can end them. * * @param sessionName - Agent session * @param now - Current time (ms) * @returns True if the session is still mid-turn afterwards */ settle(sessionName: string, now?: number): boolean; /** * Settle every tracked session and return those still mid-turn. * * @param now - Current time (ms) * @returns Snapshot of in-flight turns, oldest first */ getMidTurn(now?: number): InFlightTurn[]; /** * Snapshot of every open turn without probing. * * @returns Copies of the open turns, oldest first */ snapshot(): InFlightTurn[]; /** * Re-probe a session whenever the ActivityMonitor reports it idle, so a * finished turn is forgotten promptly instead of lingering until the next * delivery. The event only triggers the probe; it never decides alone. * * @param source - Event bus emitting 'eventPublished' * @returns Function that detaches the listener */ attachEventSource(source: TurnEventSource): () => void; } //# sourceMappingURL=in-flight-turn-tracker.service.d.ts.map