import { type AmbiguousSubmissionRecoveryFailure, type SessionBackend, type SpawnOpts, type SessionProbe } from './types.js'; interface ZmxSessionProbeResult { ok: true; sessions: string[]; unhealthySessions: string[]; raw: string; } export type ZmxManagedSessionProbe = { state: 'missing'; } | { state: 'unknown'; reason: string; } | { state: 'compatible'; pid: number; clients: number | null; } | { state: 'incompatible'; pid: number; clients: number | null; reason: 'transport-label' | 'session-label'; }; /** Convert plain line feeds into terminal-safe CRLF without doubling CRLF. */ export declare function normaliseZmxHistory(text: string): string; /** * Persistent ZMX backend built from three non-leader primitives: * * - `zmx tail` is a low-latency change/liveness signal (never a byte source); * - `zmx send` injects input without taking client leadership or resizing; * - `zmx history` is the sole authoritative plain-text screen source. * * A one-shot `zmx attach ... /bin/sh ` is used only as the * race-safe create primitive. Its stdin is /dev/null, so the client exits * immediately and never remains as a synthetic leader. The private bootstrap * does not launch the CLI until botmux has proved ownership, stamped the * transport protocol labels and observed the tail client connected. */ export declare class ZmxBackend implements SessionBackend { private readonly sessionName; private readonly opts; private tailProcess; private readonly dataCbs; private readonly screenResyncCbs; private readonly exitCbs; private reattaching; private intentionalExit; private exited; private state; private tailEpoch; private reconnectAttempt; private reconnectTimer; private stableTailTimer; private historyTimer; private historyTimerDueAt; private historyProcess; private historyGeneration; private historyCaptureSerial; private historyInFlight; private historyAgain; private historyAgainActivity; private historyAgainForceResync; private tailActivitySinceCapture; private forceResyncOnNextSnapshot; private stableHistoryPolls; private snapshotCache; private hasSnapshot; private pendingScreenResyncReplay; private readonly historyColdJitterMs; private readonly historySettleWaiters; /** A same-name session failed ownership checks and must never be killed. */ private preserveSessionOnDestroy; private pendingExit; private earlyBuffer; private lastOpts; /** Frozen ZMX PTY-root PID for the session generation we may control. */ private backingPid; /** Stable direct child of the PTY root; wrapper resolution may refine cliPid. */ private launchPid; /** Monotonic generation of compatible, transport-ambiguous text writes. */ private ambiguousTextFailureSequence; /** Highest ambiguous text generation confirmed covered by Ctrl+C. */ private cancelledAmbiguousTextFailureSequence; /** Highest ambiguous text generation for which Ctrl+C was actually attempted. */ private attemptedAmbiguousTextRecoverySequence; /** A recovery Ctrl+C may have landed, so retrying or appending is unsafe. */ private ambiguousTextRecoveryUnconfirmed; /** Recovery must leave bracketed-paste mode before injecting Ctrl+C. */ private ambiguousTextRecoveryNeedsPasteClose; /** Worker-level transaction spanning all adapter chunks and its submit key. */ private activeSubmissionFence; private activeSubmissionGeneration; private activeSubmissionTextTouched; private activeSubmissionTransportFailed; private activeSubmissionControlAccepted; /** Completion-time timestamp: a timed-out Ctrl+C may have landed just before return. */ private lastInjectedCancelAtMs; claudeJsonlPath?: string; cliPid?: number; cliCwd?: string; constructor(sessionName: string, opts?: { ownsSession?: boolean; isReattach?: boolean; sessionId?: string; recoveryStateDir?: string; }); static isAvailable(): boolean; static sessionName(sessionId: string): string; /** * Pair the authoritative healthy-name surface (`list --short`) with the full * list's `err=` rows. The full command field is not a line protocol (literal * newlines in argv spill onto continuation lines), so it must never be the * sole source of truth for a healthy session name. */ static probeSessions(env?: NodeJS.ProcessEnv): ZmxSessionProbeResult | { ok: false; }; static hasSession(name: string, env?: NodeJS.ProcessEnv): boolean; static probeSession(name: string, env?: NodeJS.ProcessEnv): SessionProbe; /** * Verify that a name still resolves to the botmux-owned transport for the * complete session UUID. The PTY-root PID is sampled on both sides of the label * reads so a same-name replacement cannot be mistaken for the process whose * labels we just inspected. */ static probeManagedSession(name: string, expectedSessionId: string | undefined, env?: NodeJS.ProcessEnv, opts?: { allowGated?: boolean; }): ZmxManagedSessionProbe; static killSession(name: string): void; /** Kill only a session whose full botmux identity is still authoritative. */ static killManagedSession(name: string, expectedSessionId: string, expectedPid?: number, env?: NodeJS.ProcessEnv): void; static listBotmuxSessions(): string[]; static listDetails(): string; get isReattach(): boolean; get lastInjectedCancelAt(): number; captureAmbiguousSubmissionFence(): number; confirmAmbiguousSubmission(fence: number): AmbiguousSubmissionRecoveryFailure | undefined; cancelAmbiguousSubmission(fence: number): AmbiguousSubmissionRecoveryFailure | undefined; /** * Space terminal writes themselves so two valid cancellation debts cannot * become OMP's double-Ctrl+C exit gesture. */ private waitForInjectedCancelCooldown; private hasPendingAmbiguousTextRecovery; private activeSubmissionCanWrite; private hasAmbiguousRecoveryDebt; private composerRecoveryStatePath; private readComposerRecoveryState; private writeComposerRecoveryState; private restoreAmbiguousRecoveryState; /** * Write-ahead arm one text transport generation before `zmx send` can touch * the PTY. A worker killed after the daemon consumes stdin but before the * synchronous client returns will therefore leave `pending` for reattach to * restore as poison instead of trusting a stale `clean` marker. */ private armAmbiguousTextGeneration; private markArmedTextFailure; private markActiveSubmissionUnconfirmed; /** Record that a recovery Ctrl+C is now transport-ambiguous itself. */ private markAmbiguousRecoveryAttempted; /** * Clear only the exact write-ahead generation whose send or recovery was * accepted. Persistence is committed before the in-memory covered marker. */ private markAmbiguousGenerationCovered; private resetAmbiguousRecoveryForFreshSession; /** Start the next terminal-side cooldown after this attempt has settled. */ private noteInjectedCancelAttemptSettled; spawn(bin: string, args: string[], opts: SpawnOpts): void; write(data: string): void; sendText(text: string): boolean; sendSpecialKeys(...keys: string[]): boolean; pasteText(text: string): void; /** * No-op by construction: zmx exposes no leaderless resize primitive. Size is * set only by an attached client's TIOCGWINSZ, and `send` deliberately never * becomes that client (upstream 8ba312d7). Since `createFreshSession` creates * the session with a non-TTY stdio, zmx's `getTerminalSize` fallback applies * and every botmux-owned session runs at a fixed 120x24 until a local * `zmx attach` takes leadership. The CLI therefore wraps its TUI at 120 * columns, which is the width reflected in the history screen we relay. */ resize(_cols: number, _rows: number): void; onData(cb: (data: string) => void): void; onScreenResync(cb: (snapshot: string) => void): void; onExit(cb: (code: number | null, signal: string | null) => void): void; getChildPid(): number | null; /** Best-effort plain-text terminal snapshot supplied by ZMX history. */ captureCurrentScreen(): string; settleCurrentScreen(): Promise; captureViewport(): string; getPaneSize(): null; isPaneAlive(): boolean; /** Detach the read-only observer while leaving the ZMX daemon and CLI alive. */ kill(): void; destroySession(): void; private createFreshSession; private waitForFreshReady; private waitForFreshLaunchPid; /** * Do not report a fresh spawn until the gate has consumed the atomically * published release token. The original child must remain attached to the * frozen PTY root throughout the wait; otherwise a dead gate can leave a * fully labelled, apparently compatible session that silently drops the * argv-baked first prompt on an automatic reattach. */ private waitForFreshReleaseConsumption; private readManagedLaunchPid; /** * Wait until the session reports at least one connected client. * * Deliberately NOT a differential against a pre-tail baseline. zmx's * `clients=` is an aggregate — `main.zig` reports `clients.items.len - 1`, * subtracting only the `zmx list` connection that asked. A user's * `zmx attach` (which this integration actively encourages), and every * transient `zmx list`/`get`/`history`/`send` botmux itself issues, are all * counted identically to our `tail`. A differential therefore produces false * NEGATIVES: a user detaching inside this window keeps the net delta at 0 and * a perfectly healthy session fails to restore with "tail 未能连接". * * `>= 1` inverts the error direction. The residual false POSITIVE — someone * else holds a client while our tail failed — is cheap and self-correcting: * tail is only a wakeup signal (never a byte source), the authoritative * screen comes from `history` polling which does not need tail at all, and * `scheduleTailRecovery` reconnects a dead observer. Blocking a restore is * the far more expensive mistake. * * Note this cannot instead watch our own child: the wait is synchronous * (`sleepSync`), so the child's 'error'/'close' callbacks cannot run until it * returns. Session identity is still verified below. */ private waitForTailClient; private stampProtocolLabels; private verifyBackingIdentity; /** Full list sampling is reserved for lifecycle edges that need client count. * Hot send/history paths use target-scoped labels + the frozen PTY-root PID, * so one unrelated unhealthy ZMX socket cannot freeze every session. */ private verifyBackingIdentityWithClients; /** Fast generation check for high-frequency read-only snapshots. */ private verifyBackingGeneration; private isBackingProcessAlive; private rejectBackingReplacement; private labelsMatchBacking; private readLabelsAsync; private captureHistoryFileAsync; private readHistorySnapshotAsync; private requestHistoryCapture; private runHistoryCapture; private publishHistorySnapshot; private stopHistoryPolling; private resolveHistorySettleWaiters; private emitScreenResync; private sendBytes; private abortPartialSend; private startTail; private scheduleTailRecovery; private stopTailAfterLaunchFailure; private stopTailForRecovery; private clearReconnectTimer; private clearStableTailTimer; private emitData; private fireExit; } export declare function buildFreshAttachArgs(sessionName: string, bootstrapPath: string): string[]; /** Render the private bootstrap and payload used by a fresh session. */ export declare function buildZmxLaunchFiles(bin: string, args: string[], opts: SpawnOpts, payloadPath: string, readyPath: string, readyNonce: string, releasePath: string, releaseToken: string): { bootstrap: string; payload: string; }; /** * Env for the one-shot client that CREATES a fresh session. Unlike the * node-pty backends (which force TERM via `name: 'xterm-256color'`), zmx sets * no TERM of its own: the forkpty child inherits the create client's env * verbatim, and the daemon/worker env arrives here with TERM scrubbed at the * pm2 boundary (INVOKER_TERMINAL_ENV_KEYS). Left absent, every CLI in the * session fails supports-color detection and renders colorless — pin the same * constant every other backend PTY already forces. Control clients (get/set/ * list/kill) and a user's own `zmx attach` from a real terminal are untouched. */ export declare function zmxFreshSessionEnv(opts: SpawnOpts): NodeJS.ProcessEnv; /** Strip every payload-delivered key from ZMX control subprocesses. */ export declare function zmxControlEnv(opts: SpawnOpts): NodeJS.ProcessEnv; export declare function parseZmxList(output: string): { sessions: string[]; unhealthySessions: string[]; malformedLines: string[]; }; export declare function parseZmxShortList(output: string): { sessions: string[]; malformedLines: string[]; }; export declare function findSessionPid(sessionName: string): number | null; export declare function tmuxKeyToBytes(key: string): string; export {}; //# sourceMappingURL=zmx-backend.d.ts.map