/**
 * Server-side Y.Doc manager for collaborative editing.
 *
 * Maintains one Y.Doc per actively edited entity. The server is the single
 * seeding authority — it loads entity content from disk into the Y.Doc when
 * the first client connects. Clients start with empty Y.Docs and receive
 * content via the Yjs sync protocol. This prevents the double-seeding bug
 * where two clients independently insert the same content.
 *
 * Lifecycle:
 * 1. First client opens entity → server creates Y.Doc, seeds from disk
 * 2. Server sends sync-step-1 to client → client responds → both converge
 * 3. Client edits → binary update to server → server applies + relays to peers
 * 4. Last client leaves → server destroys Y.Doc (frees memory)
 */
import type { WorkspaceManager } from '../core/workspace-manager.js';
import type { YjsPersistence } from './yjs-persistence.js';
/**
 * Thrown by `assertWritable` when broadcast is disabled AND a doc has
 * active clients AND the operator has not explicitly opted into the
 * degraded mode via `STUDIOGRAPH_ALLOW_DEGRADED_WRITES`. Surfaces a hard
 * error to the caller (MCP / agent / event processor) instead of silently
 * mutating the server's Y.Doc out of sync with connected editors.
 */
export declare class BroadcastDisabledLiveClientsError extends Error {
    readonly docName: string;
    readonly code: "BROADCAST_DISABLED_LIVE_CLIENTS";
    constructor(docName: string);
}
/**
 * Live-merge note: there used to be a `LiveEditorPendingError` thrown by
 * `assertWritable` when a connected editor had unsaved typing, refusing the
 * server-write outright. It was retired when server-writes switched to
 * CRDT-merging into the live Y.Doc via `applyAgentContent` (the user watches
 * the change land, with a "Agent edited this note" marker). The accepted
 * residual — a whole-body target reverting the user's un-autosaved edits in
 * rewritten regions — is removed later by the edit-region tool.
 */
