/**
 * Session-scoped streaming layer for `spectral serve`.
 *
 * Background: prior to this module each WebSocket owned its own `AgentBridge`
 * instance and the routes layer enforced single-writer-wins (4001 eviction)
 * to keep that bridge unique per session. That model lost data on browser
 * refresh — the WS close torn down the spectral process mid-stream, and a re-open
 * couldn't recover what hadn't yet hit `agent_end` (and thus SQLite).
 *
 * New model:
 *   - Pi lifecycle is per **Spectral session**, not per WS.
 *   - 0..N WebSockets may attach to the same session simultaneously. Each
 *     gets the same broadcast stream of events.
 *   - When a WS detaches (close, error, refresh), the spectral process keeps
 *     running. Closing every tab does NOT cancel the in-flight turn —
 *     it runs to completion and persists on `agent_end` as before.
 *   - On `attach`, the manager hands back a replay payload: full DB history
 *     plus a snapshot of the currently in-flight turn (if any). The client
 *     replays the snapshot through the same reducer it uses for live events
 *     and continues streaming naturally.
 *   - Persistence shape is unchanged: only the final assistant message is
 *     written to SQLite on `agent_end`. In-flight events live in memory only.
 *     A server crash mid-turn discards the in-flight state — acceptable
 *     for MVP.
 *
 * Failure modes:
 *   - spectral throws synchronously in `prompt()` → bridge surfaces as `error`
 *     event; manager broadcasts and clears `currentTurn`.
 *   - One subscriber's `ws.send` throws → caught, logged, removed from the
 *     subscriber set; broadcast continues to the rest.
 *   - `agent_end` arrives without a current turn (defensive) → broadcast
 *     anyway so any late attachers don't get stuck.
 *
 * TODO (future): idle GC. A `SessionStream` with `subscribers.size === 0`
 *   and no current turn could be disposed after some grace window (e.g. 5
 *   minutes) to release spectral resources for chronically-idle sessions. Skipped
 *   for now — streams accumulate for the lifetime of the server process.
 */
import { type AgentBridgeOptions } from "./agent-bridge.js";
import type { SessionStore } from "./storage.js";
import type { ImageAttachment, InProgressTurnSnapshot, ServerEvent, WireMessage, WireSessionMemoryDetails } from "./wire.js";
/**
 * Anything we can `send` and `close` on. The `ws` library's WebSocket fits;
 * tests can pass a stub. Kept minimal so we don't drag the `ws` types in.
 */
export interface Subscriber {
    /** Send a JSON-serialized event. Implementations should be best-effort. */
    send(event: ServerEvent): void;
    /** True when the subscriber is no longer reachable (closed/closing). */
    isOpen(): boolean;
}
/**
 * Bridge-shaped object — `AgentBridge` is the only real impl; tests duck-type
 * via this interface. Identical contract to `BridgeLike` in `routes.ts` but
 * defined here too so `session-stream.ts` doesn't depend on `routes.ts`.
 */
