/**
 * WsBridge — Agent Session Manager
 *
 * Bridges browser ↔ server ↔ Claude CLI via stdout pipe.
 * Each "work" gets a WsSession with CLI process, browser connections,
 * message history. CLI is spawned with `-p <prompt> --output-format stream-json
 * --verbose`. Multi-turn uses `--resume <sessionId> -p <newMessage>`.
 *
 * Browser clients connect to /ws/browser/:workId for live streaming.
 */
import { type ChildProcess } from "node:child_process";
import { WebSocket } from "ws";
import type { IncomingMessage } from "node:http";
import type { Duplex } from "node:stream";
import { type Work } from "./domain/work-store.js";
import { type SessionRecord } from "./server/sessions/sessions-sidecar.js";
/** Media the user attached to a chat message. Parsed out of the <attachments>
 *  envelope when the message is recorded, so the persisted/reloaded user block
 *  carries structured thumbnails instead of the raw envelope XML. */
export interface ChatBlockAttachment {
    path: string;
    url: string;
    name: string;
    kind: string;
}
export interface ChatBlock {
    type: "user" | "text" | "thinking" | "tool_use" | "tool_result" | "locator";
    text: string;
    toolName?: string;
    collapsed?: boolean;
    timestamp?: string;
    source?: "creator" | "evaluator";
    /** Set on user blocks that carried media attachments. */
    attachments?: ChatBlockAttachment[];
    label?: string;
    data?: {
        clipId?: string;
        time?: number;
        assetId?: string;
        trackId?: string;
    };
}
export interface WsSession {
    workId: string;
    /** Our stable session id within the work (ADR-008) — e.g. "s_1". DISTINCT
     *  from cliSessionId (claude's --resume UUID). The default/legacy chat
     *  session is "s_1" and maps to the legacy chat.jsonl on disk. */
    sessionId: string;
    cliSessionId?: string;
    browserSockets: Set<WebSocket>;
    cliProcess?: ChildProcess;
    idle: boolean;
    messageHistory: ChatBlock[];
    model?: string;
    /** B7(a)-lite (PRD-0009) — the PROMPT_VERSION this session's *current spawn*
     *  is actually taught up to. A fresh spawn is built with the current
     *  buildSystemPrompt (= PROMPT_VERSION); a resume that injected the changelog
     *  is taught to PROMPT_VERSION; a resume that SKIPPED injection (sidecar
     *  read/write failed, or the legacy default session had no record) stays at
     *  the version it was actually taught (or undefined when unknown). The
     *  system.init / result writeback stamps THIS, not an unconditional
     *  PROMPT_VERSION — otherwise a skipped injection would still record "taught
     *  to current" and permanently suppress the teaching it never delivered. */
    taughtPromptVersion?: number;
}
/** The id of a work's first/legacy chat session. A work created before
 *  multi-session keying has exactly one chat that maps to chat.jsonl. */
export declare const DEFAULT_CHAT_SESSION_ID = "s_1";
/**
 * Resolve the on-disk chat log path for a (workId, sessionId). The legacy
 * single-session work kept its history in `chat.jsonl`; ADR-008 §4 keeps that
 * filename for the default session `s_1` (no risky bulk rename) and uses
 * `chat-{sessionId}.jsonl` for every new session. Exported for the migration +
 * tests.
 */
export declare function chatLogPath(workId: string, sessionId: string): string;
/**
 * B7(a)-lite (PRD-0009) — incremental teaching for RESUMED sessions.
 *
 * A `--resume`d CLI session replays the system prompt it was created with, so an
 * existing work's agent never sees teaching added after that point (B7 root
 * cause). We don't re-send the whole prompt (that would lose CLI context and
 * bust the prefix cache). Instead, when resuming a work whose last-injected
 * version trails the current PROMPT_VERSION, we `--append-system-prompt` the
 * concatenation of changelog entries strictly newer than the stored version —
 * Anthropic's mid-conversation system channel, the cache-safe way to补教学.
 *
 * INVARIANT: each entry MUST be a *context statement*, not a command. Opus 4.8
 * is trained to resist system-channel override phrasing ("忽略之前的…" /
 * "regardless of…"), so we state the new fact and let the agent act on it —
 * never an imperative. See claude-api skill → prompt-caching.md §Mid-conversation
 * system messages / model-migration.md →Opus 4.8.
 *
 * To ship a knowledge-面 升级 that resumed sessions should learn: bump
 * PROMPT_VERSION and add a `[version]: "<context statement>"` row whose key is
 * the NEW version. Resume injects every row with a key > the session's stored
 * version, in ascending order.
 */
