import type { Logger, Role, Channel } from './types.js';
import { type ProcessSignals, type SystemctlRunner } from './systemd-scope.js';
export type RowState = 'live' | 'ended' | 'archived';
/** Task 250 — `detached` is reached only via the boot-seed reconciler.
 *  When the manager restarts and finds a surviving `claude-session-*.scope`
 *  unit whose PID file is on disk, it reattaches the row but cannot read
 *  PTY bytes, inject keystrokes, or kill via the master fd (the manager no
 *  longer owns it). The detached row is observation-only on the manager
 *  side; the operator's claude.ai/code URL still works because Claude
 *  Code's remote-control channel is an outbound WebSocket independent of
 *  the local PTY. End-via-systemctl-stop still works because the scope
 *  unit name is recoverable. */
export type RowStatus = 'idle' | 'busy' | 'gone' | 'detached';
export interface SessionRow {
    /** Intrinsic Claude session id (UUID). Canonical key. For a top-level
     *  jsonl row, this is the file basename. For a pre-first-turn live row,
     *  this is the sessionId from the PID file. */
    sessionId: string;
    /** Full bridge session id ("session_<suffix>") from PID file, or null. */
    bridgeSessionId: string | null;
    /** Suffix only (no "session_" prefix). Derived from bridgeSessionId. */
    bridgeSuffix: string | null;
    /** Specialist agent name from PID file (`--agent <name>`), or null.
     *  Always null on archived rows — PID file is gone by then. */
    agent: string | null;
    /** Specialist agent name cached from the JSONL `agent-setting`
     *  record (read once at row-archive transition or boot-seed of an
     *  archived row). Null when the JSONL has no `agent-setting` record
     *  (operator-driven session). UI's resolveAgentLabel falls back to
     *  this when `agent` is null. */
    cachedAgentSetting: string | null;
    cwd: string | null;
    pid: number | null;
    pidFilePath: string | null;
    state: RowState;
    status: RowStatus;
    /** ms epoch from PID file `updatedAt`/`startedAt`, or 0 if unknown. */
    updatedAt: number;
    /** Task 250 — systemd scope unit token, set by boot-seed when the row
     *  matches a surviving `claude-session-*.scope`. Null for archived rows
     *  and for live rows whose owning manager process is the current
     *  process (those use PtyTracker.scopeUnitToken in pty-spawner's
     *  module-scoped livePtys map). When set on a SessionRow without a
     *  matching tracker, the row is a detached survivor whose End button
     *  should go through this unit token rather than the (missing)
     *  PtyHandle. */
    scopeUnitToken: string | null;
    /** From sidecar. Null when no sidecar on disk for this row. */
    senderId: string | null;
    /** From sidecar. Null when no sidecar on disk for this row. */
    role: Role | null;
    /** From sidecar. Null when no sidecar on disk for this row. */
    channel: Channel | null;
    /** From sidecar; updated by url-capture when the banner lands. Null
     *  for headless spawns and for rows whose live PTY exited before the
     *  banner fired. */
    url: string | null;
    /** From sidecar. ISO 8601 timestamp captured at /spawn. */
    startedAt: string | null;
    /** From sidecar. Null when not requested. Semantically: the mode the
     *  operator picked at spawn. Compare against `effectivePermissionMode`
     *  to detect a runtime downgrade (Task 278). */
    permissionMode: string | null;
    /** Task 278 — from sidecar. The binary's actual runtime mode, derived
     *  by `permission-mode-tail.ts` from the `--debug=auto-mode` capture
     *  in `<sessionId>.permission-mode.log`. Null at spawn until the first
     *  `[auto-mode] verifyAutoModeGateAccess` line resolves. Stays null
     *  forever on non-auto spawns (no signal source for `acceptEdits` /
     *  `plan` / `bypassPermissions` on 2.1.150). When non-null and
     *  `!== permissionMode`, `SessionMetadataPane` renders a downgrade
     *  warning. */
    effectivePermissionMode: string | null;
    /** Task 260 — `hidden` from the sidecar. Defaults to `false` when no
     *  sidecar on disk. */
    hidden: boolean;
    /** From sidecar. Null when no sidecar on disk OR when the spawn was
     *  not a specialist. The `agent` field above is the in-process view
     *  (PID-file `--agent <name>`); this is the request as recorded at
     *  /spawn. They typically agree; divergence flags either a stale
     *  sidecar or a sidecar-less row. */
    sidecarSpecialist: string | null;
}
export interface WatcherEvent {
    kind: 'create' | 'modify' | 'delete';
    source: 'pid' | 'jsonl';
    row: SessionRow | null;
    sessionId: string;
}
export type Subscriber = (event: WatcherEvent) => void;
export interface FsWatcher {
    bootSeed(): void;
    arm(): void;
    subscribe(sub: Subscriber): () => void;
    getBySessionId(id: string): SessionRow | undefined;
    getByBridgeSessionId(id: string): SessionRow | undefined;
    getByBridgeSuffix(suffix: string): SessionRow | undefined;
    getByPid(pid: number): SessionRow | undefined;
    all(): SessionRow[];
    /** Resolve once a PID-file event lands for `pid`. Rejects on `timeoutMs`. */
    waitForPid(pid: number, timeoutMs: number): Promise<SessionRow>;
    /** Task 476 — test seam used by the firing-watcher mock so its synthetic
     *  row carries the same sessionId the spawner just minted. Production
     *  fs-watcher ignores the call: it reads claude's PID file as the
     *  source of truth and the spawner asserts equality against this same
     *  minted UUID. Mismatch is a spawn failure either way; this hook only
     *  exists so test mocks don't have to harvest `--session-id` from
     *  argv to align with the assertion. */
    registerPidSessionId?(pid: number, sessionId: string): void;
    /** Task 260 — re-read the sidecar for `sessionId` and re-emit a
     *  `modify` event. fs.watch does not fire for `<sid>.meta.json` writes
     *  (the watcher only watches `*.jsonl` and PID files), so this is the
     *  trigger for the http-server's url-capture path: pty-spawner writes
     *  the URL to the sidecar, then calls this to refresh the row index
     *  and emit a row-updated SSE event.
     *
     *  Returns true when the row exists (the refresh fired); false when
     *  no row matches the sessionId (caller may choose to log; the
     *  url-capture path early-returns on null). */
    refreshSidecar(sessionId: string): boolean;
    close(): void;
    /** Test seam: drive reconciliation directly, bypassing fs.watch + debounce.
     *  Production code uses the same closures via the watch callback; the seam
     *  exists because fs.watch on macOS coalesces tight create/delete bursts in
     *  vitest, leaving live-event tests timing-dependent. Tests call these
     *  after performing the same fs mutation a real session would emit. */
    __internal: {
        reconcilePidFile(path: string): void;
        reconcileJsonlFile(path: string): void;
    };
}
export declare function createFsWatcher(opts: {
    sessionsDir: string;
    projectsDir: string;
    logger: Logger;
    /** Task 250 — systemctl runner the boot-seed uses to discover surviving
     *  `claude-session-*.scope` units. Default issues real spawnSync calls;
     *  tests inject a recording/scripted stub. When the env has no systemd
     *  user manager (macOS dev hosts), the default runner returns non-zero
     *  exit codes and the boot-seed degrades to "0 survivors" gracefully. */
    runSystemctl?: SystemctlRunner;
    /** Task 451 — injectable kill/alive-check primitives for the boot-seed
     *  orphan-stop post-flight verify. Default delegates to `process.kill`.
     *  Tests inject a recording stub to assert SIGTERM/SIGKILL escalation. */
    processSignals?: ProcessSignals;
    /** Task 451 — grace period between SIGTERM and SIGKILL during boot-seed
     *  orphan-stop escalation. Default 5_000 ms (matches the scope's
     *  `TimeoutStopSec`). Tests pass small values to keep run time bounded. */
    killGraceMs?: number;
}): FsWatcher;
//# sourceMappingURL=fs-watcher.d.ts.map