import { MessageRecord, Attachment } from "../types.js";
import { type SessionResolution } from "./session-resolver.js";
/**
 * Slice 2 — busy-state queue. A deliberately small, human-facing status
 * vocabulary surfaced to the SENDER via the `message` field (cloud mode
 * transmits only `message`, never `response` — the 3.5.10 lesson). The
 * `[cogent:<status>]` marker is both human-readable (a person watching in Slack
 * sees "⏳ … busy … queued") AND machine-recognizable, so the sender's client
 * can treat a status notice as NON-TERMINAL: it must never resolve a pending
 * send_message waiter (the real answer follows when the busy peer drains its
 * queue) and never trigger a local wake.
 */
export declare const RELAY_STATUS: {
    readonly QUEUED: "queued";
    readonly PROCESSING: "processing";
    readonly DELIVERED: "delivered";
    readonly NEEDS_MANUAL: "needs-manual";
    readonly FAILED: "failed";
};
export type RelayStatus = (typeof RELAY_STATUS)[keyof typeof RELAY_STATUS];
/**
 * Build a sender-facing status notice. The `[cogent:<status>]` marker is ALWAYS
 * first so {@link parseStatusNotice} can anchor on it; the human-readable text
 * (incl. the ⏳ glyph for a Slack reader) follows.
 */
export declare function buildStatusNotice(status: RelayStatus, peerId: string, traceId?: string): string;
/**
 * Parse a status notice back to its status, or null for an ordinary message.
 * Anchored: only a message that STARTS with the marker is a notice, so a real
 * answer that merely mentions the token mid-text is (correctly) not one.
 */
export declare function parseStatusNotice(message: string): RelayStatus | null;
export declare function formatRelayMessage(fromLabel: string, fromPeerId: string, message: string, attachments?: Attachment[]): string;
/**
 * Turn a failed wake's REAL outcome into one readable clause.
 *
 * 🔴 WHY THIS EXISTS. A wake that captured nothing used to be reported as
 * "it may have been busy, or the resume produced no output" — a guess, written
 * while the subprocess's own explanation sat unread in `CliExecResult.stderr`.
 * For a user whose Claude config had moved, that stderr said, in words,
 * "No conversation found with session ID: <id>". Nobody saw it, so a one-line
 * root cause took five weeks and three releases to find.
 *
 * Pure, bounded and unit-tested (wake-failure-diagnostic.test.ts): the wording is
 * a fixture, not prose that can quietly rot. Returns "" when there is genuinely
 * nothing to add, so the caller can append unconditionally.
 */