export declare const PROMPT_VERSION = 2;
/**
 * version → one short context statement describing what that version newly
 * teaches. Declarative (context, not commands). The map is the single source of
 * truth for "what changed since version N" — `promptChangelogSince(n)` walks it.
 *
 * NOTE (Wave B review): a changelog entry MAY carry a short factual fragment
 * (e.g. "durationSec 4–15", "7 档 aspectRatio") as a "here's what changed, go
 * check docs" nudge to a RESUMED session. That is intentional and is NOT
 * governed by the single-source-of-truth lock on buildSystemPrompt (the
 * `not.toContain('durationSec')` guard in ws-bridge.test only inspects
 * buildSystemPrompt's output — this constant is independent). The full param
 * tables still live ONLY in the manual; the changelog points the agent there.
 */
export declare const PROMPT_CHANGELOG: Record<number, string>;
/**
 * Concatenate the changelog entries strictly newer than `storedVersion` (in
 * ascending version order) into one `--append-system-prompt` payload. Returns
 * an empty string when the session is already current (caller skips the flag).
 */
export declare function promptChangelogSince(storedVersion: number): string;
/**
 * Pure builder for the agent system prompt. Modules are capabilities, not
 * stages — the agent picks one based on user intent. The async wrapper on
 * the WsBridge appends dynamic shared-asset / memory context, but this
 * function is unit-testable in isolation.
 */
export declare function buildSystemPrompt(work: Pick<Work, "id" | "type" | "platforms">, opts: {
    port: number;
    workspacePath: string;
}): string;
/**
 * Split a wire message (the envelope-prefixed text the frontend sends) into the
 * clean user text + structured attachments. The agent still receives the FULL
 * wire text — it needs the <viewer-context>/<attachments> envelopes — but what
 * we PERSIST and show in the user bubble is the clean version, so a reloaded
 * chat renders clean text + attachment thumbnails instead of raw envelope XML.
 * Attachment `url` is the workspace-relative path; the browser's resolveAssetUrl
 * turns it into a served URL. Dedups by path.
 */
export declare function splitUserWireText(wireText: string, workId: string): {
    text: string;
    attachments?: ChatBlockAttachment[];
};
/**
 * Whitelist of WS event types streamed to browsers. Stage-divider events were
 * removed in D3 — there are no stage boundaries to mark anymore.
 */