export declare class YjsManager {
    private workspaceManager;
    private persistence?;
    private docs;
    /**
     * In-flight `getOrCreate` promises, keyed by docName. Concurrent calls for
     * the same docName await the same promise instead of building two Y.Docs
     * (which would race on the persistence file and split client traffic).
     */
    private loadingPromises;
    /**
     * Whether the update-broadcast observer is registered. Read once at
     * construction from `STUDIOGRAPH_YJS_BROADCAST` (default ON). Flipping the
     * env var requires a restart to take effect — dynamic re-check would let
     * connected editors silently diverge when the flag is toggled mid-session.
     */
    private readonly broadcastEnabled;
    /**
     * Operator opt-in to accept degraded-mode data loss (broadcast off +
     * server-side writes while clients are connected). Off by default —
     * forcing operators to make the data-loss trade-off explicit.
     */
    private readonly allowDegradedWrites;
    /**
     * Per-doc lock chain (Phase 2). Each `withDocLock(docName, fn)` appends
     * to this chain so server-side writes for the same doc serialize. Client
     * `handleMessage` updates do NOT acquire the lock — instead they queue
     * into {@link msgSyncQueue} while the lock is held and apply on release.
     */
    private docLocks;
    /**
     * Per-doc MSG_SYNC buffer. While `withDocLock` is held for a doc, inbound
     * client sync frames for that doc are pushed here instead of applied
     * directly. On lock release, frames are drained in arrival order via the
     * normal `handleMessage` path with origin `'ws-client'`.
     *
     * This is the Codex pass-3 #1 fix: without queueing, a client insert
     * arriving mid-lock would land in `entry.ydoc` BEFORE `applyAgentContent`
     * runs, and the agent's full-body diff would delete that just-typed char.
     * With queueing, the client's insert lands on the post-agent ytext as a
     * fresh CRDT op at its original anchor.
     */
    private msgSyncQueue;
    constructor(workspaceManager: WorkspaceManager, persistence?: YjsPersistence | undefined);
    /** Whether the update-broadcast observer is active. Exposed for diagnostics. */
    isBroadcastEnabled(): boolean;
    /**
     * Pre-write guard for server-write body paths. Throws when broadcast is
     * disabled, the doc has active clients, and the operator hasn't opted into
     * degraded-mode writes — server-side writes would silently diverge from
     * connected editors (→ `BroadcastDisabledLiveClientsError`).
     *
     * It used to ALSO refuse a server-write when a connected editor had unsaved
     * typing (`LiveEditorPendingError`). That refusal was retired with the
     * live-merge model: server-writes now CRDT-merge into the live Y.Doc via
     * `applyAgentContent` instead of being blocked. The residual — a whole-body
     * target reverting the user's un-autosaved edits in regions the agent
     * rewrote — is accepted (the future edit-region tool removes it). The user
     * sees the merge land live plus a "Agent edited this note" marker.
     *
     * Called from `writeBody` via the `assertWritable` hook, gated to
     * `source === 'server-write'`. Autosave is the client's own write and is
     * never refused.
     */
    assertWritable(docName: string): void;
    /**
     * Acquire a per-doc lock and run `fn` to completion. While the lock is
     * held, inbound client MSG_SYNC frames for the same docName are queued
     * (see {@link msgSyncQueue}) and applied after the lock releases.
     *
     * Contract: server-side writes serialize against other server-side
     * writes for the same doc. Client typing does NOT block — frames are
     * buffered and applied in arrival order on release. The Codex pass-3 #1
     * trace proved this is necessary: without queueing, a client keystroke
     * arriving mid-lock would be deleted by the agent's subsequent full-body
     * diff (the keystroke lives in ytext but not in the agent's target).
     *
     * Per-doc only — other docs' handleMessage traffic is unaffected.
     *
     * Failures inside `fn` propagate to the caller; the lock releases either
     * way so the queued sync drain still runs.
     */
    withDocLock<T>(docName: string, fn: () => Promise<T>): Promise<T>;
    /** Whether a server-write lock is currently held for the doc. */
    isLocked(docName: string): boolean;
    /**
     * Buffer an inbound MSG_SYNC frame for later application. Called from
     * `handleMessage` when the doc is locked.
     */
    private enqueueMsgSync;
    /**
     * Drain any queued MSG_SYNC frames for `docName`, applying them via the
     * normal handleMessage path. Called from `withDocLock` on release.
     */
    private drainMsgSyncQueue;
    /**
     * Apply a client sync message directly, bypassing the lock check. Used
     * by `drainMsgSyncQueue`. Mirrors the MSG_SYNC branch of
     * {@link handleMessage} — keep in sync if that branch changes.
     */
    private applyClientSync;
    /**
     * Apply a server-write target body to the live Y.Doc.
     *
     * Called from `graph.update`'s server-write branch AFTER disk + commit
     * have succeeded. The disk-first ordering is deliberate: clients only see
     * server-initiated changes that are durable. If applying here fails,
     * disk and commit are unaffected — but live clients are out of sync.
     *
     * Returns `{ applied: 'no-doc' | 'unseeded' | 'ok' }`. For the first two,
     * the caller already wrote disk; no live editor needs the broadcast.
     *
     * On `'ok'`: sets `entry.mdRevisionHash = sha256(target)` BEFORE the
     * transact wraps, applies the diff with origin `'server-agent-write'`
     * (broadcast-allowlisted), returns success.
     *
     * On failure (rare — applyTextDiff is just delete/insert ops), sends a
     * `doc_conflict` JSON frame to all clients and rethrows. Conflict UI
     * is the user's signal that disk and Y.Doc diverged.
     */
    applyAgentContent(docName: string, target: string): {
        applied: 'no-doc' | 'unseeded' | 'ok';
    };
    /**
     * Send a `doc_conflict` JSON frame to every connected client of a doc.
     * Editor handles by entering readonly conflict UI: "another writer made
     * a change we couldn't sync to your editor; reload to continue."
     *
     * Used by `applyAgentContent` failure path. Distinct from `doc_ready`
     * (the heartbeat) — conflict implies user must take action.
     */
    sendDocConflict(docName: string, reason: string): void;
    /**
     * Parse a docName ("repo/entityType/entityId") into its components.
     */
    private parseDocName;
    /**
     * Get or create a Y.Doc for an entity.
     *
     * Async because persistence loads from disk. Concurrent calls for the same
     * docName await the same in-flight promise so we don't build two Y.Docs.
     *
     * If a persisted snapshot exists, applies it to the fresh Y.Doc and marks
     * `seeded = true` immediately — reconnecting clients then see a server
     * state vector that already includes their pre-restart ops, so sync-step-2
     * is a no-op and the duplication race is closed.
     *
     * If no snapshot exists (first-ever open of this entity, or persistence
     * disabled), leaves `seeded = false`. The existing deferred-seed path in
     * addClient + handleMessage will populate the Y.Doc from the .md file.
     */
    private getOrCreate;
    private doLoad;
    private readDiskContent;
    /**
     * Send a JSON `doc_ready` notice to every connected client of `docName`,
     * carrying the authoritative ytext length the client should match before
     * unlocking its editor. This is the seed-success heartbeat the client
     * uses to confirm its local apply actually landed — without it, a frame
     * that arrived but failed to decode (e.g. Safari/WebKit fragmentation
     * issue) would silently leave the editor unlocked on an empty Y.Doc and
     * the next keystroke + autosave would overwrite the disk file.
     */
    private broadcastDocReady;
    /**
     * Send `doc_ready` to a single client. Used in addClient when the entry
     * is already seeded — the new joiner needs the heartbeat to unlock its
     * editor without waiting for the next state transition.
     */
    private sendDocReadyToClient;
    /**
     * If the Y.Doc is still empty (no client sync has populated it), load
     * content from disk and mark the entry as seeded. Broadcast the seeded
     * state to all connected clients as a sync-step-2 so they see it.
     *
     * Runs ~SEED_WAIT_MS after a client connects. Short-circuits if any
     * earlier sync already flipped the `seeded` flag.
     */
    private maybeSeedFromDisk;
    /**
     * Add a client to a document. Async because `getOrCreate` may need to load a
     * persisted snapshot from disk.
     *
     * Protocol (ordered carefully to make first paint resilient):
     *   1. Server: load doc (snapshot or fresh) — `getOrCreate`.
     *   2. Server: send sync-step-1 + sync-step-2 (full state) + awareness
     *      FIRST. The client's editor populates and unlocks the moment the
     *      sync-step-2 lands; it doesn't need to wait for the ack to render.
     *   3. Server: send `collab:joined` ack JSON LAST. This tells the client
     *      it's safe to send its own sync-step-1 + awareness, the load-race
     *      protection codex flagged. Sending content before the ack means a
     *      delayed/dropped ack still leaves the editor populated — only the
     *      client's outbound state vector is gated.
     *   4. Client: receives sync-step-2 → editor renders. Then `collab:joined`
     *      → client sends its own sync-step-1 + awareness.
     *   5. Server: applies client sync, relays to peers.
     */
    addClient(docName: string, ws: import('ws').WebSocket): Promise<void>;
    /**
     * Remove a client from a document. Destroys Y.Doc when last client leaves.
     */
    removeClient(docName: string, ws: import('ws').WebSocket): void;
    /**
     * Remove a client from ALL documents (called on WS close).
     */
    removeClientFromAll(ws: import('ws').WebSocket): void;
    /**
     * Handle a binary Yjs message from a client.
     * Applies sync messages to the server Y.Doc and relays to peers.
     * Awareness messages are relayed without server processing.
     */
    handleMessage(docName: string, sender: import('ws').WebSocket, msgType: number, payload: Uint8Array): void;
    /**
     * Encode and send a framed binary message to a client. Payloads above
     * `MAX_CHUNK_PAYLOAD` are split into MSG_CHUNK frames; smaller ones go
     * through the single-frame fast path unchanged.
     */
    private sendToClient;
    private sendSingleFrame;
    private sendChunkedFrames;
    /**
     * The docNames of every Y.Doc currently loaded (a client has it open, or
     * it's within the post-disconnect idle grace period). Cheap in-memory map
     * read — no disk I/O. Used by `graph.pullAndReconcile` to gate the
     * post-pull reconcile loop to only the entities a user actually has open,
     * so a repo-wide pull doesn't parse every changed file when nobody's
     * editing. Includes unseeded docs (the caller still matches them by
     * docName; `reconcileFromDisk` no-ops harmlessly on those).
     */
    getActiveDocNames(): string[];
    /**
     * Get current Y.Doc text content for a document (for debugging/autosave).
     */
    getText(docName: string): string | null;
    /**
     * Authoritative live body for a document, or null when there's nothing
     * trustworthy to read. Unlike {@link getText}, this returns null unless the
     * entry exists AND is seeded — an unseeded Y.Doc carries Y.Text='' before
     * its first client sync, and reporting that as the body would masquerade an
     * empty doc as a real (empty) entry.
     *
     * This is the read side of the live-vs-disk gap: a user's freshly-placed
     * image/asset embed lives in the Y.Doc and only reaches disk after the
     * autosave debounce + commit. `graph.get` reads disk, so callers that need
     * to see what the user is actually looking at (get_entity, the asset-drop
     * guard) consult this first and fall back to disk when it returns null.
     */
    peekLiveBody(docName: string): string | null;
    /**
     * Reconcile an active Y.Doc with the latest .md file on disk. EXPLICIT
     * external-disk-change primitive — call this from named sites only:
     * startup boot, git-pull event handlers, dev-tools "reload from disk".
     *
     * NOT called from the normal write path. Server-side body writes (MCP,
     * agent, event processor) route through {@link applyAgentContent}, which
     * applies the target body under the per-doc lock AFTER disk + commit
     * land. Reconcile here is for changes the server didn't originate.
     *
     * Uses Yjs-aware text diffing (see {@link applyTextDiff}). Chars in
     * UNCHANGED regions retain their original `(clientID, clock)`. Cursor
     * anchors via `Y.createRelativePositionFromTypeIndex` in unchanged regions
     * stay valid. Y.UndoManager stacks referencing those chars stay coherent.
     *
     * KNOWN LIMIT: chars in CHANGED regions are still discarded. If a user has
     * typed `world ` into a "Hello " doc but autosave hasn't fired yet, the
     * Y.Doc has "Hello world " while disk still has "Hello ". When an external
     * write lands "Hi " on disk and we reconcile, the diff becomes
     * `[delete "Hello "][insert "Hi "][equal "world "]`. The user's "world "
     * survives because it's an unchanged region; their typing in the head is
     * gone because that's the changed region. This is the irreducible residue
     * of "make ytext match disk" semantics for external sources.
     *
     * No-ops when no Y.Doc is currently active for docName — clients that open
     * later will pick up the .md content via fresh seeding in `getOrCreate`.
     */
    reconcileFromDisk(docName: string): void;
    /**
     * Record that the .md body for an entity was just written to disk. Called
     * from `graph.update`'s `onBodyWritten` callback.
     *
     * Updates `entry.mdRevisionHash` if the doc is currently loaded so the
     * next persistence snapshot captures the post-write state. No-op when
     * the doc isn't loaded — the next `doLoad` will compute the hash from
     * the disk content on demand.
     */
    recordDiskBody(docName: string, body: string): void;
    /**
     * Destroy all Y.Docs (server shutdown). Drains pending persistence writes
     * before tearing down so a debounced snapshot can't be lost on shutdown.
     */
    destroy(): Promise<void>;
}
