import type { CloudTaskStatus } from '../cloud/types.js';
import { type PidSessionEntry } from './pid-registry.js';
import { type HookSessionIndex } from './hook-sessions.js';
import { type UserPromptKind } from './prompt.js';
import { type SessionState, type SessionActivity, type AwaitingReason, type StructuredQuestion, type TodoProgress, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
import { type SessionAgentId, type SessionAttachment, type SessionMeta } from './types.js';
import { type SessionProvenance } from './provenance.js';
import { type DeviceRegistry } from '../devices/registry.js';
import { type Presence } from './detached.js';
import { type HostLink } from './host-link.js';
/** Prefer live actor attribution; the durable sidecar survives actor-less hook rewrites. */
export declare function resolveOwner(pidActor: string | null | undefined, sessionId: string | undefined): string | undefined;
/**
 * Per-PID `lsof` probes run bounded and staggered rather than as one parallel
 * fan-out: a simultaneous system-wide `lsof` burst reads to behavioral EDR
 * (CrowdStrike Falcon) as lateral-movement recon. Results are identical — the
 * cwds are just gathered at a bounded spawn rate instead of a single burst.
 */
export declare const LSOF_CONCURRENCY = 4;
/**
 * Process-local process-table memo (#2047). One `getActiveSessions` scan can
 * call `ps -A` from the terminal path, the headless path, and host ancestry —
 * without this, each is a full process-table snapshot. 5s is well under the
 * ~10–30s menu-bar / `--active` poll so a quiet re-poll usually reuses the
 * snapshot; a brand-new agent pid still appears on the next full refresh.
 */
export declare const PROCESS_TABLE_FRESH_MS = 5000;
/**
 * How long {@link listUnattributedActive} may reuse its last full `ps`+`lsof`
 * result (#2047). Between full rescans the previous rows are re-emitted after
 * dropping dead PIDs and PIDs that are now attributed. A *shrinking* attributed
 * set forces a rescan so a process that just left teams/terminals can reappear
 * as headless instead of vanishing until the next cold poll.
 */
export declare const UNATTRIBUTED_RESCAN_MS = 15000;
/** Test seam: override the clock used by process-table / unattributed memos. */
export declare function setActiveScanClockForTest(fn?: () => number): void;
/** Test seam: drop the process-table + unattributed memos and their counters. */
export declare function clearActiveScanCachesForTest(): void;
/** Test seam: how many times the live `ps` / CIM path actually ran. */
export declare function processTableLiveReadCountForTest(): number;
/** Test seam: how many full unattributed (ps + lsof) rescans ran. */
export declare function unattributedFullRescanCountForTest(): number;
/**
 * True when any pid in `prev` is absent from `next` — the attributed set
 * shrank, so a process may need to reappear as unattributed. Pure for tests.
 */
export declare function attributedSetLostPids(prev: Set<number>, next: Set<number>): boolean;
/**
 * Drop rows whose pid is now attributed or no longer alive. Pure over the
 * inputs so the unattributed TTL reuse path is unit-testable without a live
 * process table. `alive` receives the row's `startedAtMs` so callers can apply
 * the same pid-reuse guard {@link isPidAlive} uses elsewhere.
 */
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'firstUserMessage' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness' | 'tokenCount' | 'durationMs' | 'subAgentCount'>;
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
export declare function isRunningLiveSession(s: ActiveSession): boolean;
export declare function activeSessionProjectKey(s: Pick<ActiveSession, 'cwd' | 'context'>): string;
export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<Omit<ActiveSession, 'viewingIn'> & {
    ticketId: string | null;
    project: string;
    prLink: string | null;
    viewingIn: string | null;
}>;
export declare function serializeSessionsJson(sessions: SessionMeta[]): string;
/**
 * Every status here is COMPUTED by the framework from observable signals — PID
 * liveness and transcript last-write (mtime) — never self-reported by the agent.
 *
 *   - `running` / `idle` / `input_required` — a live process's working / stopped
 *     / waiting-on-you activity, from its transcript (see {@link computeLiveSignals}).
 *   - `queued` — dispatched, work in the pipeline (cloud/headless launch).
 *   - `closed` — the PID is dead. The process has exited; report that, don't
 *     fabricate `idle` ("done, waiting for you") for a process that is simply gone.
 *   - `abandoned` — no transcript write in {@link ABANDONED_STALE_MS} (days): the
 *     session is dangling, whether its PID is dead (long gone) or still alive but
 *     stuck making no progress. A soft signal — it clears the moment it writes again.
 *   - `unknown` — the residual: genuinely NO signal (no PID info AND no file).
 *
 * A LIVE, fresh process is never `unknown` — "the process is alive" is itself a
 * positive signal, so an opaque/unparseable live harness resolves to `running`
 * (its honest floor), and every tracked harness with a locatable, parseable
 * transcript (claude, codex, grok, droid, rush, gemini, kimi, hermes, opencode,
 * antigravity) gets a real working/waiting/idle from its own parser — see
 * {@link computeLiveSignals}, {@link lifecycleStatus} and {@link resolveFallbackStatus}.
 */
export type ActiveStatus = 'running' | 'idle' | 'queued' | 'input_required' | 'closed' | 'abandoned'
/** Alive, but no client is attached — the host window died and the agent outlived it. */
 | 'orphaned'
/** The host window died and took the agent with it — an unclean exit, not a normal close. */
 | 'crashed' | 'unknown';
/**
 * Coarse lifecycle bucket a UI groups a row by, projected once at the source from
 * {@link ActiveStatus} so consumers stop re-deriving it (PHNX-2484). The vocabulary
 * is deliberately small — five terminal-agnostic buckets — so the AGI EXT Fleet panel
 * reads `phase` straight off the `sessions watch --json` row instead of mirroring the
 * CLI status word onto its own enum (the ext's `mapStatusToPhase`, now deleted):
 *   `running` — dispatched or actively working;
 *   `waiting` — blocked on the operator (needs-you);
 *   `failed`  — dangling/dead and needing attention (crashed, orphaned, abandoned);
 *   `done`    — the process exited cleanly;
 *   `idle`    — alive but quiet, or an unclassified state.
 */
export type SessionPhase = 'running' | 'waiting' | 'failed' | 'done' | 'idle';
/**
 * Which rung of the recap ladder produced a row's shown {@link ActiveSession.title}
 * (RUSH-3011), best-first:
 *   - `label`  — a `/rename` or harness-set label (incl. an agent-generated
 *                title, which lands in `label`); always wins.
 *   - `last`   — the last assistant line from the transcript tail; agent-derived.
 *   - `prompt` — the first-user-prompt topic; the last-resort fallback.
 */
export type RecapSource = 'label' | 'last' | 'prompt';
export interface ActiveSession {
    context: ActiveContext;
    kind: string;
    /**
     * Custom harness / profile name when this live process was launched via
     * `agents run <profile>` (e.g. `deepseek`). `kind` stays the HOST process
     * (claude) so transcript lookup and live-signal parsers keep working.
     * `sessions --active` displays this when set (PHNX-2935).
     */
    harness?: string;
    /** Specific host app — 'code', 'cursor', 'codium', 'iterm', 'terminal', 'warp', 'tmux', etc. */
    host?: string;
    pid?: number;
    sessionId?: string;
    cwd?: string;
    /** Project/repo key derived from cwd, when known. */
    project?: string | null;
    /** User-given name from /rename command. */
    label?: string;
    /** Durable `agents run --name` launch handle, when the run was named. */
    name?: string;
    /** First meaningful line of the initial prompt (extracted topic). */
    topic?: string;
    /** Full, cleaned first genuine user turn, backfilled from the session index. */
    firstUserMessage?: string;
    /**
     * The row's shown title — WHAT the session is, best-source-wins (RUSH-3011).
     * The ladder ({@link deriveSessionRecap}): a `/rename` or harness `label` →
     * the last agent line (`lastAgentLine`) → the first-prompt `topic`. A session
     * whose agent did work shows an agent-derived line, not the stale first
     * prompt. Folded on at the end of {@link getActiveSessions}; `recapSource`
     * names which rung produced it.
     */
    title?: string;
    /** Which ladder rung produced {@link title}. */
    recapSource?: RecapSource;
    /**
     * The first user turn cleaned for a "You" line — a screenshot path folds to
     * `[image]`, a pasted `$ cmd` to the command, a `/skill` install path to
     * `/<name>`, so path noise never shows. See {@link classifyUserPrompt}.
     */
    userPromptClean?: string;
    /** What kind of first turn {@link userPromptClean} was. */
    userPromptKind?: UserPromptKind;
    /**
     * The most recent assistant line (from the transcript tail) — the free,
     * always-current signal of what the agent last said/did. Ladder rung 3.
     */
    lastAgentLine?: string;
    /** Live preview: the latest turn (agent message or tool action), from the state engine. */
    preview?: string;
    /** Inferred activity: working / waiting_input / idle (from the transcript tail). */
    activity?: SessionActivity;
    /**
     * Output-token throughput (tokens/sec) over a rolling 60s window, from the
     * transcript tail. The number the Fleet shows next to a running agent;
     * absent when no transcript is resolvable or the agent format reports no usage.
     */
    tokPerSec?: number;
    /** Why the agent is waiting, when activity is waiting_input. */
    awaitingReason?: AwaitingReason;
    /** The structured decision (question/plan/permission + options) the agent is waiting on. */
    question?: StructuredQuestion;
    /**
     * Plan markdown from the last `ExitPlanMode` tool call. Present when the
     * transcript ever entered plan-review; `awaitingReason === 'plan_review'`
     * says whether it is still pending.
     */
    plan?: string;
    /**
     * Live plan progress from the most recent `TodoWrite` (RUSH-1380): the checklist
     * items + a done/total tally + the current step. The Fleet renders this
     * as an N/M pill + checklist for every session — including remote and
     * device-dispatched agents that have no local tool-call stream to parse.
     */
    todos?: TodoProgress;
    /** Last few assistant turns (most-recent last), for at-a-glance context in the UI. */
    tail?: string[];
    /** PR opened during the session. */
    pr?: DetectedPr;
    /** Worktree the session runs in. */
    worktree?: DetectedWorktree;
    /** Tracker ticket the session is tied to. */
    ticket?: DetectedTicket;
    /** Per-session rate/usage limit detected in the transcript (RUSH-1523). */
    rateLimited?: boolean;
    /** Tracker refs the session CREATED (Linear create_issue / gh issue create). */
    createdTickets?: string[];
    /** Team name the session SPAWNED via `agents teams create/add`. */
    spawnedTeam?: string;
    /** Files/screenshots attached to the session prompt. */
    attachments?: SessionAttachment[];
    /** Total tokens and elapsed duration from the indexed transcript. */
    tokenCount?: number;
    durationMs?: number;
    /** Number of subagents launched, persisted by the index scanner. */
    subAgentCount?: number;
    /** Durable documents created in the live transcript tail. */
    artifacts?: import('./highlights.js').ProducedArtifact[];
    /** Created plan document singled out for plan-first consumers. */
    planFile?: string;
    sessionFile?: string;
    startedAtMs?: number;
    /**
     * Agent version (e.g. `2.1.207`) for the row's `agent version` cell. Not a
     * live-scan signal — a running process does not report its own semver — so it
     * is backfilled at render time from the indexed {@link SessionMeta} by session
     * id (RUSH-2205), never asserted by a source.
     */
    version?: string;
    /**
     * Email of the account that produced the session (display-only). Like
     * {@link version}, a running process does not report which account a
     * `--strategy balanced` launch selected, so it is backfilled at render time
     * from the indexed {@link SessionMeta} by session id (PHNX-3184). This is what
     * the AGI EXT status bar renders as the session's account — it reads it off the
     * `sessions watch --json` row instead of spawning a per-tab `agents sessions
     * <id> --device <host> --json` (the 2026-08-25 CPU incident, agi-cli#3019).
     * Never group on this — two orgs can share one email; group on the index's
     * `accountKey`.
     */
    account?: string;
    /**
     * Last-activity epoch — the transcript's last write (mtime). Distinct from
     * {@link startedAtMs} (session START): a session begun 3h ago but last touched
     * 20s ago has an old start and a fresh last-activity. The Floor renders "Xs ago"
     * off this so an idle-but-old session doesn't read as freshly active.
     */
    lastActivityMs?: number;
    status: ActiveStatus;
    /**
     * Coarse lifecycle bucket derived once from {@link status} (see {@link SessionPhase}).
     * Folded on at the end of {@link getActiveSessions} by {@link foldPhase}, AFTER
     * {@link foldHostLink} has finalized `status` — so `orphaned`/`crashed` land in the
     * `failed` bucket rather than being re-derived (and mis-bucketed as `idle`) downstream.
     */
    phase?: SessionPhase;
    /** Indexed launch origin, backfilled by the sessions command for JSON consumers. */
    origin?: 'cli' | 'routine';
    /** Routine definition name when origin is `routine`. */
    routineName?: string;
    /**
     * Foreground/background presence for the detach/attach model:
     *   `attached`   — live interactive TUI you're watching;
     *   `background` — detached: running headless, unattended (via `agents sessions detach`);
     *   `parked`     — the headless continuation has exited; the transcript is durable.
     * Absent for ad-hoc headless runs and cloud/team rows, which aren't on the
     * foreground/background axis. Folded on at the end of {@link getActiveSessions}
     * from the detach store — never asserted by a source.
     */
    presence?: Presence;
    /**
     * Whether anything is still on the other end of this session — folded on at the
     * end of {@link getActiveSessions} by {@link foldHostLink} from the raw signals
     * below, never asserted by a source. Drives the `orphaned` / `crashed` statuses.
     */
    hostLink?: HostLink;
    /**
     * Whether this session's process was alive at scan time — the boolean
     * {@link applyState} already computes, kept rather than thrown away.
     *
     * `status` cannot stand in for it. `abandoned` fires on transcript staleness
     * BEFORE the liveness check, so it covers a live-but-stuck process as well as
     * a long-dead one; only `closed`/`crashed` are unconditionally dead. A consumer
     * that must tell "still there, just quiet" from "gone" needs this, not the
     * status. Absent from cloud rows (no pid) and from a peer running an older CLI.
     */
    pidAlive?: boolean;
    /**
     * Clients attached to this session's tmux session (`#{session_attached}`), for
     * a tmux-hosted row. Absent — NOT zero — when the session is not tmux-hosted:
     * zero means "tmux says nobody is looking", absent means "we cannot tell".
     */
    tmuxClients?: number;
    /**
     * When the owning IDE window last refreshed its slice of the live-terminals
     * registry. Absent for a session no IDE window owns. A stale value means that
     * window is gone — see {@link HOST_HEARTBEAT_STALE_MS}.
     */
    windowHeartbeatMs?: number;
    /** How many live PIDs resolve to this same session (subagents/forks). 1 unless collapsed. */
    pidCount?: number;
    /**
     * Where the process actually lives — machine host, local vs SSH, tmux pane,
     * and whether a rail exists to type back into it. Read from the process env
     * (`/proc/<pid>/environ` on Linux, `ps eww` on macOS) during enrichment.
     * Absent for cloud sessions (no local pid) and any pid whose env is unreadable.
     */
    provenance?: SessionProvenance;
    /**
     * Who initiated this session — the resolved actor id stamped at spawn
     * (`resolveActor().id`, read back from the pid registry / teammate record).
     * A tailnet login/email for a resolved human, `UNRESOLVED@<host>` when it
     * couldn't be determined, absent when the launch predates actor stamping.
     * Surfaced as the owner column in `--active` (RUSH-2018).
     */
    owner?: string;
    /**
     * The machine this session runs on, as a normalized device id (machineId()
     * form). Set when merging cross-machine results so the grouped `--active`
     * view can bucket by computer. Absent for a purely local query (the renderer
     * falls back to provenance.host, then the local machine).
     *
     * For a host-dispatched run this is the EXECUTION host, not the box holding
     * the live shim process — see {@link foldExecutionMachine}.
     */
    machine?: string;
    /**
     * Set only on the dispatching box's own row for a host-dispatched run
     * (`agents run --device <peer>`): the live process here is the ssh/TTY shim,
     * while the agent itself runs on {@link machine}. Names the box the dispatch
     * was issued from, so a merged fleet view can prefer the executing machine's
     * own richer row over this shim (see `dedupeByMachineSession`).
     */
    offloadedFrom?: string;
    teamName?: string;
    /**
     * For a teams teammate: the session id of the ORCHESTRATOR that spawned the
     * team (the agent that ran `agents teams add`, captured from AGENTS_SESSION_ID
     * at spawn). Lets the listing answer "which session spun up this team" and
     * group teammates under their orchestrator. Distinct from `sessionId`, which is
     * the teammate's OWN transcript.
     */
    orchestratorSessionId?: string;
    /** Display label for the orchestrator (its topic/label), resolved when the
     * orchestrator is itself present in the active set. Display-only. */
    orchestratorLabel?: string;
    /**
     * For a teams teammate: a one-line summary of the mission it was spawned with
     * (the `prompt` stored on the teammate record — the team's task/target), so the
     * listing answers "what is this team working on", not just its name. Survives
     * before the teammate has produced any transcript (a pending/staged teammate
     * still shows its target). Distinct from `topic`, which is derived from the
     * teammate's own transcript once it starts.
     */
    assignedTask?: string;
    agentId?: string;
    cloudProvider?: string;
    cloudTaskId?: string;
    cloudStatus?: string;
    /**
     * IDE window that owns this terminal. Source of truth is the per-window
     * slice key in `live-terminals.json` (computeWindowId in the swarmify
     * extension): `${vscode.env.sessionId}-${extension-host pid}`. Lets the
     * renderer cluster terminals that belong to the same IDE window even when
     * two windows have the same cwd open. Only populated for `terminal` context.
     */
    windowId?: string;
    /**
     * Controlling TTY of the agent process (e.g. 'ttys003'), from the `ps -A`
     * read. macOS/Linux terminal sessions only; '??'/none normalized to undefined.
     * A disambiguation bridge (and the basis for future terminal addressing).
     */
    tty?: string;
    /**
     * Ghostty tab index (1-based) the session is shown in, when it can be matched
     * to a Ghostty surface by working directory (+ title). Transient, populated by
     * the renderer just before printing — NOT part of the pure discovery path.
     */
    ghosttyTab?: number;
    /**
     * Resolved tmux attach target (`session:window.pane`) for a tmux-hosted local
     * session, from the pane id via `mapPanesToTargets`. Transient, renderer-set
     * (after the --json/--waiting gates) — NOT emitted on the discovery path.
     */
    tmuxTarget?: string;
    /**
     * Which host app + tab a tmux-hosted session is currently being VIEWED in,
     * resolved from the attached tmux client (its terminal PID -> app via
     * HOST_MATCHERS, its tab via the per-app resolver). `undefined` means no
     * client is attached — the session is running detached. Transient,
     * renderer-set (see src/lib/session/viewing-in.ts) — NOT on the discovery path.
     */
    viewingIn?: {
        app: string;
        tab?: number;
    };
    /**
     * The editor tab that launched this agent (`AGENT_TERMINAL_ID`), from the pid
     * registry. This is the one identifier that survives an SSH hop AND a session
     * rotation: a Factory tab offloaded to a device has no local process to inspect,
     * and its spawn-time session id goes stale the moment the agent moves to another
     * session (`/clear`, exit-and-rerun), so `--active --device <device>` joined on
     * this is how that tab re-identifies its own session. Absent for any launch that
     * did not inherit a terminal id.
     */
    terminalId?: string;
    /**
     * The launch id (`AGENT_LAUNCH_ID`) the CLI stamps on every agent at spawn — a
     * stable UUID that is identical locally and across an SSH hop and survives a
     * session-id rotation (`/clear`, exit-and-rerun). Unlike `sessionId` (which the
     * non-Claude harnesses only mint after boot) it exists from the first tick, so
     * it is the join key a client uses to re-identify a session on the watch stream.
     * Populated wherever the by-pid launch registry resolves — reliably for
     * `agents run`-launched processes; still absent for editor-launched terminals
     * whose shell pid does not line up with the agent-pid registry today (the same
     * limitation the `readPidSessionEntry` call in `listTerminalsActive` documents).
     */
    launchId?: string;
    /**
     * tmux pane id (`%N`) when this row was discovered via the tmux source AND its
     * session id could not be resolved (a born-unidentifiable non-Claude pane). It
     * is the dedupe key for such id-less rows, so two anonymous panes in the same
     * cwd render as two distinct rows instead of collapsing onto each other. Unset
     * once a session id resolves (the id is the identity then).
     */
    paneId?: string;
    /**
     * The `ag-<agent>-<shortid>` tmux session name this row was discovered under,
     * when it came from the tmux source. Kept even when the full session id could
     * NOT be resolved (the row's `sessionId` is then absent), because the name's
     * `<shortid>` suffix is the only stable selector such a row exposes — it is
     * what `agents sessions --active` prints as the tmux target and what
     * `agents sessions inject <shortid>` matches on to nudge an id-less remote
     * session (PHNX-3688). Absent for non-tmux rows.
     */
    tmuxName?: string;
    /**
     * Daemon-computed 1–2 line goal from the session's first user turn (PHNX-3939).
     * Produced off the request path by the background `SessionSummarizerService`
     * and merged from the `session_summaries` cache in {@link applyImmutableMemo};
     * rides the `sessions watch` stream free via the `...row` spread.
     */
    goal?: string;
    /** Daemon-computed progress checkpoints, newest last (PHNX-3939). */
    checkpoints?: import('./types.js').SessionCheckpoint[];
    /** Daemon-computed detailed checklist for the session (PHNX-3939). */
    summaryChecklist?: import('./types.js').SessionChecklistItem[];
    /** Lifecycle of the daemon-computed summary; `skipped` when disabled (PHNX-3939). */
    summaryState?: import('./types.js').SummaryState;
}
export declare function activeStatusFromCloudStatus(status: CloudTaskStatus): ActiveStatus;
interface ActiveQueryOptions {
    /** Skip the `ps` scan for ad-hoc headless agents. */
    skipHeadless?: boolean;
    /**
     * A `--local` query: this machine only. Never dial a remote-host teammate
     * (one dispatched via `agents teams add --device`) over ssh — report its
     * last-persisted meta.json state instead of polling it. RUSH-2118: without
     * this, `agents sessions --active --local` still fired one ssh round-trip
     * per remote-host teammate (finished or not) on every call.
     */
    localOnly?: boolean;
}
/**
 * Thrown by {@link listTmuxAgentSessions} when the socket exists (a server has
 * run) but tmux could not be asked for its truth — spawn failure, timeout, or a
 * nonzero exit. Distinct from a missing socket, which means no server ever ran
 * and is genuinely empty, not degraded. `getActiveSessions` still swallows this
 * to `[]` so its return type stays a plain session array, but
 * {@link describeActiveDiscoveryHealth} re-probes and surfaces it so the empty
 * case a real fleet sweep found (RUSH-2507) reads differently from a clean one.
 */
export declare class TmuxDiscoveryDegradedError extends Error {
    constructor(message: string);
}
/**
 * A session whose transcript hasn't been written in this long is ABANDONED /
 * dangling — the framework can no longer treat it as live work, whether its PID
 * is dead (long gone) or still alive but making no progress (a hung agent).
 * Two days: past a normal work gap, well short of a session legitimately kept
 * open across a long weekend. Deliberately far wider than
 * {@link ACTIVE_SESSION_STALE_MS} (which bounds transcript-borrowing, a different
 * concern) — this is the lifecycle threshold, not the freshness window.
 */
export declare const ABANDONED_STALE_MS: number;
/**
 * Every process executable ("comm") name that identifies a given session-agent
 * kind in the headless `ps`-scan. Driven off the AGENTS registry's `cliCommand`
 * (the real executable name — e.g. `agy` for antigravity, `cursor-agent` for
 * cursor) plus {@link EXTRA_SESSION_AGENT_COMMS} for members with no registry
 * row. Exported so the completeness test can assert every {@link SESSION_AGENTS}
 * member resolves — the fix for the harness-parity gap where bare-headless
 * grok/kimi/antigravity/openclaw/hermes/rush were silently dropped.
 */
export declare function sessionAgentComms(id: SessionAgentId): string[];
/**
 * Resolve an agent kind from a process's reported executable. `comm` may be an
 * absolute path (shim-launched agents), and Windows image names carry an
 * `.exe` suffix (`claude.exe`), so basename + suffix-strip before the lookup.
 */
export declare function agentKindFromComm(commRaw: string): string | undefined;
/** Agent kind from an `ag-<agent>-<shortid>` tmux session name, else undefined. */
export declare function agentKindFromName(sessName: string): string | undefined;
/** The 8-char session-id prefix from an `ag-<agent>-<shortid>` name, else undefined. */
export declare function shortIdFromName(sessName: string): string | undefined;
/**
 * Map every `ag-<agent>-<shortid>` tmux session name to its full session UUID in
 * ONE batched DB lookup. The live scan calls this once per poll (not per pane),
 * then resolvePaneIdentity reads the map — the recovery that makes a detached
 * agent findable by `focus <id>` even when its durable identity records are gone.
 * `findSessionsByShortIds` is injected so this stays unit-testable without a DB.
 */
export declare function resolveNamesToSessionIds(sessionNames: string[], deps: {
    findSessionsByShortIds: (shortIds: string[]) => Map<string, {
        id: string;
    }>;
}): Map<string, string>;
/**
 * True when `pid` names a live process AND — when a session's recorded
 * `startedAtMs` is supplied — that process is plausibly the SAME one, not a later
 * process that recycled the pid. The OS reuses pids, so a bare
 * `process.kill(pid, 0)` existence check reports a dead session as alive (a
 * "zombie") once its pid is handed to an unrelated process. A genuine session
 * process starts at or before its own recorded start, so a process that began
 * meaningfully AFTER `startedAtMs` is a reused pid and the session is dead. When
 * the start time can't be read, we keep the existence answer.
 */
export declare function isPidAlive(pid: number, startedAtMs?: number): boolean;
/**
 * Resolve a Claude transcript for `cwd` across the given project roots, newest
 * mtime winning when the same id/cwd resolves in more than one version home
 * (the actively-written copy is the newest). Pure over `projectRoots`, so it is
 * testable against temp dirs without touching the real home directory.
 */
export declare function pickClaudeSessionFileAcrossRoots(projectRoots: string[], cwd: string, sessionId?: string): string | undefined;
/**
 * Pick a Claude transcript file within a project dir.
 *
 * With a CONCRETE session id: return that id's `<id>.jsonl` or undefined — NEVER a
 * sibling's. Falling back to the newest file here is the bug that made N distinct
 * co-located sessions (e.g. several editor tabs in one cwd, or two worktree siblings)
 * all collapse onto ONE file and render identical preview + topic (they look like
 * duplicate cards). The mtime fallback is only sound when NO id is known.
 *
 * With NO id: return the newest `.jsonl` by mtime (the legitimate single-session
 * heuristic for a directly-launched agent with no registry entry).
 */
export declare function pickSessionFile(projectDir: string, sessionId?: string): string | undefined;
/**
 * One `stat` → the transcript's creation (≈ session start) and last-write (≈ last
 * activity) epochs. Both `undefined` when the file can't be stat'd (vanished /
 * unresolved). `birthtimeMs` can be 0 on filesystems without creation time — coerce
 * that to `undefined` so callers fall through to a real signal instead of epoch 0.
 */
export declare function sessionFileTimes(sessionFile: string | undefined): {
    birthtimeMs?: number;
    mtimeMs?: number;
};
/**
 * The lifecycle status computed purely from the two hard signals the framework
 * always has — PID liveness and transcript last-write (mtime) — independent of
 * anything the agent self-reports or what its last transcript turn happened to
 * look like. Returns the definitive lifecycle label when one applies, or
 * `undefined` when the process is live and fresh (so the caller uses the richer
 * activity-derived status instead).
 *
 *   - No write in {@link ABANDONED_STALE_MS} ⇒ `abandoned` (dangling): a
 *     days-stale session is not live work, whether its PID is dead (long gone)
 *     or still alive but stuck. Checked first — it outranks a bare `closed`.
 *   - Otherwise, PID dead ⇒ `closed`: the process has exited. This is the fix
 *     for the old "dead PID reports idle" lie — `idle` reads as "done, waiting
 *     for you", but a dead process is done, period.
 *   - Otherwise (alive + fresh) ⇒ `undefined`: defer to the activity engine.
 */
export declare function lifecycleStatus(pidAlive: boolean, mtimeMs: number | undefined, nowMs?: number): ActiveStatus | undefined;
/**
 * The ONE place a fallback status is decided when no rich transcript state is
 * available — an opaque kind we cannot parse (openclaw), or a transcript
 * whose parse/tail was empty or unreadable. Honest by construction: computed from
 * PID + mtime, never a fabricated `idle`.
 *
 *   - Dead PID, or a days-stale transcript ⇒ {@link lifecycleStatus} (`closed` /
 *     `abandoned`). A dead process is `closed`, not the old fabricated `idle`
 *     (which the UI reads as "done, waiting for you"); a dead process whose file
 *     also vanished is still `closed` (death is a definitive answer, not `unknown`).
 *   - Live + fresh ⇒ `running`. A live process is, at minimum, running: we may
 *     not see WHAT an opaque harness (or an empty tail) is doing, but the process
 *     being alive is a positive signal — never a blank `unknown` (the old blanket
 *     bug for every non-claude/codex live agent) nor a downgraded `idle`.
 */
export declare function resolveFallbackStatus(sessionFile: string | undefined, pidAlive: boolean, nowMs?: number): ActiveStatus;
/**
 * Locate the live transcript for an agent process. Claude files are keyed by cwd
 * (+ optional session uuid), so they resolve straight off disk. Every OTHER
 * tracked harness — Codex (date-partitioned), plus grok / droid / rush / gemini /
 * kimi / hermes / opencode / antigravity / cursor (per-session dirs, SQLite, single-JSON)
 * — is resolved through the session index. When the session id is KNOWN, that id
 * selects the transcript. Only an id-less process falls back to the newest indexed
 * transcript for that cwd, bounded by ACTIVE_SESSION_STALE_MS so a live pid never
 * borrows a weeks-old transcript. This is what lets a live NON-claude/codex agent
 * get a real status instead of falling through to `unknown` (the file feeds
 * {@link computeLiveSignals}). An opaque kind we don't track still yields undefined
 * here and degrades honestly to a live `running`.
 *
 * The id branch exists because the cwd fallback is only safe when there is nothing
 * better: it answers `WHERE agent = ? AND cwd = ? ORDER BY last_activity DESC LIMIT 1`
 * (`latestSessionFileForCwd`), so with two same-harness agents in ONE cwd — routine on
 * this fleet — every one of them resolves to whichever transcript was touched last.
 * That is the same "one stranger's transcript" hazard the id-less tmux pane already
 * refuses to guess at (see `listTmuxAgentSessions`), and since RUSH-2682 it reaches
 * further than a status badge: the live row now backs `sessions preview <id>`, so the
 * guess renders session B's digest under session A's header and caches it against A.
 * An id we cannot resolve yields undefined — the honest "not indexed here" render —
 * rather than a neighbour's transcript (RUSH-2691).
 */
export declare function findSessionFileForKind(kind: string, cwd?: string, sessionId?: string): string | undefined;
/** Live per-session signals derived from one transcript-tail read. */
interface LiveSignals {
    /** Rich inferred state (activity/preview/badges). Undefined when unreadable. */
    state?: SessionState;
    /** Rolling output-token throughput (tokens/sec). Undefined when no usage in the tail. */
    tokPerSec?: number;
}
/** Test seam: drop the in-process live-signals memo. */
export declare function clearLiveSignalsCacheForTest(): void;
/**
 * Derive the inferred state (working / waiting / idle + preview/badges) and, for
 * the two harnesses whose raw lines carry it, the output-token throughput.
 *
 * Claude/Codex take the fast bounded byte-tail ({@link readSessionTailWithRaw}) —
 * the hot path, and the only two that also yield throughput (their raw lines
 * carry usage the event model drops). EVERY OTHER tracked harness (grok, droid,
 * rush, gemini, kimi, hermes, opencode, antigravity, cursor) is parsed with its own
 * parser and run through the SAME {@link inferSessionState}, so a live
 * non-claude/codex agent gets a real working/waiting/idle instead of the blanket
 * `unknown` it used to fall through to. An opaque/untracked kind or an
 * unreadable/empty transcript yields an empty signal set, and the caller's
 * {@link resolveFallbackStatus} reports the honest live floor (`running`).
 *
 * Unchanged-mtime hits reuse the previous result (see {@link liveSignalsCache})
 * so a 30s active-session poll does not re-tail every quiet transcript.
 */
export declare function computeLiveSignals(kind: string, sessionFile: string | undefined, cwd: string | undefined, pidAlive: boolean): LiveSignals;
/**
 * One-line summary of a teammate's spawn prompt — the team's task/target. Takes
 * the first non-empty line, strips a leading `MISSION:`/`CONTEXT:`/`TASK:` label,
 * and truncates. Exported for tests.
 */
export declare function summarizeMission(prompt: string | null | undefined): string | undefined;
/**
 * Live teams teammates. Reuses AgentManager which already polls PIDs via
 * `kill -0`. `localOnly` (RUSH-2118) skips the ssh round-trip AgentManager
 * would otherwise issue for every distributed (remote-host) teammate.
 */
export declare function listTeamsActive(opts?: {
    localOnly?: boolean;
}): Promise<ActiveSession[]>;
/** Live editor-terminal agents across every IDE window. */
export declare function listTerminalsActive(): Promise<ActiveSession[]>;
interface ProcRow {
    pid: number;
    ppid: number;
    tty?: string;
    comm: string;
    kind?: string;
}
/** Parse `ConvertTo-Csv` output of Win32_Process rows. Exported for tests. */
export declare function parseWin32ProcessCsv(out: string): ProcRow[];
/**
 * Resolve every candidate PID's cwd, bounded and staggered so the probes no
 * longer fan out as one simultaneous system-wide `lsof` burst (a behavioral-EDR
 * recon trigger). Order matches the input `pids`. The `probe` seam is injectable
 * for testing the bound; production always uses the real `lsof`-backed probe.
 */
export declare function resolveCwds(pids: number[], probe?: (pid: number) => Promise<string | undefined>): Promise<(string | undefined)[]>;
/**
 * Resolve the host app for a single pid by walking its process ancestry with the
 * same HOST_MATCHERS logic `detectHost` uses. Reads the whole process table per
 * call, so it's for the low-cardinality renderer path (one tmux client per
 * session), not a hot loop. Returns undefined when nothing above the pid is a
 * recognised UI. Exported for the "viewing in <app>" resolver.
 */
export declare function hostFromPid(pid: number): Promise<string | undefined>;
export interface AgentCandidate {
    pid: number;
    kind: string;
}
/**
 * Find the launch registry entry recorded by a WRAPPER of this process. The
 * shim delegate records the pid it spawned, but on Windows the `.cmd` shell
 * path makes that a cmd.exe intermediary whose child is the real agent binary
 * — so the agent pid itself has no entry and the wrapper one ancestor up does.
 * The nearest entry wins, and only if its agent matches the candidate's kind:
 * a claude session shelling out to codex must not hand codex its identity.
 */
export declare function readAncestorSessionEntry(pid: number, ppidMap: Map<number, number>, kind: string, readEntry?: (pid: number) => PidSessionEntry | undefined): PidSessionEntry | undefined;
/**
 * Collapse agent processes spawned by another live agent process of the same
 * kind onto their nearest kept ancestor. Claude runs subagents, forks, and
 * even its bundled ripgrep as child `claude` processes — on POSIX those
 * children resolve to the parent's cwd and collapse in dedupeBySession, but
 * where no cwd can be recovered (Windows has no lsof) every fork would print
 * as its own headless row. Two exceptions keep their own row: a candidate with
 * its own registry entry — on its pid OR on a wrapper ancestor strictly below
 * the pid it would fold into (the shim's entry lands on the cmd.exe
 * intermediary on Windows) — a child whose live argv carries `--session-id`
 * (RUSH-2384: empty by-pid must not fold a real session into a parent and
 * drop it from the active set) — and a child of a *different* agent kind
 * (claude shelling out to codex is a real second session, not a fork).
 * Returns the kept roots plus, per root pid, how many descendants folded in.
 *
 * `hasLiveSessionId` defaults to reading `--session-id` from the live process
 * argv; tests inject a pure predicate so they do not need real agent pids.
 */
export declare function foldSubordinateAgents(candidates: AgentCandidate[], ppidMap: Map<number, number>, readEntry: (pid: number) => PidSessionEntry | undefined, hasLiveSessionId?: (pid: number) => boolean): {
    kept: AgentCandidate[];
    foldedByRoot: Map<number, number>;
};
/**
 * Agent processes not attributed to a team or the runtime registry.
 * Classified by walking the ppid chain: any recognised UI ancestor (IDE
 * helper, terminal-app, or multiplexer) means `terminal`; nothing of the
 * sort means `headless` (daemon, launchd-spawned, orphan).
 *
 * Full `ps`+`lsof` rescans are throttled to {@link UNATTRIBUTED_RESCAN_MS}
 * (#2047). Between rescans the previous rows are re-emitted after dropping
 * dead PIDs and PIDs that are now attributed. A shrinking attributed set
 * forces a rescan so a process that just left teams/terminals can reappear
 * as headless.
 */
export declare function listUnattributedActive(attributed: Set<number>): Promise<ActiveSession[]>;
/** One tmux pane's resolved agent identity for the authoritative source. */
interface PaneIdentity {
    agent: string;
    /** Custom harness/profile name from the launch registry, when set. */
    harness?: string;
    /** Exact session id when resolvable (launch registry, or the hook join). */
    sessionId?: string;
    /** The agent's OS pid from the launch registry (may differ from `pane_pid`). */
    pid?: number;
}
/**
 * Attribute a single tmux pane to the agent actually running in it.
 *
 * The launch registry — written per bare-spawn AND per wrap, each stamped with the
 * `tmuxPane` it targeted (see src/lib/exec.ts) — is the EXACT, per-pane source of
 * truth. So an agent spawned into an EXISTING pane (a split, where `$TMUX` is
 * already set so no new session meta is stamped) is attributed to its OWN launch,
 * not the session's original agent — closing the gap where such an agent was
 * dropped by this source and left to the weaker ps-scan fallback. Session-meta
 * labels remain the fallback for the wrapped origin pane of a session whose
 * registry entry is absent (a failed best-effort write, or a legacy session that
 * predates the registry's `tmuxPane` field) — and ONLY for that origin pane
 * (`meta.pane`), so a split shell pane of a labeled session isn't mis-attributed
 * the wrapped agent. When `meta.pane` is unknown (attach-existing sessions), any
 * labeled pane is accepted and the caller's per-session dedupe keeps one.
 * `source: 'teams'` panes are skipped — teammates are surfaced by listTeamsActive.
 * Pure so it is unit-tested without tmux.
 */
export declare function resolvePaneIdentity(pane: string, sessName: string, meta: {
    labels?: Record<string, string>;
    source?: string;
    pane?: string;
} | null, liveEntry: PidSessionEntry | undefined, getHookIndex: () => HookSessionIndex, nameToFullId: Map<string, string>): PaneIdentity | undefined;
/**
 * Agents hosted in the shared-socket tmux server — the authoritative source for
 * tmux-hosted interactive spawns (see src/lib/exec.ts `runInTmux`). Enumerates
 * every pane on the shared socket and attributes each to the agent running in it
 * via {@link resolvePaneIdentity}: the per-pane launch registry (exact) first, the
 * session-meta labels as fallback. Because tmux (not a per-window
 * `live-terminals.json`) is the source of truth, a tmux-hosted agent is captured
 * with its exact `%pane` even when the extension registry is stale — INCLUDING an
 * agent bare-spawned into a split of an existing session, which older logic dropped
 * (it kept only the first pane per session meta). `source: 'teams'` is skipped.
 */
export declare function listTmuxAgentSessions(): Promise<ActiveSession[]>;
/**
 * Union of all sources. Teams and terminals spawn actual CLI processes that
 * also show up in `ps`, so headless attribution runs last with the already-
 * attributed PIDs removed. The tmux source goes FIRST into the dedupe so a
 * tmux-hosted agent's row (which carries the exact `%pane`) wins over a staler
 * terminal/headless row for the same session id.
 */
export declare function getActiveSessions(opts?: ActiveQueryOptions): Promise<ActiveSession[]>;
/** Local discovery sources probed by {@link describeActiveDiscoveryHealth}. */
interface ActiveDiscoveryHealth {
    /** Sources that failed on the probe (currently only `'tmux'`). Empty = nothing degraded locally. */
    degradedSources: string[];
}
/**
 * Cheap post-hoc health probe for the local discovery sources. `getActiveSessions`
 * always swallows a source failure to `[]` so its return type stays a plain
 * session array — this re-probes the same sources and reports which ones threw,
 * so a caller that got an empty list back can tell "genuinely nothing running"
 * apart from "a source errored and got silently folded into empty" (RUSH-2507).
 * Intended for the empty-result branch only, not the hot path — it repeats a
 * `list-panes` call `getActiveSessions` already made.
 */
export declare function describeActiveDiscoveryHealth(): Promise<ActiveDiscoveryHealth>;
/**
 * True when a live agent process on this host carries `--session-id <id>` in
 * its argv. RUSH-2384 last-resort for `agents message`: getActiveSessions can
 * still miss a row (teams status flap, attributed-ancestor skip), but the
 * process table + argv are ground truth for "this session is alive and
 * mailbox-reachable". Only UUID-shaped ids are accepted so a short prefix
 * never false-matches.
 */
export declare function isSessionIdLiveOnProcessTable(sessionId: string, deps?: {
    readTable?: () => Promise<Array<{
        pid: number;
        kind?: string;
    }>>;
    sessionIdOf?: (pid: number) => string | undefined;
}): Promise<boolean>;
/**
 * Fold tmux's attached-client count onto every tmux-hosted row.
 *
 * Keyed off `provenance.mux` — which {@link enrichProvenance} has already stamped
 * on any row whose process env names a tmux pane — rather than off
 * {@link listTmuxAgentSessions}. That source only emits a row when it can resolve
 * the pane's agent IDENTITY (launch registry or session meta), and on a machine
 * where neither resolves it emits nothing at all while the same sessions still
 * arrive through the terminal/headless sources carrying full tmux provenance.
 * Hanging the client count off the identity-resolving source would have made the
 * whole orphan signal silently dead on exactly those machines.
 *
 * One `list-panes` per distinct socket, and only when some row is tmux-hosted —
 * a fleet with no tmux pays nothing. A query failure leaves the count undefined,
 * which the classifier reads as "cannot tell", never as a false zero.
 */
export declare function foldTmuxClients(rows: ActiveSession[]): Promise<void>;
/**
 * Fold the host link onto each row and, where it changes the answer, onto the
 * status. Runs AFTER {@link foldPresence}, because a deliberately backgrounded
 * session (`presence` `background`/`parked`) is supposed to have no client and
 * must not be reported as an orphan.
 *
 * Precedence is deliberate, and the two new statuses slot in where they add
 * information rather than destroy it:
 *
 *   - `abandoned` wins outright. A days-stale session is already dangling; that
 *     it also lost its window is not the headline, and it keeps a crashed row
 *     from lingering as an alert forever.
 *   - `crashed` REPLACES `closed`. Both mean the process is gone, but `closed`
 *     reads as a normal exit; `crashed` says the host window went down with it
 *     and never cleaned up.
 *   - `orphaned` replaces `idle` / `input_required` on ANY `no-client`, and
 *     `running` ONLY when the owning WINDOW was lost (`hostWindowLost`). A
 *     session still working with merely zero tmux clients is a normal headless
 *     run — since RUSH-3125 a detached remote pane is the steady state — so
 *     flagging every one would bury the real signal (the false positive reverted
 *     in 6d973b823). But a running agent whose IDE window stopped republishing
 *     its heartbeat outlived an unclean host death and is genuinely stranded, so
 *     it IS promoted. A session sitting idle — or worse, waiting on a question —
 *     with no client attached is the same stranded case: nobody is coming.
 */
/**
 * Attribute each live row to the machine the session actually EXECUTES on.
 *
 * A host-dispatched run (`agents run --device <peer>`) leaves a live process on
 * the DISPATCHING box — the ssh/TTY shim — carrying the remote run's session id.
 * Nothing about that local process knows the agent is on the peer, so the row
 * was tagged with THIS machine: `--device <dispatcher>` then claimed a session
 * that is not running here, and its preview dead-ended at "full transcript not
 * indexed here" because the transcript lives on the peer (RUSH-2479).
 *
 * The dispatch already recorded the truth. `registerHostSession` /
 * `registerInteractiveHostSession` write the index row with
 * `machine: normalizeHost(task.host)` (`lib/hosts/session-index.ts:55,134`), so
 * this folds that recorded machine back onto the live row. Every consumer then
 * agrees on one owner: the `--device`/`--device` scope, the browser's device
 * filter, `_remote`/preview routing (`liveSessionToMeta`), and the id resolver.
 *
 * Two rows are deliberately left alone:
 *   - one the cross-machine fan-out already attributed to a peer — that is the
 *     peer's own self-report, which outranks this box's index copy;
 *   - one whose indexed machine IS this box — nothing was offloaded.
 *
 * Pure over the array; `machineOf` is injected so the join is unit-tested
 * without a SQLite index.
 */
export declare function foldExecutionMachine(rows: ActiveSession[], machineOf: (sessionId: string) => string | undefined, self: string): void;
/**
 * Is the PROCESS behind this row running on this machine?
 *
 * `machine` answers a different question — "where does the agent execute" —
 * which is what a `--device` scope, preview routing, and resume ownership need.
 * For an offloaded run the two answers diverge: {@link foldExecutionMachine}
 * points `machine` at the peer, while the shim process, its tmux pane, and its
 * terminal window are all still HERE. `offloadedFrom` marks exactly that row.
 *
 * Any caller reaching for a LOCAL pid, pane, or window must ask this rather
 * than `machine === self`. A local tmux pane id (`%N`) handed to a peer's tmux
 * server does not fail — pane ids are small per-server integers, so it can
 * resolve against an unrelated pane and attach the user to someone else's
 * session. That is why this predicate exists instead of ten copies of the
 * comparison.
 */
export declare function sessionProcessIsLocal(s: Pick<ActiveSession, 'machine' | 'offloadedFrom'>, self: string): boolean;
/**
 * The machine to reach for this session's PROCESS — its pid, tmux pane, and
 * window — or `undefined` when that process is right here.
 *
 * Not the same as `machine`, which is where the AGENT executes. For an offloaded
 * run the process lives on the dispatcher (`offloadedFrom`) while the agent runs
 * on the peer, so a caller that ssh'd to `machine` would carry the dispatcher's
 * pane id to a box that never had it.
 */
export declare function sessionProcessHost(s: Pick<ActiveSession, 'machine' | 'offloadedFrom'>, self: string): string | undefined;
export declare function foldHostLink(rows: ActiveSession[]): void;
/** Labels win, then the last assistant line, then the first-prompt topic. */
export declare function deriveSessionRecap(row: Pick<ActiveSession, 'label' | 'topic' | 'tail'>): {
    title?: string;
    recapSource?: RecapSource;
    userPromptClean?: string;
    userPromptKind?: UserPromptKind;
    lastAgentLine?: string;
};
/** Fold the recap ladder onto every row (see {@link deriveSessionRecap}). */
export declare function foldRecap(rows: ActiveSession[]): void;
/**
 * Project a {@link SessionPhase} from the finalized {@link ActiveStatus} (PHNX-2484).
 * This is the single source of truth the AGI EXT previously mirrored as
 * `mapStatusToPhase` — the ext now reads the `phase` field instead of re-deriving it.
 * `queued` counts as `running` (dispatched, in the pipeline); `abandoned`, `orphaned`,
 * and `crashed` all bucket to `failed` (dangling/dead — needs attention), which is the
 * drift this fixes: those three used to fall through a status-only map to `idle` and
 * silently hide a dead agent.
 */
export declare function derivePhase(status: ActiveStatus | undefined): SessionPhase;
/** Fold the coarse lifecycle {@link SessionPhase} onto every row (see {@link derivePhase}). */
export declare function foldPhase(rows: ActiveSession[]): void;
/** Reap only stale sessions with proven-dead pids; unknown or live processes remain recoverable. */
export declare function isReapableOrphan(row: Pick<ActiveSession, 'status' | 'pidAlive'>): boolean;
/**
 * Resolve each teams row's `orchestratorLabel` from the orchestrator's own row,
 * when that orchestrator session is itself in the active set (it usually is — the
 * agent that ran `agents teams add` is running). Falls back to nothing, so the
 * renderer shows the short id. Pure over the array; exported for tests.
 */
export declare function annotateOrchestratorLabels(sessions: ActiveSession[]): void;
/**
 * Attach provenance (host / local-vs-SSH / tmux pane / reply rail) to every
 * session that has a live pid. Mutates in place. Runs after dedupe so we probe
 * each session once, not once per fork pid. Probes run in parallel — each is a
 * single /proc read (Linux) or `ps` call (macOS); failures leave `provenance`
 * undefined rather than blocking the listing. The probes use the same bounded
 * concurrency as the adjacent per-PID lsof sweep so large session lists cannot
 * spawn one `ps eww` subprocess per row at once on macOS.
 *
 * A row that already carries provenance (the tmux path, which knows its exact
 * mux/reply from the pane) is not skipped — it is probe-and-MERGED. The tmux
 * path can only stamp a `transport:'local'` placeholder because the pane alone
 * doesn't reveal how the shell above it was reached; the process env does. So we
 * still read the env and fill in the real SSH origin/term, while preserving the
 * authoritative mux/reply the pane already gave us. Skipping this (the old
 * behavior) is exactly why ssh-launched tmux sessions rendered as local.
 */
export declare function enrichProvenance(sessions: ActiveSession[], probe?: (pid: number) => Promise<SessionProvenance | undefined>): Promise<void>;
/**
 * Match an SSH client IP to a registered device (pure — testable with a plain
 * registry object). Returns the device name + ssh login user when the IP is a
 * known device address.
 */
export declare function matchOriginDevice(clientIp: string, reg: DeviceRegistry): {
    device: string;
    user?: string;
} | undefined;
/**
 * Collapse rows that resolve to the *same* session — a session with many
 * subagent/fork PIDs (all matched to one transcript file) would otherwise print
 * dozens of identical rows. Keyed by session id, falling back to the transcript
 * file, then the cloud/run handle, then {@link anonymousWorkerKey}. The first row
 * wins and carries a `pidCount`.
 */
export declare function dedupeBySession(sessions: ActiveSession[]): ActiveSession[];
export {};