export declare const ALLOWED_STREAM_TYPES: readonly ["user", "text", "thinking", "tool_use", "tool_result", "locator"];
export declare class WsBridge {
    /** Nested keying (ADR-008): workId → sessionId → WsSession. A work has one
     *  entry per concurrent chat session; the default/legacy session is `s_1`.
     *  Trend sessions reuse the sessionKey as the workId slot with a single
     *  default sub-session — they are ephemeral and never multi-session. */
    private sessions;
    /** TTL (ms) after which an idle session is auto-archived on sweep.
     *  Injectable so tests don't wait 7 days. */
    private readonly idleTtlMs;
    private eventListeners;
    private browserWss;
    /** Backend HTTP port — injected into the agent env as AUTOVIRAL_PORT so the
     *  `autoviral` CLI (and any direct fetch) can reach this daemon. */
    private readonly serverPort;
    constructor(serverPort: number, opts?: {
        idleTtlMs?: number;
    });
    handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): boolean;
    /** Resolve the effective sessionId. Trend keys (`trends_…`) and any caller
     *  that omits sessionId target the default session. */
    private resolveSessionId;
    /** Look up a live in-memory session (or undefined). */
    private getSessionEntry;
    /** Store a live in-memory session, creating the per-work sub-map on demand. */
    private setSessionEntry;
    /** Drop a live in-memory session; prunes the per-work map when empty. */
    private deleteSessionEntry;
    /**
     * Sidecar for a session key. Returns null ONLY for ephemeral `trends_` keys
     * (intentionally history-less). A `coach_` key DOES get a real sidecar — the
     * research/strategy coach is a PERSISTED session whose history survives reload
     * (PRD-0006 D5). A normal work id gets one too.
     */
    private sidecarFor;
    /**
     * True iff `key` names a real Work (has a work.yaml record + checkpointable
     * deliverable). False for the two workless session kinds: ephemeral `trends_`
     * and the persisted `coach_`. Guards the work-record-only side effects
     * (getWork / syncMessage / createCheckpoint) so a coach turn doesn't try to
     * checkpoint a non-existent composition or look up a missing work.
     */
    private isWorkBound;
    ensureSession(workId: string, sessionId?: string): WsSession;
    /**
     * Append a single chat block to the per-session JSONL log on disk.
     * Fire-and-forget — write failure does not block the main flow. The default
     * session `s_1` maps to the legacy `chat.jsonl`; other sessions use
     * `chat-{sessionId}.jsonl` (ADR-008 §4).
     */
    private appendToChatLog;
    /**
     * Build a system prompt with full context for a given work — async wrapper
     * around the pure D3 prompt that adds dynamic shared-asset / memory blocks.
     */
    private buildSystemPromptWithContext;
    /**
     * Start a new CLI session. Loads work context, builds system prompt,
     * then spawns `claude -p <prompt> --output-format stream-json --verbose`.
     */
    createSession(workId: string, initialPrompt: string, model?: string, sessionId?: string): Promise<WsSession>;
    /**
     * Create an ephemeral trend research session.
     * Uses sonnet model, auto-kills after 180s, filters CLI events into simplified research events.
     */
    createTrendSession(sessionKey: string, prompt: string): Promise<WsSession>;
    /**
     * Assemble the coach's grounding context (works + selected-platform trends +
     * interests) from disk, then build the research/strategy system prompt
     * (D5, PRD-0006). Lazy by construction: the pure builder caps + budgets the
     * works block, so we never embed the full library every turn. Falls back to a
     * trends/interests-only prompt if the scrape is missing (honest thin-data).
     */
    private buildCoachPrompt;
    /**
     * Create (or restart) the PERSISTED research/strategy coach session for a
     * coach key (`coach_*`). Unlike the ephemeral `trends_` session this one is
     * sidecar-backed (history survives reload). The coach runs on a SESSION-SCOPED
     * model (`COACH_DEFAULT_MODEL` by default, or `opts.model`) — never the global
     * `config.model`, so switching the coach's tier can't steal the editing
     * agent's tier (the bug S6 fixes). `initialPrompt` is the user's first message;
     * we prepend the grounded coach system prompt on the FIRST turn only (resume
     * keeps the agent's existing context).
     */
    createCoachSession(coachKey: string, initialPrompt: string, opts?: {
        platform?: string;
        model?: string;
    }): Promise<WsSession>;
    /**
     * Set the model alias for ONE live session, scoped to that (workId,
     * sessionId) — the SESSION-scoped model fix (PRD-0006 D5). The old
     * ModelSwitcher wrote the GLOBAL `config.model`, so changing the coach's tier
     * also changed the editing agent's tier. This mutates only the in-memory
     * session; the new tier binds on the session's NEXT spawn (killing the live
     * CLI forces a respawn). Returns true if the session existed.
     */
    setSessionModel(workId: string, model: string, sessionId?: string): boolean;
    /**
     * Send a follow-up message using --resume + new -p.
     * Kills current CLI (if busy) and spawns a new one that resumes the session.
     */
    /**
     * Record a user-typed message in chat history without sending it to the CLI.
     * Used by `/api/works/:id/chat` after `createSession` (which sends the prompt
     * but doesn't push a user block).
     */
    recordUserMessage(workId: string, text: string, sessionId?: string): void;
    sendMessage(workId: string, text: string, sessionId?: string): Promise<boolean>;
    killSession(workId: string, sessionId?: string): boolean;
    killTrendSession(sessionKey: string): boolean;
    getSession(workId: string, sessionId?: string): WsSession | undefined;
    /** All live in-memory sessions for a work (ADR-008 multi-session). Empty map
     *  if the work has none in memory. */
    getWorkSessions(workId: string): Map<string, WsSession>;
    /**
     * Register a listener for session events. Returns cleanup function.
     * Used by TestRunner to wait for events without polling.
     */
    onSessionEvent(workId: string, callback: (event: string, data: unknown) => void): () => void;
    /** Flattened view: one entry per work, its DEFAULT session (or any one live
     *  session if the default isn't in memory). Back-compat shape for callers
     *  that predate multi-session keying. */
    getAllSessions(): Map<string, WsSession>;
    /**
     * After trend session completes, read the agent-written data.json and
     * copy it to the dated YAML cache so GET /api/trends/:platform picks it up.
     * Also read report.md and broadcast it to the frontend.
     */
    private finalizeTrendData;
    private cleanupTrendSession;
    /**
     * B7(a)-lite (PRD-0009) — compute the `--append-system-prompt` payload for a
     * RESUMED session and, when something is injected, persist that this session
     * is now taught up to the current PROMPT_VERSION.
     *
     * Reads the session's `lastInjectedPromptVersion` from its sidecar record
     * (undefined ⇒ 0, so a legacy/pre-feature session gets the full changelog
     * once). Returns the concatenated changelog of every version strictly newer
     * than the stored one, or undefined when already current. Best-effort: any
     * sidecar read/write failure falls back to NOT injecting (the resume still
     * works; the session just stays on its old teaching until next time).
     *
     * CRITICAL (review fix, PRD-0009 Wave B): it stamps `session.taughtPromptVersion`
     * with the version this resume actually delivers — NOT an unconditional
     * PROMPT_VERSION. The downstream system.init / result writeback reads that
     * field, so a resume that SKIPPED injection (sidecar read failed, or already
     * current) does not falsely record "taught to current" and permanently
     * suppress the teaching it never delivered. Left undefined only when we cannot
     * know the taught version (read threw before we read `stored`) — the writeback
     * then leaves `lastInjectedPromptVersion` untouched so the next resume retries.
     *
     * (The legacy default session whose cliSessionId only lived in work.yaml is
     * NOT a no-record case here: createSession calls ensureSidecarRecord before
     * this runs, which seeds the record from work.yaml — so `record` is present
     * and the version persists. The `if (record)` guard is defensive only.)
     */
    private resumePromptAppend;
    private spawnCli;
    private handleBrowserConnection;
    /**
     * Work-scoped broadcast — fans an event to EVERY session's browser sockets
     * on the work (ADR-008 §3: focus / playhead / selection stay shared across
     * sessions). Also notifies in-process event listeners (TestRunner). Used for
     * cross-session work-level events; per-turn CLI streaming uses
     * broadcastToSession so two concurrent chats don't cross-contaminate.
     */
    broadcastToBrowsers(workId: string, payload: {
        event: string;
        data: unknown;
    }): void;
    /**
     * Session-scoped broadcast — fans an event to the browser sockets of ONE
     * (workId, sessionId) session only (its own chat stream). Same-session
     * multi-attach (N tabs on the same session) all receive it. Still notifies
     * the work-scoped event listeners so TestRunner sees the stream.
     */
    private broadcastToSession;
    /**
     * Ensure a sidecar record exists for (workId, sessionId), performing the lazy
     * legacy migration: a work with no `.sessions.jsonl` and a default session id
     * synthesizes `s_1` seeded from `work.yaml.cliSessionId` (history stays in the
     * legacy `chat.jsonl` — no bulk rename). Reopening an archived session
     * restores it. Returns the record (or null for ephemeral trend keys).
     */
    private ensureSidecarRecord;
    /** Bump lastActive (+ seed preview on first user line) for a chat session. */
    private bumpSessionActivity;
    /** List a work's sessions from the sidecar (back-compat: triggers the lazy
     *  migration for legacy works so the list is never empty for a work that has
     *  ever had a chat). Excludes archived sessions unless includeArchived. */
    listSessions(workId: string, opts?: {
        includeArchived?: boolean;
    }): Promise<SessionRecord[]>;
    /**
     * Create a brand-new chat session for a work (mints the next id), persisting
     * its sidecar record. Returns the record. Does NOT spawn a CLI — the next
     * sendMessage/createSession on that sessionId does.
     */
    createNewSession(workId: string): Promise<SessionRecord | null>;
    /**
     * Hard-delete a chat session: dispose its in-memory WsSession + CLI, tombstone
     * the sidecar record, and remove its chat log. ANY session is deletable now
     * (incl. the default s_1 — callers enforce the "keep at least one" invariant),
     * so on success we ALWAYS remove the session's chat log (chatLogPath returns
     * chat.jsonl for s_1, chat-{sid}.jsonl otherwise). When deleting s_1 we also
     * remove the legacy chat.json snapshot so no orphan file survives to resurrect
     * stale history via the loadWorkChat fallback.
     */
    deleteSession(workId: string, sessionId: string): Promise<boolean>;
    /**
     * Idle-TTL auto-archive sweep (ADR-008 §5). For every non-trends work with a
     * sidecar, archive sessions whose lastActive is older than idleTtlMs: dispose
     * the in-memory session + CLI (release memory), keep the chat log on disk, and
     * flag archived=true. Restorable later. Returns the archived (workId,
     * sessionId) pairs. `nowMs` injectable for tests.
     */
    archiveIdleSessions(workId: string, nowMs?: number): Promise<string[]>;
    /**
     * Boot/periodic sweep across ALL works — auto-archives every idle session
     * (ADR-008 §5). Fire-and-forget at startup; failures on one work don't abort
     * the rest. Returns the total count archived. `nowMs` injectable for tests.
     */
    sweepAllIdleSessions(nowMs?: number): Promise<number>;
    /**
     * Lazy legacy migration of a single work (ADR-008 §4). If the work has no
     * `.sessions.jsonl` but has a chat.jsonl / chat.json / work.yaml.cliSessionId,
     * synthesize the `s_1` chat record (history stays in chat.jsonl). Idempotent —
     * a no-op when a sidecar already exists. Returns true if a record was written.
     */
    migrateLegacyWork(workId: string): Promise<boolean>;
    /** Parse a browser WS route into { workId, sessionId }. Accepts both
     *  /ws/browser/{workId} (legacy → default session) and
     *  /ws/browser/{workId}/{sessionId}. Returns null on no match. */
    private extractBrowserRoute;
}
//# sourceMappingURL=ws-bridge.d.ts.map