export declare function describeWakeFailure(exitCode: number | null | undefined, stderr: string | undefined, stdout?: string): string;
export declare class AutoRelayService {
    private localPeerId;
    private localSessionId;
    /**
     * The local session pinned at register time (the caller was provably active
     * then, so its resolution is trustworthy). At wake time the resolver
     * exact-matches this pin instead of re-guessing newest-mtime — the fix for
     * the silent wrong-session drift. Re-pinned when the resolver confirms a new id.
     */
    private pinnedSessionId;
    private localCwd;
    /** Every folder this process announced the current peer name under — what a drain or a stop must cover. */
    private announcedCwds;
    /**
     * Peer participation mode (3.2.0, Phase 17 OBS-03).
     * - "agent" (default): full auto-relay — inbound directed messages run
     *   through delivery-ack + queueing + execRemote.
     * - "observer": short-circuit before delivery-ack and processing; the peer
     *   is a passive watcher. Set by `setLocalPeer()` from the register-peer
     *   tool when the operator passes `mode: "observer"`.
     *
     * Phase 20 will OR additional conditions (presence/DND/AFK) into the
     * short-circuit check via the `_shouldSkipAutoRelay()` helper.
     */
    private localMode;
    /**
     * Human-readable label captured at register-peer time. Preserved here so
     * triggerReRegistration() (startup.ts) can pass the original label on
     * auto-reconnect after a server eviction or WS cycle, instead of falling
     * back to the peerId. Closes the v3.2.1 label-normalization regression
     * where 'Cogent_Master' silently became 'cogent-master' after the first
     * mass-reregister event.
     */
    private localLabel;
    private processing;
    /** Senders already told this bridge is superseded — one notice each, not one per message. */
    private supersededNotified;
    /** Reaping leftovers is a one-shot per process, not a per-registration action. */
    private reapedStaleBridges;
    private queue;
    private awarenessOnly;
    private expectingResponseFrom;
    private responseWaiters;
    /**
     * Pending delivery-ack waiters, keyed by MESSAGE ID.
     *
     * 🔴 AUD-002. This used to be keyed by PEER ID, so a peer could have only one outstanding
     * waiter and a second concurrent send displaced the first — resolving it, so the first send
     * reported "delivered" with no acknowledgment from anyone. Delivery acknowledgment is the one
     * claim this system must never fabricate.
     */
    private deliveryAckWaiters;
    /**
     * Acks that arrived before their waiter existed, keyed by message id.
     *
     * The recipient acknowledges over WS as soon as it receives the message, which can beat our own
     * POST response. Waiters used to be registered BEFORE the send to dodge that race, which is what
     * forced peer-keying. Buffering the early ack instead lets registration happen after the message
     * id is known, so correlation stays exact. Entries are consumed once and expire.
     */
    private recentAcks;
    private sendDeliveryAckFn;
    /**
     * Set the local peer info. Called from register-peer tool after
     * successful registration so we know which session to inject into.
     */
    setLocalPeer(peerId: string, sessionId: string, cwd: string, mode?: "observer" | "agent", label?: string, pinnedSessionId?: string): void;
    /**
     * Emit one content-free wake-trace line (ids/decisions/timings only — NEVER
     * message or response text). Correlated by a per-wake traceId. Events:
     * delivered · resolved · wake · captured · refused · failed.
     */
    private _trace;
    /**
     * Internal predicate: should auto-relay short-circuit BEFORE delivery-ack,
     * BEFORE waiter resolution, and BEFORE _processMessage calls the local agent CLI via execRemote?
     *
     * Extracted as a method so Phase 20 (presence/DND/AFK) can OR additional
     * conditions in here without rewriting handleIncoming. The short-circuit
     * site is upstream of delivery-ack on purpose: observers are passive — the
     * sender should not believe the message was "delivered to an agent that
     * will act on it". (STATE.md blocker for Phase 20 reusability.)
     */
    private _shouldSkipAutoRelay;
    /**
     * Get the local peer info for re-registration after eviction.
     * Returns null if no peer has been registered yet.
     */
    getLocalPeer(): {
        peerId: string;
        sessionId: string;
        cwd: string;
        label: string;
        mode: "observer" | "agent";
    } | null;
    /**
     * Re-detect the current local agent session from disk. If the session has
     * rotated (CC/Codex creates a new session file after ~1hr idle), update
     * localSessionId so the next execRemote call targets the active session.
     */
    refreshSessionId(): Promise<SessionResolution>;
    /**
     * Set the function used to send delivery acknowledgments via WebSocket.
     * Injected by startup.ts to avoid circular dependency with ws-client.
     */
    setSendDeliveryAck(fn: (messageId: string, fromPeerId: string, toPeerId: string) => void): void;
    /**
     * Mark that we expect a response from a peer (because we just sent them
     * an explicit message via send_message). When their response arrives,
     * inject it into the local session but do NOT relay our output back.
     * This breaks the infinite auto-relay loop.
     */
    expectResponse(fromPeerId: string): void;
    /**
     * Wait for a response from a peer. Returns a Promise that resolves with
     * the response message content when the target peer responds.
     *
     * This is the key mechanism that makes cloud mode behave like local mode:
     * send-message calls this before sending, then awaits the Promise.
     * When the response arrives via WS, the Promise resolves with the actual
     * response text, which send-message returns as the tool result.
     *
     * On timeout, the Promise rejects but the expectation is kept in
     * expectingResponseFrom so late responses still get inject-only treatment
     * (preventing infinite auto-relay loops).
     */
    waitForResponse(fromPeerId: string, timeoutMs: number): Promise<string>;
    /**
     * Resolve the oldest pending waiter for `fromPeerId` with the given message.
     * Returns true if a waiter was found and resolved, false otherwise.
     */
    private _popAndResolveWaiter;
    /**
     * Clear the expectation for a peer and cancel any pending waiter.
     */
    clearExpectation(fromPeerId: string): void;
    /**
     * Cancel only the active response waiter for a peer while keeping the
     * expectation marker. Used when send-message returns early but a late
     * response should still be treated as inject-only.
     */
    cancelResponseWaiter(fromPeerId: string): void;
    /**
     * Cancel ALL pending response waiters for a peer by rejecting them with
     * a clear error. Used by clearExpectation()/cancelResponseWaiter() to
     * unblock callers without lying to them. The previous behaviour resolved
     * waiters with the literal string "Superseded by newer send_message call",
     * which was then returned as the peer's "response" -- a confusing and
     * easily misinterpreted artifact.
     */
    private _cancelWaiter;
    /**
     * Wait for a delivery acknowledgment from a peer. Returns a Promise that
     * resolves when the target peer's MCP sends a delivery_ack frame, or
     * rejects on timeout (recipient likely offline).
     */
    waitForDeliveryAck(fromPeerId: string, messageId: string, timeoutMs: number): Promise<void>;
    /**
     * Settle the waiter for exactly this message.
     *
     * 🔴 Correlation is by MESSAGE ID, and the acknowledging peer must match the peer we sent to.
     * The frame has carried `messageId` all along (src/cloud/ws-client.ts:52); startup.ts used to
     * drop it and resolve by peer, so any ack settled any outstanding send to that peer — and an ack
     * naming a peer the sender never messaged could settle a waiter outright (AUD-003).
     *
     * An ack with no matching waiter is BUFFERED rather than discarded: the recipient can legitimately
     * acknowledge before our POST response returns.
     */
    resolveDeliveryAck(fromPeerId: string, messageId: string): void;
    /** Remember an ack whose waiter does not exist yet, with bounded retention. */
    private _bufferEarlyAck;
    private _cancelDeliveryAckWaiter;
    /**
     * Process an incoming cloud message by calling the local agent CLI via execRemote()
     * and relaying the response back to the sender.
     *
     * Guards:
     * - Not registered yet (localPeerId is null)
     * - Message not addressed to us
     * - Message is from ourselves (prevent echo)
     * - Message is already a response (response field is non-null)
     * - Serialized processing with queue to prevent concurrent execRemote calls
     */
    handleIncoming(msg: MessageRecord): Promise<void>;
    /**
     * Tell the sender we are a LEFTOVER bridge and are not answering — instead of going silent.
     *
     * Silence is the failure mode this codebase treats as worst: it is indistinguishable from
     * "nothing to do". A superseded bridge is usually reachable on a channel the newer bridge is NOT
     * on (the measured case had the ghost on the FREE relay and the live one on TEAM), so without
     * this the sender would wait forever with no explanation.
     *
     * Once per sender per process — a leftover that is being messaged steadily must not turn into a
     * notice generator.
     */
    private _reportSuperseded;
    /**
     * Dispatch a message under the `processing` lock: an awareness-marked broadcast is
     * inject-only (see, no reply); everything else is a full wake+reply. Routing both
     * through here guarantees we never run two session resumes concurrently.
     */
    private _dispatch;
    /**
     * Slice 2: relay a NON-TERMINAL status notice back to the sender as a new
     * cloud message. The notice rides in the `message` field (cloud transmits
     * only `message`) with a `[cogent:<status>]` marker the sender's client
     * recognizes, so it does NOT resolve a pending send_message waiter. Tagged
     * `isRelayEcho` so server 3.1.6+ filters it from history like other echoes.
     * Best-effort: a failure here must never break the busy-queue path.
     */
    private _emitStatusNotice;
    /**
     * Slice 3 (Story 8.4): relay a captured reply back to the sender, recording
     * HOW it was captured so a downgrade (structured `event_stream` → raw
     * `stdout_compat`, or a structured error) is EXPLICIT in the wake trace and
     * never disguised as a clean auto-capture (AC3). On an unstable capture it
     * also emits a visible, non-terminal `[cogent:needs-manual]` notice to the
     * sender — the answer still relays, but the human/agent knows it wasn't a
     * trusted structured capture. Used by BOTH the primary and rotated-session
     * retry paths (F-B) so neither can disguise a downgrade.
     */
    private _relayCapturedReply;
    /**
     * Internal: process a single message -- calls the local agent CLI via execRemote and relay response.
     */
    private _processMessage;
    /**
     * Write the durable wake record. Wrapped whole: visibility must never be able to fail a
     * wake that would otherwise have succeeded, so every error here is swallowed and logged.
     */
    private _recordWake;
    /**
     * Fail-loud (2026-08-11): a wake that captured NO reply — empty stdout, a
     * non-zero exit with no rotated-session retry, a failed retry, or a thrown
     * execRemote — used to record NOTHING, so the sender saw response:null with no
     * error (the invisible failure that hid the demo busy-miss). Record ONE visible
     * success:false notice to the sender so a human/agent knows to reply manually.
     * On Codex this pairs with Wake-C (check-on-stop), which auto-recovers the reply
     * at the next turn boundary. Refusal (AMBIGUOUS_SESSION) and sandbox-blocked
     * paths record their own failure and return before this, so no double-record.
     */
    private _recordNoReplyFailure;
    /**
     * Inject a message into the local session without relaying the response.
     * Used for expected responses to our own send_message calls.
     */
    private _injectOnly;
    /**
     * Full-group-chat awareness (COGENT_BROADCAST_AWARENESS): inject a PEER's broadcast
     * into the local session so the agent SEES what its peers said. The response is
     * discarded and the notice forbids replies/tool calls, so this can never emit another
     * broadcast — no storm. Mirrors _injectOnly but with the awareness-only formatter.
     */
    private _injectPeerBroadcast;
    /**
     * Get diagnostic info about the auto-relay state.
     * Used by health-check to help debug relay issues.
     */
    getDiagnostics(): {
        registered: boolean;
        localPeerId: string | null;
        mode: "observer" | "agent";
        processing: boolean;
        queueLength: number;
        expectingResponseFrom: string[];
        activeWaiters: string[];
        activeDeliveryAckWaiters: string[];
    };
    /**
     * Stop claiming this peer's name for this checkout, WITHOUT stopping work already in flight.
     * Called the moment the MCP client is gone: a draining bridge serves no agent, and while it
     * counted as the peer's holder, the restarted agent's new bridge stood by behind it (review r2
     * NEW-1). The announcement is MARKED, not removed — see markBridgeDraining (R3-2). Idempotent.
     * 🔴 EVERY announcement, not only the last registered cwd's (review r7 SEC2-1, ABC2-2, FIX2-5):
     * after a register with another spelling of the folder, the one for the bridge's own folder stayed
     * unmarked, and the restarted agent waited out the drain or stood by for good.
     */
    announceDraining(): void;
    /** Remove this bridge's ownership announcements (graceful shutdown, a rename). Idempotent. */
    withdrawAnnouncement(): void;
    /**
     * Stop the auto-relay service: clear queue and reset state.
     */
    stop(): void;
}
/** Singleton instance used throughout the application. */
export declare const autoRelay: AutoRelayService;
//# sourceMappingURL=auto-relay.d.ts.map