export interface BridgeLike {
    start(): Promise<void>;
    prompt(text: string, images?: ImageAttachment[]): Promise<void>;
    dispose(): void;
    /**
     * Manually compact the session context via spectral's built-in compaction.
     * Fires compaction_start / compaction_end events through the bridge's
     * emit callback. Used by the Fork & Compact flow after the first
     * assistant turn of a forked session completes.
     */
    compact?(customInstructions?: string): Promise<void>;
    /**
     * Optional sticky reasoning effort. Maps frontend effort strings
     * (xhigh|high|medium|low|minimal|none) to spectral ThinkingLevel.
     */
    setReasoningEffort?(effort: string | undefined): void;
    /**
     * Optional sticky model selection. Phase 3 (Available Models whitelist).
     * Real AgentBridge implements this; test stubs may omit it (the manager
     * skips the call when undefined, which is exactly what tests want).
     * Returns true when the model is in effect; false when the bridge
     * surfaced an error (the manager will then drop the prompt).
     */
    setModel?(modelId: string | null | undefined): Promise<boolean>;
    /**
     * Return the modelId of the first available model from the backend
     * whitelist (preserving backend sortOrder). Used as defense-in-depth
     * default when neither the envelope nor SQLite supply a modelId.
     * Returns `undefined` when no models are available.
     */
    getFirstAvailableModelId?(): string | undefined;
    /**
     * Return current session context usage from spectral's built-in estimator.
     * Returns `undefined` when the bridge hasn't started yet or the model
     * has no context window configured.
     */
    getContextUsage?(): {
        tokens: number | null;
        contextWindow: number;
        percent: number | null;
    } | undefined;
    getSessionBranch?(): Array<{
        type: string;
        id: string;
        timestamp?: string;
        message?: unknown;
        content?: unknown;
        customType?: string;
        summary?: unknown;
        fromId?: string;
        data?: unknown;
        details?: unknown;
        firstKeptEntryId?: string;
    }>;
    getMemoryActivity?(): {
        phase: "idle" | "observing" | "compacting" | "reflecting" | "pruning";
        inFlight: {
            observer: boolean;
            compaction: boolean;
            reflection: boolean;
            pruner: boolean;
        };
    };
}
/** Factory signature shared with `routes.ts`. */
export type BridgeFactory = (args: AgentBridgeOptions) => BridgeLike;
export interface SessionMemoryStatus {
    mode: "active" | "passive";
    phase: "idle" | "observing" | "compacting" | "reflecting" | "pruning";
    inFlight: {
        observer: boolean;
        compaction: boolean;
        reflection: boolean;
        pruner: boolean;
    };
    reflections: {
        count: number;
        tokens: number;
    };
    observations: {
        committedCount: number;
        committedTokens: number;
        pendingCount: number;
        pendingTokens: number;
    };
    thresholds: {
        observation: number;
        compaction: number;
        reflection: number;
    };
    progress: {
        sinceObservationBoundTokens: number;
        sinceLastCompactionTokens: number;
        observationPoolTokens: number;
    };
}
export interface AttachResult {
    history: WireMessage[];
    currentTurn: InProgressTurnSnapshot | null;
    /** Resolves when the underlying spectral session is ready (or rejects on start failure). */
    ready: Promise<void>;
    /** True when this session was created via "Fork & Compact" and has not yet
     *  triggered its first compaction. The dispatcher includes this in
     *  `session_ready` so the frontend can adapt the send button. */
    forkCompactPending: boolean;
    /** Cumulative context tokens used across the session (null initially, updated on first token_usage). */
    contextWindowUsed: number | null;
    /** Model's total context window in tokens (null if model metadata not yet available). */
    contextWindowMax: number | null;
}
export interface SessionStreamManagerOptions {
    store: SessionStore;
    /**
     * Fallback cwd used ONLY when a session has no resolvable project (which
     * shouldn't happen in normal operation since FK enforces it). Sessions
     * with a valid project always run spectral against `project.path`.
     */
    cwd: string;
    /**
     * Backend base URL — threaded into every AgentBridge so spectral proxies all
     * inference through `${backendUrl}/v1` instead of reading
     * `~/.spectral/agent/auth.json`. Required in production; tests that supply a
     * `bridgeFactory` may pass any non-empty placeholder.
     */
    backendUrl: string;
    /**
     * Machine JWT — used as the Bearer credential spectral sends to the backend
     * proxy on every inference call. See `backendUrl` for context.
     */
    machineJwt: string;
    bridgeFactory?: BridgeFactory;
    agentDir?: string;
}
export declare class SessionStreamManager {
    private readonly store;
    private readonly cwd;
    private readonly backendUrl;
    private readonly machineJwt;
    private readonly bridgeFactory;
    private readonly agentDir;
    private readonly streams;
    private disposed;
    constructor(opts: SessionStreamManagerOptions);
    /**
     * Attach a subscriber to a session. Lazily creates the underlying spectral
     * session on first attach. The caller is responsible for sending the
     * initial `session_ready` frame using the returned replay payload (this
     * keeps wire-protocol concerns in the routes layer).
     *
     * Throws if the session id is unknown in SQLite (caller should turn this
     * into a wire-level error frame + close).
     */
    attach(sessionId: string, subscriber: Subscriber): AttachResult;
    /**
     * Detach a subscriber. Idempotent. Does NOT dispose the underlying spectral
     * session — even when subscribers reach zero, the in-flight turn must
     * complete and persist.
     */
    detach(sessionId: string, subscriber: Subscriber): void;
    /** True if the session has an in-flight turn (manager-side; not WS-side). */
    hasActiveTurn(sessionId: string): boolean;
    /**
     * Return the set of session IDs that currently have an in-flight turn.
     * Cheap O(streams) scan used by the list-project / list-sessions endpoints
     * to enrich responses with `hasActiveTurn` markers so the frontend can
     * render a yellow pulsating dot next to active sessions/projects.
     */
    getActiveTurnSessionIds(): Set<string>;
    getSessionMemoryStatus(sessionId: string): SessionMemoryStatus;
    getSessionMemoryDetails(sessionId: string): WireSessionMemoryDetails;
    compactSession(sessionId: string): Promise<void>;
    /**
     * Persist a user message and forward it to spectral. Resolves after the user
     * message is persisted + spectral is invoked (NOT after the turn completes —
     * the turn lifetime is observed via the broadcast stream).
     *
     * Broadcast ordering:
     *   1. user message persisted to SQLite
     *   2. `user_message_appended` broadcast to all subscribers (including
     *      the originating tab)
     *   3. new `currentTurn` opened
     *   4. `bridge.prompt()` invoked (events arrive asynchronously and are
     *      buffered + broadcast as they come)
     *
     * Sticky model selection (Phase 3 — Available Models whitelist):
     *   - When `modelId` is provided, we apply it via `bridge.setModel()` and
     *     persist to SQLite for cross-restart recovery, BEFORE invoking
     *     `bridge.prompt()`. If `setModel` fails (unknown model, registry
     *     unavailable, agent-side error) the bridge has already emitted an
     *     `error` wire event and we drop the prompt to avoid running it
     *     against the wrong model.
     *   - When `modelId` is omitted, we look up SQLite. If a previous turn
     *     persisted a value, we reapply it on this turn (this is the
     *     cross-restart recovery path: a fresh server process has lost spectral's
     *     in-memory model state, so we re-pin from durable storage).
     *   - When neither envelope nor SQLite have a value, we leave model
     *     selection to spectral's own settings file (pre-Phase-3 behaviour).
     */
    prompt(sessionId: string, content: string, modelId?: string, images?: ImageAttachment[], reasoningEffort?: string, opts?: {
        skipPersistence?: boolean;
    }): Promise<void>;
    /**
     * Tear down everything. Best-effort: disposes every bridge, drops all
     * subscribers. After this the manager is unusable.
     */
    dispose(): void;
    /** Test/inspection helper: how many streams are currently tracked. */
    streamCount(): number;
    /**
     * Count of sessions with an in-flight turn (i.e. a `currentTurn` set).
     * Used by `gracefulShutdown` to decide whether to keep waiting before
     * tearing down — a non-zero count means at least one assistant response
     * is mid-stream and we'd rather let it finish (within the grace window)
     * than orphan a half-streamed message in the UI.
     *
     * Cheap O(streams) scan; we only call it ~50× during a 5 s graceful
     * shutdown so the linear walk is fine.
     */
    activeTurnCount(): number;
    /**
     * Cancel the in-flight turn for a session (user pressed Stop in the UI).
     * Disposes the agent bridge and broadcasts `agent_end` so all subscribers
     * see the turn close. The stream itself is kept alive — the next user
     * message (via `prompt()`) will lazily create a fresh bridge.
     *
     * Idempotent: if no stream exists for the session, or no turn is in
     * flight, this is a no-op.
     */
    cancelTurn(sessionId: string): void;
    /**
     * Tear down a single session's stream — disposes the agent bridge and clears
     * subscribers. Idempotent. Called by the routes layer right before
     * `DELETE /api/sessions/:id` so the SQL cascade doesn't leave a zombie
     * spectral process driving events at a session that no longer exists.
     *
     * Does NOT remove the session from the store — that's the caller's job.
     */
    disposeSessionStream(sessionId: string): void;
    /**
     * Tear down every stream whose session belongs to the given list of ids.
     * Used by the project-delete path: the route layer reads the project's
     * session ids from `deleteProject()` and passes them here BEFORE the SQL
     * cascade fires, so no spectral process ever observes the FK cascade.
     */
    disposeProjectStreams(sessionIds: readonly string[]): void;
    /**
     * Set the autonomous iterative loop state for a session.
     *
     * When `active` is true, the manager replays `originalPrompt` after each
     * `agent_end` event — the agent sees its own file changes from prior
     * iterations and iteratively improves its solution (Ralph Wiggum pattern).
     * The loop stops when the agent emits `<LOOP_DONE>` in its response or the
     * safety iteration limit is reached.
     */
    setLoopActive(sessionId: string, active: boolean, originalPrompt?: string, maxIterations?: number, goal?: string): void;
    /**
     * Fork & Compact: trigger compaction after the first assistant turn of a
     * forked session. Uses spectral's built-in `compact()` which generates a summary
     * of older context, retaining the most recent ~20K tokens (including the
     * user's new message + the assistant's response).
     *
     * Custom instructions reference the most recent user message so the LLM
     * summary prioritizes information relevant to the current task.
     */
    private triggerForkCompact;
    /**
     * Send the next prompt for an autonomous loop iteration, waiting for any
     * in-flight observational-memory compaction to finish first so the LLM
     * sees the compacted context.
     *
     * The compaction-trigger extension fires on agent_end (via setTimeout),
     * so we poll stream.compacting briefly. If compaction hasn't started after
     * a short grace period, we check whether the context window exceeds the
     * threshold and proactively trigger compaction via the bridge (which
     * invokes spectral's full pipeline, including the session_before_compact hook
     * where the observational memory extension provides its summary).
     *
     * A duplicate call from the extension's delayed compaction trigger is
     * harmless — spectral throws "Already compacted" which the extension catches.
     */
    /**
     * Send the next prompt for an autonomous loop iteration, compacting first
     * if the context window exceeds the threshold.
     *
     * Instead of polling for the extension's delayed compaction-trigger (which
     * fires via setTimeout), we proactively call bridge.compact() directly.
     * bridge.compact() → session.compact() → fires session_before_compact
     * (where the extension's compaction-hook provides the observational-memory
     * summary), then appends the compaction entry, reloads the compacted
     * context into agent.state.messages, and emits compaction_start/end.
     *
     * The extension's trigger still fires (via setTimeout), but by the time
     * its callback runs, bridge.compact() has already appended the compaction
     * entry → prepareCompaction() returns undefined → "Already compacted"
     * → the trigger's onError handler catches it harmlessly.
     */
    private buildLoopPrompt;
    private sendNextLoopIteration;
    private createStream;
    private handleBridgeEvent;
    /**
     * Flush the current in-flight turn's events to SQLite for crash recovery.
     * Only the events accumulated since the last flush are written — we append
     * them to the already-stored JSONL via INSERT OR REPLACE. Called every
     * `BATCH_FLUSH_INTERVAL` events from `handleBridgeEvent`.
     *
     * Errors are caught, logged, and swallowed: batch persistence is a
     * best-effort hardening, never a failure path that should block the stream.
     */
    private flushInFlightTurn;
    private broadcast;
    /**
     * Push the current prompt queue state to all subscribers of a session.
     * Called after every queue mutation (enqueue, remove, clear, dequeue).
     * Safe to call when no stream exists or no subscribers — silently no-ops.
     */
    pushQueueState(sessionId: string): void;
    /**
     * Auto-dequeue the next prompt from the persistent queue and start a
     * new turn. Called from the `agent_end` handler when no loop is active.
     * Returns true if a prompt was dequeued and a turn started.
     */
    private maybeAutoDequeue;
}
//# sourceMappingURL=session-stream.d.ts.map