/**
 * Session Manager — spawn and monitor Claude Code sessions via tmux.
 *
 * This is the core capability that transforms Claude Code from a CLI tool
 * into a persistent agent. Sessions run in tmux, survive terminal disconnects,
 * and can be monitored/reaped by the server.
 */
import { EventEmitter } from 'node:events';
import { SessionLivenessOracle } from './SessionLivenessOracle.js';
import type { ReapGuard } from './ReapGuard.js';
/** Diagnostics for a single running session */
export interface SessionDiagnostic {
    name: string;
    id: string;
    jobSlug?: string;
    ageMinutes: number;
    maxDurationMinutes?: number;
    isStale: boolean;
    staleReason?: string;
}
/** System memory pressure levels */
export type MemoryPressure = 'low' | 'moderate' | 'high' | 'critical';
/** Full diagnostics snapshot for intelligent scheduling decisions */
export interface SessionDiagnostics {
    sessions: SessionDiagnostic[];
    maxSessions: number;
    staleSessions: SessionDiagnostic[];
    memoryPressure: MemoryPressure;
    memoryUsedPercent: number;
    freeMemoryMB: number;
    totalMemoryMB: number;
    suggestions: string[];
}
import { InputGuard } from './InputGuard.js';
import type { InputDetector } from '../monitoring/PromptGate.js';
import type { Session, SessionManagerConfig, ModelTier } from './types.js';
import type { IntelligenceFramework } from './intelligenceProviderFactory.js';
import { StateManager } from './StateManager.js';
/**
 * Pure age-gate idle decision — extracted so the exact 2026-06-13 incident can be
 * reproduced at the decision boundary (Bug-Fix Evidence Bar). A session past its
 * age limit is "truly idle" — and thus age-kill-eligible — ONLY when ALL THREE
 * hold: its pane shows an idle prompt, it has no non-baseline child process, and
 * its framework transcript is not recently active. Any ONE positive activity
 * signal (a live child process OR a growing transcript) defers the kill. The
 * transcript term is the fix: it closes the blind spot to MCP/tool work that runs
 * OUT of the pane's process tree. The incident inputs
 * (idleAtPrompt=true, hasActiveProcs=false, transcriptActive=true) return
 * `false` ⇒ DEFERRED, not killed — which the pre-fix two-signal decision got wrong.
 */
export declare function isAgeGateTrulyIdle(idleAtPrompt: boolean, hasActiveProcs: boolean, transcriptActive: boolean): boolean;
/** Patterns that indicate Claude is sitting at its idle prompt (not actively working).
 *  Exported for ModelSwapService (§5.3 of FABLE-MODEL-ESCALATION-SPEC) — the
 *  mid-session swap must verify prompt-ready idleness from the same source of
 *  truth the monitor loop uses, not a parallel copy that can drift. */
export declare const IDLE_PROMPT_PATTERNS: string[];
/**
 * Pure gate for the post-API-error nudge (the 2026-05-29 re-arm fix). A session is
 * nudged only when it has NOT already been nudged in the CURRENT idle episode
 * (`armedThisEpisode` false) AND it is under the lifetime runaway cap. The episode
 * flag is cleared on recovery, so a long-running session that hits a SECOND transient
 * API error is nudged again — fixing the prior once-per-session-forever strand.
 * Exported so the decision boundary is unit-testable without driving the tmux loop.
 */
export declare function shouldErrorNudge(armedThisEpisode: boolean, totalNudges: number, max?: number): boolean;
/**
 * Parse a `ps -o time` accumulated-CPU-time string to seconds. Handles the
 * formats ps emits across platforms: `MM:SS`, `MM:SS.ss`, `HH:MM:SS`, and the
 * day-prefixed `DD-HH:MM:SS` (Linux/BSD). Returns 0 on anything unparseable —
 * used as a CPU-progress delta signal, so a bad sample just reads as no growth.
 * Exported for unit tests.
 */
export declare function parseProcTimeToSeconds(raw: string): number;
export interface SessionManagerEvents {
    sessionComplete: [session: Session];
    /**
     * Emitted when a session goes idle after a server-side throttle ("Server is
     * temporarily limiting requests · not your usage limit") rather than a
     * generic API error. The RateLimitSentinel owns recovery from here — the
     * immediate single-nudge is deliberately skipped for this case.
     */
    rateLimitedAtIdle: [sessionName: string];
}
export declare class SessionManager extends EventEmitter {
    private config;
    private state;
    private monitorInterval;
    private monitoringInProgress;
    private inputGuard;
    private registryPath;
    /** Track when each session was first seen idle at the Claude prompt. Key = session ID */
    private idlePromptSince;
    /**
     * Per-session marker of the most-recently injected message that has NOT yet
     * been confirmed submitted. Keyed by tmuxSession. Used by StuckInputSentinel
     * to recover messages that stranded at the input prompt.
     *
     * Why this exists for codex specifically: Claude Code's readline QUEUES input
     * typed while a turn is in flight and auto-submits it when the turn ends, so a
     * busy-delivery is self-healing. Codex's TUI does NOT — a message typed while
     * codex is "Working" is held as an unsubmitted DRAFT and never auto-submits
     * when the turn ends (live repro 2026-05-31: a user message sat stranded for
     * 3h on a busy 37-min codex turn). The in-process verifyInjection timers only
     * poll for 6.5s — far short of a multi-minute codex turn — and the generic
     * StuckInputSentinel prompt-text reader can't tell a real codex draft from the
     * dim placeholder hint codex renders at an EMPTY `›` prompt. A marker (the
     * actual injected text) is the robust, placeholder-immune tell: the codex
     * placeholder never equals what we injected. The sentinel matches this marker
     * at the `›` prompt and fires Enter once codex goes idle. Cleared on confirmed
     * submit (verifyInjection / sentinel) and GC'd for dead sessions.
     *
     * Distinct from `pendingInjections` (response-verification: did the session die
     * before replying?) — that map is cleared the moment the session produces ANY
     * output, which for a busy codex session happens while the draft is still
     * stranded. This map clears only when the marker actually leaves the prompt,
     * so it survives a long busy turn. */
    private strandedDraftMarkers;
    /** Throttle stale session cleanup to every 5 minutes */
    private lastCleanupAt;
    /** Optional callback to check if a session has active subagents (prevents false zombie kills) */
    private subagentChecker?;
    /** Optional callback: is this session currently in active compaction recovery? If so, skip zombie kill. */
    private activeRecoveryChecker?;
    /** Shared stateless KEEP-guard consulted by terminateSession (§P2). */
    private reapGuard?;
    /** Pressure-tier provider for the work-evidence fallback (reap-notify R2.1). */
    private pressureTier?;
    /** Sessions the §P5 backstop has flagged long-`indeterminate` — excluded from
     *  the ABSOLUTE spawn cap so unverifiable panes can't lock out spawning. */
    private longIndeterminateSessions;
    /** Multi-machine lease/awake predicate gating autonomous reaps (§Multi-machine). */
    private isAwakeMachine?;
    /** Optional callback: is this tmux session currently bound to a live messaging topic
     *  (Telegram/Slack/iMessage)? Returns a stable identifier (e.g. topic ID or channel ID)
     *  when bound, null otherwise. When bound, the zombie-kill threshold is extended via
     *  `effectiveBoundIdleKillMinutes` since "idle at prompt" is the healthy waiting state. */
    private topicBindingChecker?;
    /**
     * Subscription-pool pinning resolver (Subscription & Auth Standard). When set,
     * a new claude-code spawn launches under the returned account's config home
     * (`CLAUDE_CONFIG_DIR`) and the session is tagged with `subscriptionAccountId`
     * — which is the linkage auto-swap needs to move a session when its account
     * hits a quota wall. Null/unset → spawns use the default config exactly as
     * before (the gate: this stays unset unless `subscriptionPool.pinSessionsToPool`
     * is enabled, so it is a strict no-op by default). Returns null per-call when
     * no eligible account exists. */
    private spawnAccountResolver?;
    /**
     * Census #5/#6: live credential re-pointing gate. When set AND enabled, a pinned account's
     * spawn home is resolved through the ledger (the account's CURRENT slot) instead of the
     * enrollment `configHome` the resolver returns — so after a swap a session lands on the slot
     * the credential ACTUALLY lives in now, not the stale enrollment home. Unset / flag-off /
     * ledger-unknown → the enrollment home is used exactly as today (strict no-op while dark).
     */
    private credentialLocationGate?;
    /** Prompt Gate InputDetector — monitors terminal output for interactive prompts */
    private promptDetector?;
    /** Sessions with active relay leases (prompt relayed, waiting for response) — extends idle timeout */
    private relayLeases;
    /** Sessions the SessionReaper has leased for a two-phase reap (the transient
     *  'reaping' state of SESSION-REAPER-SPEC §3.6). While a session id is here,
     *  the idle-kill path skips it so only the reaper acts on it. */
    private reapingSessions;
    /** Sessions whose termination is currently in flight — guards terminateSession()
     *  against re-entrant double-kill / double-event emission within a tick. */
    private terminating;
    /** Track pending Telegram injections awaiting agent response.
     *  Key = tmuxSession name. Cleared when agent replies via /telegram/reply/:topicId. */
    private pendingInjections;
    /** Dedup ledger for the Telegram→session delivery chokepoint.
     *  Key = `${tmuxSession}:${messageId}` → deliveredAt(ms). A single user message
     *  that is over-forwarded upstream (lifeline re-forward, PendingRelayStore
     *  re-drive, sentinel pause/resume) must still reach the session at most once.
     *  Bounded by pruning entries older than the dedup window on each delivery. */
    private recentTelegramDeliveries;
    private static readonly TELEGRAM_DELIVERY_DEDUP_WINDOW_MS;
    /** Track sessions nudged after an API error in the CURRENT idle episode.
     *  Key = session ID. Set when we nudge; CLEARED when the session recovers (produces
     *  output / leaves idle), so the NEXT API-error episode in a long-running session
     *  gets its own nudge. (Before 2026-05-29 this was once-per-session-FOREVER — a long
     *  autonomous run that hit a SECOND transient API error was never re-nudged and
     *  silently stranded at the prompt. The runaway cap lives in errorNudgeTotal.) */
    private errorNudgedSessions;
    /** Total error-nudges issued per session (never cleared until sessionComplete).
     *  Bounds runaway: even with per-episode re-arming, a session stuck in a tight
     *  error→nudge→error loop that never truly recovers stops being nudged after
     *  MAX_ERROR_NUDGES_PER_SESSION and falls through to the normal zombie-kill path. */
    private errorNudgeTotal;
    /** Sessions where we've already retried Enter for stuck pasted text.
     *  Key = session ID. Prevents infinite retry loops — one retry per session. */
    private pasteRetried;
    /**
     * Sessions that have been logged once for "over age limit but actively
     * working." Tracked to avoid log spam — the deferred-kill warning fires
     * once per session, not every tick.
     */
    private overAgeButActiveLogged;
    /** Per-session back-off so the age-gate respects the KEEP-guard's verdict: after a kill
     *  is vetoed (session kept), suppress re-requests for a window instead of re-asking every
     *  5s tick (the 2026-06-05 17,503-line flood). Constructed in the constructor. */
    private ageKillBackoff;
    /** Cached count of running sessions, updated asynchronously by the monitor tick.
     *  Used by the health endpoint to avoid synchronous tmux polling. */
    private _cachedRunningCount;
    private _cachedRunningSessions;
    /** Worktree manager — when set, spawnSession resolves an isolated worktree per topic. */
    private worktreeManager;
    private buildContextStore;
    private pendingInjects;
    /** Per-session shim directory root (one subdir per session). Used for K9 mandatory shim. */
    private shimRoot;
    /**
     * SDK-credit reader for the june15-headless-spawn-reroute 'auto' decision
     * (PR2). Plumbed from server boot's anthropicRegistration.readSdkCredit (the
     * SAME TTL-cached reader PR1's routing policy uses, so the two routing layers
     * cannot drift). Returns null when credit state is unknown → the shared
     * decideSdkVsSubscription falls to the subscription floor. Unset (tests/
     * standalone) ⇒ 'auto' is treated as always-reroute-on-unknown (== subscription
     * floor), matching the pure function's null-snapshot contract. */
    private readSdkCredit?;
    /** Last reroute-decision per framework, so the transition-only log line fires
     *  on a CHANGE rather than every spawn (mirrors PR1's onRoute). Keyed by
     *  framework; value is the resolved path string. */
    private lastRerouteDecision;
    /** State root for the per-agent encrypted vault (P3b gh-token resolution —
     *  same derivation as the pending-inject ledger: StateManager.baseDir when
     *  present, else `<projectDir>/.instar`). */
    private vaultStateDir;
    constructor(config: SessionManagerConfig, state: StateManager);
    /** Per-agent GitHub token env flags for spawned sessions (Phase-3 increment
     *  P3b, option C — per-agent credential isolation). Resolves the agent's OWN
     *  token from its encrypted vault at spawn time; when the vault holds none,
     *  returns [] and the spawn env is unchanged (machine-global gh behavior,
     *  exactly as before). Mirrors the existing credential-injection art
     *  (INSTAR_AUTH_TOKEN / ANTHROPIC_API_KEY via tmux -e). The hardened triage
     *  spawn deliberately does NOT call this — it scrubs credentials. */
    private ghTokenEnvFlags;
    /** Lazily-constructed tri-state liveness oracle (UNIFIED-SESSION-LIFECYCLE §P1).
     *  Backs the boot purge and isSessionAliveAsync so a slow/unreachable tmux is
     *  treated as `indeterminate`, never `dead`. */
    private _livenessOracle;
    private get livenessOracle();
    /** Test/DI seam: inject a liveness oracle (e.g. one with a scripted exec).
     *  Production builds the oracle lazily from config; this lets unit tests drive
     *  the alive/dead/indeterminate verdicts deterministically. */
    setLivenessOracle(oracle: SessionLivenessOracle): void;
    /** Effective idle-at-prompt kill threshold (config override or hardcoded default) */
    private get effectiveIdleKillMinutes();
    /** Effective idle-at-prompt kill threshold for sessions bound to a live messaging topic.
     *  Much higher than the default — topic-bound sessions sit at the prompt waiting for the
     *  next user message; that's the healthy state, not a zombie. */
    private get effectiveBoundIdleKillMinutes();
    /** Effective absolute max session duration (config override or hardcoded default) */
    private get effectiveMaxDurationMinutes();
    /**
     * Wire the WorktreeManager into spawnSession. When set, every spawned session
     * resolves to an isolated worktree (PARALLEL-DEV-ISOLATION-SPEC.md, AC-1).
     *
     * @param shimRoot Filesystem root for per-session shim directories
     *                 (e.g. `<stateDir>/session-shims/`). When unset, no PATH shim
     *                 is injected (degrades to non-isolated mode for back-compat).
     */
    setWorktreeManager(wm: import('./WorktreeManager.js').WorktreeManager, shimRoot?: string | null): void;
    /**
     * Per-session shim (PARALLEL-DEV-ISOLATION-SPEC.md "Phase D: Mandatory destructive
     * command interception"). Creates `<shimDir>/git`, `<shimDir>/rm`, and a `.shellrc`
     * that defines bash functions overriding any user-shell aliases. Sourcing happens
     * via `BASH_ENV` env var (set in tmux spawn).
     */
    private installSessionShim;
    private resolveShimRunner;
    /**
     * Set the InputGuard for cross-topic injection defense.
     * Must be called after construction with state dir info.
     */
    setInputGuard(guard: InputGuard, registryPath: string): void;
    /**
     * Set the subagent checker callback for zombie cleanup awareness.
     * When set, the zombie cleanup will skip sessions that have active subagents.
     * Must be called after SubagentTracker is constructed.
     */
    setSubagentChecker(checker: (session: Session) => boolean): void;
    /**
     * Set the topic-binding checker — used by the zombie-kill loop to distinguish
     * sessions that are waiting for the next message from a live conversation
     * (healthy "idle at prompt") from sessions that have nothing to do (zombies).
     *
     * When the checker returns a non-null identifier for a session, the kill
     * threshold is extended to {@link effectiveBoundIdleKillMinutes} (default 24h).
     * This is a structural exemption — the binding is an authoritative fact, not a
     * judgment call. If Claude truly dies inside the tmux pane, isSessionAlive in
     * the bridge's message-routing path will detect it and trigger a clean respawn.
     *
     * Must be called after the messaging adapter is constructed.
     */
    setTopicBindingChecker(checker: (tmuxSession: string) => string | number | null): void;
    /**
     * Wire the subscription-pool pinning resolver (Subscription & Auth Standard).
     * Called from server startup ONLY when `subscriptionPool.pinSessionsToPool` is
     * enabled, so leaving it unwired keeps spawns on the default config (no-op).
     * The resolver picks the optimal pool account (the scheduler's reset-date /
     * headroom score) at spawn time and returns its configHome + id, or null.
     */
    setSpawnAccountResolver(resolver: () => {
        configHome: string;
        accountId: string;
    } | null): void;
    /**
     * Wire the credential re-pointing gate (census #5/#6). Called from server startup; absent /
     * flag-off keeps spawn placement on the enrollment home exactly as today.
     */
    setCredentialLocationGate(gate: import('./CredentialLocationGate.js').CredentialLocationGate): void;
    /**
     * Census #5/#6: resolve a pinned account's LIVE spawn home through the ledger gate. Returns the
     * account's current slot when re-pointing is enabled AND the ledger knows it; otherwise the
     * enrollment `configHome` unchanged (today's behavior). Sync + fail-open (the gate never throws).
     */
    private resolvePinnedSpawnHome;
    /**
     * Defensive onboarding-readiness seed before any launch under a pool
     * account's CLAUDE_CONFIG_DIR (2026-06-09 incident): a headless-enrolled
     * home is missing the interactive first-launch flags, so an interactive
     * relaunch into it wedges on the onboarding screens. Cheap + idempotent —
     * safe to call on every pinned/swapped launch. Never throws (fail-safe by
     * the util's contract); failures are logged, not fatal, because the worst
     * case is the pre-fix behavior, not a dead spawn path.
     */
    private ensurePinnedHomeInteractiveReady;
    /**
     * Register a predicate that vetoes zombie cleanup when a compaction recovery
     * is in flight. When it returns true, the idle-prompt killer skips the
     * session and resets its idle clock so cleanup won't race the recovery
     * window. Wired by CompactionSentinel at server startup.
     */
    setActiveRecoveryChecker(checker: (session: Session) => boolean): void;
    /**
     * Register the shared ReapGuard (UNIFIED-SESSION-LIFECYCLE §P2). Once set,
     * every AUTONOMOUS terminate consults it: a non-null KEEP reason makes the
     * terminate a no-op `{ terminated:false, skipped:<reason> }`. Wired in server.ts
     * with the same signal closures that back SessionReaper, so a killer can only
     * *request* a kill — the authority refuses to end a guarded session. Operator
     * kills bypass the guard. Unset (tests/standalone) → no guard consult.
     */
    setReapGuard(guard: ReapGuard): void;
    /**
     * Wire the SDK-credit reader used by the headless-spawn-reroute 'auto'
     * decision (june15-headless-spawn-reroute, PR2). Server boot passes
     * anthropicRegistration.readSdkCredit — the same TTL-cached reader PR1's
     * routing policy uses — so the reroute decision and the intelligence-funnel
     * routing share ONE credit source and can't drift. Unset ⇒ 'auto' resolves to
     * the subscription floor (null-snapshot contract of decideSdkVsSubscription).
     */
    setSdkCreditReader(reader: () => Promise<import('../providers/primitives/observability/usageMeterProvider.js').AgentSdkCreditSnapshot | null>): void;
    /**
     * Flag/unflag a session as long-`indeterminate` (UNIFIED-SESSION-LIFECYCLE §P5).
     * Driven by the StaleSessionBackstop. A flagged session still counts toward the
     * soft scheduler cap, but is excluded from the ABSOLUTE `maxSessions × 3` cap so
     * a fleet of unverifiable panes can never lock a human out of spawning.
     */
    markLongIndeterminate(sessionId: string, isLong: boolean): void;
    /**
     * Update a session's display `name` only (UNIFIED-SESSION-LIFECYCLE bonus —
     * session label follows topic rename). NEVER touches `tmuxSession` (the
     * stable key tmux + every internal lookup uses) or `id` (the stable UUID
     * state lookups use). The rename surfaces in the dashboard + any place that
     * prints `session.name`; the operational identity stays intact.
     *
     * Idempotent + safe: if the session isn't found or the name is unchanged,
     * the call is a no-op. Returns true when a save occurred.
     */
    renameSessionByTmux(tmuxSession: string, newName: string): boolean;
    /**
     * Resolve tri-state liveness for many sessions from ONE tmux snapshot via the
     * P1 oracle (UNIFIED-SESSION-LIFECYCLE §P5 backstop). `reachable` is false when
     * the snapshot was non-authoritative (control plane unreachable) — in which
     * case every session resolves `indeterminate`. Shares the oracle's short-TTL
     * cache, so the backstop and boot-purge never double-probe within a tick.
     */
    probeLivenessBatch(tmuxSessions: string[]): Promise<{
        reachable: boolean;
        liveness: Map<string, 'alive' | 'dead' | 'indeterminate'>;
    }>;
    /**
     * Register the multi-machine lease/awake predicate. When set and it returns
     * false, an AUTONOMOUS terminate is a no-op `skipped:'not-lease-holder'` — only
     * the awake/lease-holding machine may autonomously reap; a standby may detect
     * and signal but never kill another machine's sessions. Operator kills bypass.
     * Unset → treated as awake (single-machine default).
     */
    setAwakeChecker(fn: () => boolean): void;
    /**
     * Set the Prompt Gate InputDetector for prompt monitoring.
     * When set, monitorTick() will capture output and feed it to the detector.
     */
    setPromptDetector(detector: InputDetector): void;
    /**
     * Grant a relay lease to a session — extends idle timeout while waiting for
     * a Telegram relay response. Prevents the zombie killer from killing sessions
     * that are legitimately waiting for user input.
     */
    grantRelayLease(sessionId: string, durationMs: number): void;
    /**
     * Clear a relay lease (prompt was answered or timed out).
     */
    clearRelayLease(sessionId: string): void;
    /**
     * True if the session has an unexpired relay lease (a prompt was relayed and
     * we are waiting on the user). Gate H of the SessionReaper — keyed on the
     * instar session id, not the tmux name. (SESSION-REAPER-SPEC §3.1(4)H.)
     */
    isRelayLeaseActive(sessionId: string): boolean;
    /** The resolved protected-session list (incl. the `<project>-server` default).
     *  Exposed so the SessionReaper's gate A uses the SAME list the idle-kill /
     *  terminateSession guards enforce, rather than re-reading raw file config. */
    getProtectedSessions(): string[];
    /** Mark a session as being reaped (two-phase reap window). The idle-kill path
     *  skips reaping sessions so the reaper is the single actor on them. */
    markReaping(sessionId: string): void;
    /** Clear the reaping lease (reap completed or aborted). */
    clearReaping(sessionId: string): void;
    /** True if a reap is in flight for this session. */
    isReaping(sessionId: string): boolean;
    /**
     * Single-writer session termination — the sole ReapAuthority
     * (UNIFIED-SESSION-LIFECYCLE §P0, building on SESSION-REAPER-SPEC §3.6). EVERY
     * autonomous killer funnels through this so a session is killed at most once,
     * with exactly-once `beforeSessionKill`/`sessionComplete`/`sessionReaped`
     * emission and a correct `endedReason`.
     *
     * The authority holds the safety checks so a killer can only *request* a kill:
     *  - CAS on live status + an in-flight guard (prevents the double-kill race).
     *  - `protectedSessions` (never kill).
     *  - Lease-holder gate: an AUTONOMOUS reap on a standby machine is a no-op
     *    `skipped:'not-lease-holder'` — only the awake machine reaps.
     *  - ReapGuard (§P2): an AUTONOMOUS reap of a session the guard says to KEEP is
     *    a no-op `skipped:<keep-reason>` — even a buggy killer cannot end a guarded
     *    session.
     * `origin:'operator'` bypasses the lease-gate and the guard (an explicit human
     * kill must always happen). Default `origin` is `'autonomous'` so an in-process
     * caller can never accidentally mint operator privilege.
     *
     * Re-entrancy: the guard is consulted BEFORE this call acquires its own
     * in-flight lock, so the authority's lock for the kill it is performing is
     * never misread by the guard.
     *
     * `disposition:'recovery-bounce'` marks a kill-to-respawn (SessionRecovery,
     * version-skew, context-exhaustion) so the §P3 notifier stays silent — that is
     * a bounce, not a disappearance. Default `'terminal'`.
     *
     * @returns `{ terminated: true }` on a kill performed by THIS call, or
     *          `{ terminated: false, skipped }` describing why it was a no-op.
     */
    terminateSession(sessionId: string, reason: string, opts?: {
        finalStatus?: 'completed' | 'killed';
        disposition?: 'terminal' | 'recovery-bounce';
        origin?: 'operator' | 'autonomous';
        /** UNTRUSTED caller-supplied provenance claim (REMOTE-SESSION-CLOSE-SPEC
         *  §2.3) — recorded in the reap-log as `viaClaim`, NEVER consulted in any
         *  authority/bypass decision (signal-vs-authority). */
        via?: string;
        knownDead?: boolean;
        bypassRecoveryFlag?: boolean;
        bypassActiveProcessKeep?: boolean;
        /**
         * Killer-supplied work evidence (reap-notify spec R2.1): computed at the
         * KILLER's decision point — the only moment the work was observable (the
         * quota-shed migrator computes it BEFORE its Ctrl+C grace round tears the
         * work down; a chokepoint re-run is empty by construction for
         * guard-cleared kills). Clamped to the WorkEvidence enum here regardless
         * of which killer supplied it; unknown names are dropped, not stored.
         */
        workEvidence?: string[];
    }): Promise<{
        terminated: boolean;
        skipped?: string;
    }>;
    /**
     * Pressure-tier provider for the evidence fallback (reap-notify spec R2.1):
     * at tier 'critical' the fork-based closures are skipped and the
     * `unverified-under-pressure` marker is stamped instead. Wired at boot to
     * the shared PressureGauge; absent ⇒ forks run normally.
     */
    setPressureTierProvider(provider: () => 'normal' | 'moderate' | 'critical'): void;
    /**
     * Observe-only work-evidence snapshot for a LIVE session (reap-notify spec
     * R2.1) — for killers that must capture evidence at their decision moment,
     * before their own teardown destroys it (the quota-shed migrator's Ctrl+C
     * grace round). Never throws; never blocks anything.
     */
    collectWorkEvidence(sessionId: string): string[];
    /**
     * Associate a Claude Code session UUID with an instar session.
     * Called on hook events from a Claude Code session, allowing
     * SubagentTracker lookups to bridge the two ID spaces.
     *
     * Last-writer-wins, NOT write-once: a fresh respawn or `claude --resume`
     * rotates the conversation UUID, and every subsequent hook event carries
     * the NEW id. The old write-once guard froze the first-ever UUID, so after
     * any respawn the record pointed at a transcript that no longer grew (or
     * never existed) — which made both sentinels' jsonl-growth recovery
     * verification permanently fail and falsely escalate healthy sessions
     * ("no jsonl growth after 6 attempts" on a session that was actively
     * answering). 2026-06-06 echo-api-errors incident.
     *
     * Safe because every hook event's session_id is the MAIN conversation's
     * UUID (subagents are identified by separate agent_id/agent_transcript_path
     * fields, never by swapping session_id), and a dead conversation emits no
     * further events to flip the value back.
     */
    setClaudeSessionId(instarSessionId: string, claudeSessionId: string): void;
    /**
     * Find a running session by its instar session ID.
     */
    getSessionById(instarSessionId: string): Session | undefined;
    /**
     * Look up the topic binding for a tmux session from the topic-session registry.
     * Returns null if the session is not bound to any topic.
     */
    private getTopicBinding;
    /**
     * Start polling for completed sessions. Emits 'sessionComplete' when
     * a running session's tmux process disappears.
     *
     * Uses async tmux calls to avoid blocking the event loop when
     * many sessions are running.
     */
    startMonitoring(intervalMs?: number): void;
    private monitorTick;
    /**
     * Stop the monitoring poll.
     */
    stopMonitoring(): void;
    /**
     * Current host memory-pressure tier (june15-headless-spawn-reroute PR2, O2).
     * Extracted from getSessionDiagnostics' inline computation so the reroute
     * pre-spawn gate and the diagnostics surface read ONE definition of pressure.
     * The reroute gate refuses to hold a fresh ~200-500MB REPL when the host is
     * already 'high'/'critical' — the 2026-06-05 laptop-meltdown class.
     */
    private currentMemoryPressure;
    /**
     * Count live sessions currently on the rerouted-interactive lane
     * (june15-headless-spawn-reroute PR2, findings S1/O2). The concurrency cap
     * (subscriptionPath.maxRerouted) bounds the held-REPL memory envelope ACROSS
     * all spawn classes — jobs, A2A, and mentor spawns alike — since A2A/mentor
     * spawns carry no jobSlug and would otherwise be bounded only by the global
     * maxSessions.
     */
    private countReroutedInteractiveSessions;
    /**
     * Decide whether a claude-code headless spawn should be rerouted onto the
     * interactive (subscription) lane (june15-headless-spawn-reroute PR2, "The
     * core switch"). ONLY claude-code is eligible — the lever is Anthropic-billing-
     * specific; codex/gemini headless paths don't draw the Anthropic SDK pot.
     *
     * - mode 'off'/absent           → false (today's behavior, byte-for-byte)
     * - mode 'force'                → true  (zero `claude -p` traffic, the soak lever)
     * - mode 'auto'                 → consult the SAME shared decideSdkVsSubscription
     *                                 + TTL-cached credit reader PR1 uses, so the two
     *                                 routing layers can't drift. Subscription → reroute.
     *
     * Emits a transition-only log line when the resolved lane CHANGES for this
     * framework (mirrors PR1's onRoute — a line per change, not per spawn).
     */
    private shouldRerouteToInteractive;
    /**
     * Spawn a new Claude Code session in tmux.
     *
     * When a WorktreeManager is wired in (see `setWorktreeManager`) and `topicId`
     * is supplied, the session is resolved into an isolated topic worktree
     * (PARALLEL-DEV-ISOLATION-SPEC.md). Without `topicId`, falls back to the
     * legacy main-checkout behavior for back-compat with non-topic-bound jobs.
     */
    spawnSession(options: {
        name: string;
        prompt: string;
        /** Either a Claude tier name, a generic tier ('fast'|'balanced'|'capable'),
         *  or a raw model id. Resolution happens inside the framework's headless
         *  builder via resolveModelForFramework, so generic tiers work uniformly
         *  across Claude and Codex. */
        model?: ModelTier | string;
        jobSlug?: string;
        triggeredBy?: string;
        maxDurationMinutes?: number;
        topicId?: number | 'platform';
        worktreeMode?: 'dev' | 'read-only' | 'doc-fix' | 'platform';
        worktreeSlug?: string;
        /** Per-call framework override; falls back to INSTAR_FRAMEWORK env, then claude-code. */
        framework?: IntelligenceFramework;
        /** Phase 6 local-model adapter — when set on a codex-cli launch,
         *  the spawned session uses `codex exec --oss --local-provider <p>`
         *  instead of OpenAI. Ignored for non-codex frameworks. */
        codexLocalProvider?: 'ollama' | 'lmstudio';
        /** Optional Claude Code per-session tool allowlist
         *  (mapped to `--allowedTools <comma-separated>`).
         *  When omitted, the spawn runs with full tools (back-compat).
         *  When an empty array, no `--allowedTools` flag is emitted —
         *  callers must explicitly pass at least one tool name to scope. */
        allowedTools?: string[];
        /** When true, a codex-cli spawn launches with full bypass so it can make
         *  MCP tool calls (e.g. threadline_send). Set ONLY by Threadline
         *  inbound-reply spawns — codex cancels MCP calls under any sandbox.
         *  Jobs leave this false and keep the workspace-write sandbox. No effect
         *  on non-codex frameworks. */
        codexAllowMcpTools?: boolean;
        /** When true, a claude-code spawn launches with NO project MCP servers
         *  (`--strict-mcp-config` + an empty `--mcp-config`), ignoring the project's
         *  `.mcp.json`. This is required for headless one-shot spawns that don't need
         *  MCP: the project's MCP set includes interactively-authenticated remote
         *  servers (e.g. Fathom's `mcp-remote`, the claude.ai connectors) that can't
         *  complete their OAuth handshake in a headless `claude -p` run, so the
         *  session HANGS on boot and never processes its prompt. Verified live: the
         *  mentor autonomous-fix loop session stalled ~4.5 min at 0.1% CPU on MCP
         *  init; with this flag a headless spawn boots in ~9s. No effect on Codex
         *  spawns (Codex MCP wiring is separate). */
        disableProjectMcp?: boolean;
        /** Threadline A2A continuity (claude-code HEADLESS only): when set, the
         *  headless `claude -p` spawn is launched with `--session-id <uuid>` so the
         *  transcript is created at a deterministic, caller-chosen id. The caller
         *  (ThreadlineRouter.spawnNewThread) stores this uuid as the thread's
         *  resume-map entry, so the next inbound message on the same thread can
         *  resume the exact conversation via `resumeSessionId`. Mutually exclusive
         *  with `resumeSessionId` (sessionId wins). No effect on non-claude
         *  frameworks or interactive spawns. */
        sessionId?: string;
        /** Threadline A2A continuity (claude-code HEADLESS only): when set, the
         *  headless `claude -p` spawn is launched with `--resume <uuid>` so it
         *  reloads the full prior transcript captured by an earlier `sessionId`
         *  spawn. This is what makes A2A follow-ups land in a warm session with
         *  full context instead of cold-spawning memoryless. Mutually exclusive
         *  with `sessionId` (sessionId wins when both are present). A stale uuid is
         *  covered by the existing resume-crash fallback. No effect on non-claude
         *  frameworks or interactive spawns. */
        resumeSessionId?: string;
    }): Promise<Session>;
    /**
     * Pre-spawn cap + memory-pressure gate for the reroute (june15-headless-spawn-
     * reroute PR2, findings O2/S1). Decides whether a rerouted-interactive spawn
     * may proceed:
     *  - cap reached (>= subscriptionPath.maxRerouted) OR host memory pressure
     *    elevated ('high'/'critical') → REFUSE.
     *  - under 'force': a refusal THROWS loudly (force guarantees zero `claude -p`,
     *    so there is no headless fallback) — and degradation-reports first.
     *  - under 'auto': a refusal returns `{ allow: false }` after degradation-
     *    reporting, so spawnSession falls through to the headless lane (the SDK
     *    pot still works; the host stays healthy).
     * Returns `{ allow: true }` when the reroute may proceed.
     */
    private evaluateRerouteGate;
    /**
     * F6 recurrence cap (spec "Structural guards" + Distrust Temporary
     * Success): the auto-mode headless fallback is self-healing — a reroute
     * that's BROKEN (cap stuck, pressure stuck high, continuity gap) would
     * silently fall back on EVERY spawn and look fine forever while the entire
     * June-15 reroute is dead. Count fallbacks in a rolling window; past the
     * cap, raise ONE escalated degradation event per window saying "dead, not
     * transiently degraded".
     */
    private rerouteFallbackTimes;
    private rerouteFallbackEscalatedAt;
    private recordRerouteFallback;
    /**
     * Spawn a claude-code job/A2A one-shot rerouted onto the interactive
     * (subscription) lane (june15-headless-spawn-reroute, PR2 "The core switch").
     *
     * This is the control-flow fork from spawnSession's headless branch. It:
     *  - builds an INTERACTIVE claude launch (no `-p`; the REPL opens and waits)
     *    via buildInteractiveLaunch, carrying the resolved model + the A2A
     *    `--session-id`/`--resume` continuity flag through unchanged;
     *  - splices the SAME `claudeHeadlessExtraFlags` (--allowedTools,
     *    --strict-mcp-config --mcp-config {}) the headless path uses — both are
     *    valid INTERACTIVE flags and the gate is framework-only, so the load-
     *    bearing no-project-MCP boot (avoids the ~4.5-min OAuth-remote-MCP hang)
     *    is preserved (spec F4/S3);
     *  - creates the tmux session with the same env isolation as the headless
     *    block PLUS `-x 200 -y 50` (prompt detection is tuned for the wide pane,
     *    spec F6);
     *  - enforces the memory-pressure + concurrency cap gate BEFORE rerouting
     *    (spec O2/S1): under 'auto', exceeding either falls back to the headless
     *    lane (degradation-reported); under 'force' the spawn is refused loudly;
     *  - appends a deterministic completion sentinel to the prompt and stamps the
     *    session with completionMode='pattern' + the per-session pattern + a hard
     *    maxLifetimeMinutes, so the monitor loop can reap a never-exiting REPL on
     *    sentinel-match or lifetime-expiry (spec O1, "the crux");
     *  - fires the ready-wait + inject ASYNCHRONOUSLY (does NOT block the return —
     *    callers expect the Session back promptly), reusing the same
     *    waitForClaudeReadyWithRetry + injectMessage machinery handleReadyAndInject
     *    uses.
     */
    private spawnReroutedInteractive;
    /**
     * Wait for Claude to be ready, then inject a prompt — the minimal ready-wait
     * + inject path shared by the rerouted-interactive spawn
     * (june15-headless-spawn-reroute). Factored from handleReadyAndInject's shape;
     * deliberately WITHOUT the resume-fresh-spawn fallback (a rerouted job's
     * delivery is best-effort — the monitor loop's lifetime cap bounds a dead
     * REPL, and the job reruns cleanly on its next trigger).
     */
    private injectAfterReady;
    /**
     * Check if a session is still running by checking tmux AND verifying
     * that the Claude process is running inside (not a zombie tmux pane).
     */
    isSessionAlive(tmuxSession: string): boolean;
    /**
     * Check if a session is still running by checking tmux AND verifying
     * that the Claude process is running inside (async version).
     * Used by the monitoring loop to avoid blocking the event loop.
     *
     * Previously only checked `tmux has-session` which missed zombie sessions
     * where tmux was alive but Claude had exited — causing stuck sessions
     * that blocked the scheduler for hours.
     */
    private isSessionAliveAsync;
    /**
     * Kill a session by terminating its tmux session.
     */
    killSession(sessionId: string): boolean;
    /**
     * Check if a tmux session has active (non-baseline) child processes.
     * Returns true if the session is doing real work — running tools, bash commands,
     * subagents, etc. Returns false if only baseline processes (MCP servers, caffeinate)
     * are running, meaning the session is truly idle.
     *
     * This is the ground truth for whether a session is active — it doesn't care about
     * terminal output patterns, topic bindings, or subagent trackers. If the process
     * tree shows work happening, the session is active. Period.
     */
    hasActiveProcesses(tmuxSession: string): boolean;
    /**
     * Accumulated CPU-seconds of a session's non-baseline descendant processes.
     *
     * Unlike `hasActiveProcesses` (which checks process EXISTENCE only), this reads
     * each descendant's accumulated CPU time. Comparing two samples across a window
     * reveals whether the process actually USED CPU in the interval — the signal
     * StaleSessionBackstop needs to tell a wedged-but-ALIVE session (0% CPU, e.g. a
     * hung `codex exec --json` job) from one genuinely working. A wedged codex job
     * keeps a live process (so `hasActiveProcesses` reads true and the stale-session
     * escalation never fires), but its CPU-seconds stay flat — which this exposes.
     *
     * Returns 0 on any failure; callers compare DELTAS, so a flat/zero reading just
     * reads as "no CPU progress" (the backstop's other signals + the operator-ask
     * escalation make that safe — it never auto-kills).
     */
    descendantCpuSeconds(tmuxSession: string): number;
    /**
     * Pane (shell) PID for each RUNNING session, keyed by instar session id.
     * Read-only: it issues a single `tmux list-panes` per running session and
     * returns whatever resolves — sessions whose pane can't be read are simply
     * omitted (never throws). Consumed by ResourceSampler to attribute per-session
     * CPU/RSS. The pane shell PID is the root of the session's process tree, so a
     * caller measuring it (and its descendants, via `ps`) sees the session's load.
     */
    getRunningSessionPanePids(): Array<{
        id: string;
        pid: number;
    }>;
    /**
     * Pure check: does this already-captured pane show Claude Code actively
     * working? Footer-hint based (see CLAUDE_WORKING_INDICATORS). Exposed so
     * callers that already hold a pane capture (e.g. verifyInjection) can reuse
     * the canonical signal without a second tmux capture. Public for unit tests.
     */
    paneShowsActiveWork(pane: string | null | undefined): boolean;
    /**
     * Is this session actively working right now? True when the captured pane
     * shows Claude Code's mid-turn footer ("esc to interrupt" / "tokens · esc" /
     * "ctrl+t to hide tasks") OR the session has a live, non-baseline child
     * process (a tool is running).
     *
     * The footer half is the discriminator that matters most for recovery: a
     * long extended-think on a large context shows the footer but spawns no
     * child process and writes nothing to the JSONL until the turn lands — which
     * is exactly the state the compaction-recovery loop used to misread as
     * "stuck" and re-inject into, burying the user's real message. A session
     * that is genuinely idle at its prompt, or one that has wedged and fast-fails
     * every turn, shows neither tell and returns false (recovery proceeds
     * unchanged). Never throws — a capture/ps failure resolves to false.
     */
    isSessionActivelyWorking(tmuxSession: string): boolean;
    /**
     * Capture the current output of a tmux session.
     */
    captureOutput(tmuxSession: string, lines?: number): string | null;
    /**
     * The last `lines` MEANINGFUL rows of a session's pane — blank-fill-immune.
     *
     * `capture-pane -S -N` returns the last N PHYSICAL rows, so in a tall pane
     * the meaningful text (idle prompt, modal, readiness marker) can sit above a
     * run of trailing blank rows and a small-N capture comes back entirely blank.
     * That made genuinely-idle sessions read as ACTIVE forever in the n=5 idle/
     * age checks (the #20/#47 inflated-blocker family; cycle-2 finding, task #77;
     * same class PR #818 fixed inside PromptGate). This captures a wider physical
     * window, trims trailing blanks (shared core/paneText semantics — interior
     * blanks preserved), and returns the last `lines` meaningful rows.
     */
    captureMeaningfulTail(tmuxSession: string, lines: number): string | null;
    /**
     * Transcript-activity probe — ground-truth liveness independent of the tmux
     * pane and process tree. A session doing MCP/tool work (e.g. driving Playwright)
     * runs that work OUT of the pane's process tree and, between tool calls, shows an
     * idle-looking prompt with no non-baseline child process — so the pane+procs
     * activity check reads it as idle and the age-kill terminal-reaped it mid-work
     * (2026-06-13 incident). The framework JSONL grows on every turn/tool event, so
     * a transcript modified within `withinMs` proves the session is alive and
     * producing. Returns true on a recent mtime; false on anything unprobeable
     * (no session id, '' path, missing file, stat error) — never block a kill on a
     * transcript we cannot read (the safe direction is to fall through to the
     * pane/procs verdict, not to keep a possibly-dead session alive forever).
     */
    isTranscriptRecentlyActive(session: Session, withinMs: number): boolean;
    private currentPaneCwd;
    private recordBuildContext;
    private withBuildContextRestoreNote;
    /**
     * Resolve the IntelligenceFramework a tmux session was spawned under
     * by reading the INSTAR_FRAMEWORK env we set in its tmux env block.
     * Cached so repeated injections don't re-shell out. Returns null when
     * the env isn't present (legacy sessions, non-instar tmux sessions).
     */
    private readonly sessionFrameworkCache;
    /** Invalidate cached framework for a tmux name. Call after killing or
     *  respawning a session whose name may be reused under a different
     *  framework — e.g., after a /route switch. */
    clearSessionFrameworkCache(tmuxSession: string): void;
    /** Public accessor for a session's resolved intelligence framework
     *  (claude-code | codex-cli), or undefined if it can't be determined.
     *  Cached per session. Used by sentinel wiring to pick the right
     *  activity-signal patterns when classifying a captured frame. */
    frameworkForSession(tmuxSession: string): IntelligenceFramework | undefined;
    private getSessionFramework;
    /**
     * Send input to a running tmux session.
     */
    sendInput(tmuxSession: string, input: string): boolean;
    /**
     * Send a tmux key sequence (without -l literal flag).
     * Use for special keys like 'C-c' (Ctrl+C), 'Enter', 'Escape'.
     * Unlike sendInput() which uses -l (literal), this sends key names directly.
     */
    sendKey(tmuxSession: string, key: string): boolean;
    /**
     * List all sessions that are currently running.
     * Pure filter — does not mutate state. The monitor tick handles lifecycle transitions.
     * WARNING: This calls synchronous tmux has-session for each session.
     * For health checks and non-critical callers, prefer getCachedRunningSessions().
     */
    listRunningSessions(): Session[];
    /**
     * Every tmuxSession name SessionManager has ever known (any status — running,
     * completed, failed, killed). Backs the OrphanProcessReaper's exact-id orphan
     * classification (UNIFIED-SESSION-LIFECYCLE §P0 #6): a process is an instar-
     * orphan ONLY when its tmuxSession exactly matches a name instar has tracked,
     * never via a project-prefix substring (which over-matches user-created
     * sessions that happen to share the prefix).
     */
    listKnownTmuxSessions(): Set<string>;
    /**
     * Get cached running session info (count + list) without blocking the event loop.
     * Updated asynchronously by the monitor tick every 5 seconds.
     * Safe to call from the health endpoint and other latency-sensitive paths.
     */
    getCachedRunningSessions(): {
        count: number;
        sessions: Session[];
    };
    /**
     * Fast startup purge — remove session records for sessions that are *definitively*
     * dead. Called once at server boot BEFORE monitoring starts, to prevent the death
     * spiral where stale records overwhelm startup and block health checks.
     *
     * UNIFIED-SESSION-LIFECYCLE §P1 (the 2026-05-27 fix): liveness is resolved from a
     * single `tmux list-sessions` via the tri-state oracle, NOT a per-session
     * `has-session` with a 1s timeout. The old code treated a timeout (tmux busy at
     * boot) identically to "session gone" and mass-purged live sessions — "9 of 9".
     * Now ONLY a `dead` verdict (server reachable + exact id absent) purges; `alive`
     * and `indeterminate` are kept. An indeterminate session lingers one extra tick
     * (cheap, caught by monitoring) rather than being falsely reaped (expensive).
     */
    purgeDeadSessions(): Promise<number>;
    /**
     * Get diagnostics for all running sessions, including staleness detection
     * and memory pressure. Used by the scheduler to build intelligent notifications
     * when jobs are blocked by session limits.
     */
    getSessionDiagnostics(): SessionDiagnostics;
    /**
     * Detect if a session has completed by checking output patterns.
     */
    detectCompletion(tmuxSession: string): boolean;
    /**
     * Per-session completion detection (june15-headless-spawn-reroute, PR2).
     * Scans the captured pane for THIS session's own `completionPatterns` (the
     * injected sentinel) rather than the global death-phrase config. A rerouted-
     * interactive REPL never exits and never prints the global death phrases on
     * success, so the global detectCompletion would never reap it. Used by the
     * monitor loop ONLY for completionMode==='pattern' sessions; the default lane
     * is byte-for-byte untouched. Returns false when the session has no
     * per-session patterns (defensive — should not happen for a 'pattern' session).
     */
    detectSessionCompletion(session: Session): boolean;
    /**
     * Reap completed/zombie sessions.
     */
    reapCompletedSessions(): string[];
    /**
     * Remove stale session state files for sessions that have been
     * killed or completed beyond the retention period.
     * Killed sessions: removed after 1 hour.
     * Completed sessions: removed after 24 hours.
     */
    cleanupStaleSessions(): string[];
    /**
     * Resolve the codex launch model, applying the rate-limit model-swap when
     * configured. For non-codex frameworks, or when codex.rateLimitModelSwap is
     * disabled / has no fallbackModel, returns the requested model unchanged with
     * ZERO disk I/O (the fast-path guard lives in resolveCodexLaunchModelWithUsage).
     * Best-effort: a usage-read failure resolves to the requested model — it never
     * blocks a spawn. Used by both the headless (spawnSession) and interactive
     * (spawnInteractiveSession) codex launch paths.
     */
    private resolveCodexLaunchModel;
    /**
     * Spawn an interactive Claude Code session (no -p prompt — opens at the REPL).
     * Used for Telegram-driven conversational sessions.
     * Optionally sends an initial message after Claude is ready.
     */
    spawnInteractiveSession(initialMessage?: string, name?: string, options?: {
        telegramTopicId?: number;
        slackChannelId?: string;
        slackThreadTs?: string;
        resumeSessionId?: string;
        framework?: IntelligenceFramework;
        codexLocalProvider?: 'ollama' | 'lmstudio';
        defaultModel?: string;
        /** Warm-session A2A (claude-code only): pin a deterministic --session-id so an
         *  eviction mid-thread can --resume losslessly (#746). Ignored when resuming. */
        sessionId?: string;
        /** Subscription & Auth Standard P1.3 (claude-code only): launch this session
         *  under the given account's config home (CLAUDE_CONFIG_DIR) — the account-swap
         *  mechanism. Additive; an explicit value always wins. When unset AND the
         *  spawn-account resolver is wired (pinSessionsToPool), B1 resolves the
         *  scheduler-picked pool home automatically so the interactive lane pins like
         *  the headless lane; unset + no resolver = inherit the parent's login. */
        configHome?: string;
        /** P1.3: record which subscription-pool account this session runs under. An
         *  explicit value wins; otherwise B1 fills it from the resolver-pinned account
         *  (kept in lockstep with configHome). */
        subscriptionAccountId?: string;
        /** Topic Profile §6 — per-topic thinking mode, threaded to the launch
         *  builder (claude `--effort` / codex `model_reasoning_effort`; no-op on
         *  frameworks without a reasoning knob). */
        thinkingMode?: import('./topicProfileValidation.js').ThinkingMode;
        /** Topic Profile — per-topic Claude Code `--effort` level pin, threaded to
         *  the launch builder as `--effort <level>` (claude-code only; strict no-op
         *  on other frameworks). A DIRECT CLI-flag pin, distinct from thinkingMode. */
        effort?: import('./topicProfileValidation.js').EffortLevel;
        /** Explicit per-spawn working directory (reap-notify spec R2.8 / L13 —
         *  a NAMED extension: no spawn path accepted a per-spawn cwd before).
         *  The resume-queue drainer threads a queue entry's recorded cwd /
         *  worktreePath here so an interrupted worktree session resumes in ITS
         *  tree, not the module-level project dir. Unset = projectDir (today's
         *  behavior, byte-for-byte). */
        cwd?: string;
    }): Promise<string>;
    /**
     * Wait for Claude to be ready, inject the initial message, and — when the
     * resume path crashes during startup — fall back to a fresh spawn carrying
     * the same message instead of silently dropping it.
     *
     * Why this exists: a stale --resume UUID can crash Claude during startup
     * (`Session died during startup` in the logs). Without fallback, the user's
     * first message after an idle pause vanishes; the presence proxy fires its
     * "session appears stopped" 5 minutes later; the user must re-send to
     * recover. The fallback closes that gap by retrying once without --resume.
     *
     * Single retry only — if the fresh-spawn also fails, we surface the error
     * via the existing degradation reporter and the bridge's own respawn paths.
     */
    private handleReadyAndInject;
    /**
     * Boot-time recovery for injects orphaned by a server restart (finding
     * 8d300555). Sweeps the durable pending-inject records: a still-alive
     * session gets its message re-delivered through the normal readiness path
     * (the live incident — codex idle at a fresh prompt, bootstrap unread); a
     * dead or expired record is reported via DegradationReporter and expired —
     * the loss becomes VISIBLE instead of vanishing with the old process.
     * At-least-once by design: server death between inject and clear means one
     * duplicate on the next boot, which beats a silent drop.
     */
    recoverPendingInjects(): Promise<{
        redelivered: string[];
        expired: string[];
        deadSession: string[];
        failed: string[];
    }>;
    /**
     * Spawn a scoped triage session with restricted tool access.
     * Unlike interactive sessions, triage sessions use --allowedTools + --permission-mode dontAsk
     * instead of --dangerously-skip-permissions. This gives them read-only access.
     *
     * Used by TriageOrchestrator for behind-the-scenes session investigation.
     */
    spawnTriageSession(name: string, options: {
        allowedTools: string[];
        permissionMode: string;
        resumeSessionId?: string;
    }): Promise<string>;
    /**
     * Inject a Telegram message into a tmux session.
     * Short messages go via send-keys; long messages are written to a temp file.
     *
     * Image handling: [image:/path] tags from Telegram photo downloads are
     * transformed into explicit instructions so Claude Code knows to read the
     * image file (it can natively view images via the Read tool).
     */
    /**
     * Inject a paste notification into a tmux session.
     * Uses the same injection path as Telegram/WhatsApp messages
     * so InputGuard provenance checks apply.
     */
    injectPasteNotification(tmuxSession: string, notification: string): string;
    /**
     * Inject a paste notification and CONFIRM it actually submitted (was not left
     * stuck at the prompt by the paste-end Enter race). Returns true only when the
     * injected marker is no longer sitting at the input prompt within the recovery
     * window — i.e. the session genuinely consumed the message. Observes outcome
     * across the same window verifyInjection (fired by injectMessage) uses to resend
     * Enter, so this does NOT duplicate recovery; it only reports the result.
     *
     * Used by topic-linkage reply surfacing, which must not treat a
     * dispatched-but-stuck inject as "delivered to the user" (the A2 bug: a stalled
     * inject silently resolved the commitment and suppressed the Telegram fallback).
     */
    injectPasteNotificationConfirmed(tmuxSession: string, notification: string): Promise<boolean>;
    injectTelegramMessage(tmuxSession: string, topicId: number, text: string, topicName?: string, senderName?: string, telegramUserId?: number, messageId?: number): boolean;
    /**
     * Clear the injection tracker for a topic when the agent sends a reply.
     * Called from the /telegram/reply/:topicId route.
     */
    clearInjectionTracker(topicId: number): void;
    /**
     * Get all pending injections (for diagnostics / event emission on session death).
     */
    getPendingInjection(tmuxSession: string): {
        topicId: number;
        injectedAt: number;
        text: string;
    } | undefined;
    /**
     * Inject a WhatsApp message into a tmux session.
     * Tags with [whatsapp:JID] and handles long messages via temp files.
     */
    injectWhatsAppMessage(tmuxSession: string, jid: string, text: string, senderName?: string): void;
    /**
     * Inject an iMessage into a tmux session.
     * Tags with [imessage:SENDER] and handles long messages via temp files.
     * Tracks injection for stall detection (uses a synthetic numeric topicId
     * derived from hashing the sender identifier).
     */
    injectIMessageMessage(tmuxSession: string, sender: string, text: string, senderName?: string): void;
    /**
     * Clear the injection tracker for an iMessage sender.
     * Called from the /imessage/reply/:recipient route.
     */
    clearIMessageInjectionTracker(sender: string): void;
    /**
     * Send text to a tmux session via send-keys, with Input Guard protection.
     *
     * When an InputGuard is configured, messages are checked for provenance
     * before injection. Suspicious messages still reach the session but with
     * a system-reminder warning injected afterward (async, non-blocking).
     *
     * For multi-line text, uses bracketed paste mode escape sequences so the
     * terminal treats newlines as literal text rather than Enter keypresses.
     * This avoids tmux load-buffer/paste-buffer which trigger macOS TCC
     * "access data from other apps" permission prompts.
     */
    injectMessage(tmuxSession: string, text: string): boolean;
    /**
     * Internal-only injection for trusted in-process recovery agents (the
     * sentinels). Bypasses InputGuard's topic-prefix provenance requirement so a
     * recovery nudge can reach a session that is NOT bound to any Telegram topic
     * (e.g. a developer's interactive Claude Code window). Without this path, the
     * rate-limit / socket-disconnect / silence sentinels silently no-op for
     * non-topic-bound sessions — the exact bug behind "the throttle never
     * recovered in my dev window."
     *
     * SECURITY BOUNDARY: this method is NOT exposed over HTTP. The only callers
     * are in-process sentinel recovery deps wired in server.ts. HTTP injection
     * continues to flow through injectMessage(), which enforces topic prefixing.
     * Every internal injection is recorded with source 'sentinel-recovery' so the
     * audit log can distinguish trusted recovery nudges from user/topic traffic.
     *
     * @param source - audit label for the trusted caller (e.g. 'sentinel-recovery')
     */
    injectInternalMessage(tmuxSession: string, text: string, source?: string): boolean;
    /**
     * Raw tmux send-keys injection. No validation — just sends text to the session.
     * Used by injectMessage after provenance checks pass.
     */
    private rawInject;
    /**
     * Verify an injection actually submitted by polling for a marker snippet
     * still present at the ❯ prompt. On Claude Code v2.1.105+, the Enter after
     * bracketed-paste-end is occasionally eaten by a race with the paste-end
     * sequence — and the recovery Enter can be eaten by the same race. Single-
     * shot verification leaves the user stuck when recovery also misses.
     *
     * Polls at 500/1500/3500/6500ms from injection (markerCheckSchedule),
     * stops as soon as the marker is no longer at ❯, and escalates the
     * recovery action across attempts: Enter, Enter, C-m, Enter+sleep+Enter.
     * Bounded — never more than 4 recovery actions. No-op when text submits
     * normally. Reports a single Degradation entry on first recovery firing.
     *
     * Runs asynchronously via setTimeout. Does not block the caller.
     */
    private verifyInjection;
    /**
     * Detect whether a marker snippet of injected text is still sitting at an
     * input prompt line in the captured pane. Matches the marker on the same
     * line as the prompt char or on the immediately-following line (TUIs wrap
     * long input across two visible rows).
     *
     * Recognises BOTH framework prompt chars: Claude Code's `❯` and codex's `›`
     * (U+203A). Marker-based detection is deliberate for codex — codex renders a
     * dim placeholder hint (e.g. "Explain this codebase") at an EMPTY `›` prompt
     * that is byte-identical to real input once color is stripped, so reading the
     * prompt text generically would false-fire on every idle codex session. The
     * injected marker never equals the placeholder, so matching it is robust.
     *
     * Exposed as a method so StuckInputSentinel and tests can reuse it.
     */
    isMarkerStuckAtPrompt(pane: string, marker: string): boolean;
    /**
     * Extract the distinguishing first-40-chars marker from injected text, the
     * same way verifyInjection and the pending-injection map do. Returns null if
     * the text is too short to be a reliable marker (<8 visible chars). Shared so
     * the recorded marker and the verification marker can never drift apart.
     */
    static extractInjectionMarker(injectedText: string): string | null;
    /**
     * Record that `text` was just injected into `tmuxSession` and has not yet been
     * confirmed submitted. Only the latest injection per session is tracked
     * (a newer message supersedes an older stuck one). No-op when the marker is
     * too short to track reliably. See the strandedDraftMarkers field for why this
     * primarily matters for codex sessions. */
    recordStrandedDraftMarker(tmuxSession: string, text: string, framework: string): void;
    /** Read the stranded-draft marker record for a session (or undefined). Used by
     *  StuckInputSentinel to do marker-based recovery for codex sessions. */
    getStrandedDraftMarker(tmuxSession: string): {
        marker: string;
        framework: string;
        injectedAt: number;
    } | undefined;
    /** Clear the stranded-draft marker for a session — called once the marker is
     *  no longer stuck at the prompt (confirmed submitted) or the session is
     *  gone. */
    clearStrandedDraftMarker(tmuxSession: string): void;
    /** All tmux sessions with a stranded-draft marker. Used by the sentinel to GC
     *  records for sessions that are no longer running. */
    strandedDraftMarkerSessions(): string[];
    /**
     * Fire one recovery action for a stuck input. Escalates the action by
     * attempt index — early attempts use plain Enter, later attempts use
     * C-m (literal carriage return) or Enter+sleep+Enter to defeat tighter
     * race windows. Bounded; called from verifyInjection's polling loop and
     * from the persistent StuckInputSentinel after a server restart.
     */
    fireStuckInputRecovery(tmuxSession: string, attempt: number): void;
    /**
     * Wait for Claude to be ready in a tmux session by polling output.
     * Looks for Claude Code's prompt character (❯) which appears when ready for input.
     */
    waitForClaudeReady(tmuxSession: string, timeoutMs?: number): Promise<boolean>;
    /**
     * Detect whether Claude Code's prompt is visible in a tmux session.
     * Also auto-accepts consent dialogs that block startup.
     *
     * Checks multiple indicators across a wider capture window to handle
     * varying TUI layouts (different terminal sizes, large banners, etc.)
     */
    private detectClaudePrompt;
    /**
     * Wait for Claude to be ready with a two-phase approach:
     * 1. Primary wait: poll for the prompt within the timeout
     * 2. Extended wait: if the session is alive but prompt wasn't detected,
     *    do a final longer check — Claude may have just finished loading
     *
     * This handles cases where Claude Code takes longer than expected due to
     * API auth refresh, large CLAUDE.md parsing, session-start hooks, or
     * network latency. The extended phase catches sessions that are "almost ready."
     */
    private waitForClaudeReadyWithRetry;
    tmuxSessionExists(name: string): boolean;
    private generateId;
}
//# sourceMappingURL=SessionManager.d.ts.map