import type { WebSocket } from "ws";
import type { ServerMessage } from "../ws/protocol.js";
import { EventBuffer } from "./event-buffer.js";
import { PromptQueue } from "./prompt-queue.js";
import { SearchService } from "../search/search-service.js";
import { RpcBridge } from "./rpc-bridge.js";
import type { SkillExpansion } from "../skills/resolve-skill-expansions.js";
import { SessionStore, type PersistedSession } from "./session-store.js";
import { type PromptParts } from "./system-prompt.js";
import { CostTracker, type SessionCost } from "./cost-tracker.js";
import type { ColorStore } from "./color-store.js";
import type { RoleManager } from "./role-manager.js";
import type { ToolManager } from "./tool-manager.js";
import type { ToolGroupPolicyStore } from "./tool-group-policy-store.js";
import { McpManager } from "../mcp/mcp-manager.js";
import { ProjectContextManager } from "./project-context-manager.js";
import { GoalStore } from "./goal-store.js";
import { PrStatusStore } from "./pr-status-store.js";
import type { GateStore } from "./gate-store.js";
import type { SandboxManager } from "./sandbox-manager.js";
import { WorktreePool } from "./worktree-pool.js";
import { type PipelineContext } from "./session-setup.js";
export type SessionStatus = "starting" | "preparing" | "idle" | "streaming" | "aborting" | "terminated";
/**
 * Returns true only for rpc events that represent genuine new user-visible
 * activity (a message, tool call, or end-of-turn). Lifecycle frames the
 * agent CLI emits automatically on resume (agent_start, agent_idle,
 * connection_state, state, session_title, etc.) return false so they don't
 * clobber the persisted `lastActivity` timestamp on restore / role-restart /
 * abort-restart paths.
 *
 * See goal `goal-fix-lastac-724b3421` for the bug this guards against.
 */
export declare function isUserVisibleActivity(event: any): boolean;
/** Provenance of a prompt enqueued into a session. Read by TeamManager on
 *  agent_start to decide whether to reset idle-nudge backoff counters.
 *  Only "user" and "system" reset the counter; everything else preserves it. */
export type PromptSource = "user" | "auto-nudge" | "task-notification" | "verification" | "system" | "agent";
export interface SessionInfo {
    id: string;
    title: string;
    cwd: string;
    status: SessionStatus;
    /** Monotonic version of `session.status`. Bumped on every status transition
     *  (via `broadcastStatus`). Heartbeats re-broadcast WITHOUT bumping so the
     *  client can treat them as idempotent. In-memory only — not persisted.
     *  See docs/design/unify-session-status.md. */
    statusVersion: number;
    createdAt: number;
    lastActivity: number;
    clients: Set<WebSocket>;
    rpcClient: RpcBridge;
    eventBuffer: EventBuffer;
    unsubscribe: () => void;
    isCompacting: boolean;
    titleGenerated: boolean;
    goalId?: string;
    /** Assistant type: "goal" | "role" | "tool" */
    assistantType?: string;
    /** Whether this session has a live HTML preview panel */
    preview?: boolean;
    /** If this is a delegate session, the parent session ID */
    delegateOf?: string;
    /** First-class parent session ID for visible child sessions (not delegate lifecycle). */
    parentSessionId?: string;
    /** Kind discriminator for first-class child sessions, e.g. "pr-walkthrough". */
    childKind?: string;
    /** Whether the session should be treated as read-only by clients/tools. */
    readOnly?: boolean;
    /** PR walkthrough job metadata for session-hosted walkthrough children. */
    walkthroughJobId?: string;
    walkthroughChangesetId?: string;
    walkthroughTargetKey?: string;
    /** Role in a team goal (e.g., 'coder', 'reviewer', 'tester', 'team-lead') */
    role?: string;
    /** The team goal ID this agent belongs to */
    teamGoalId?: string;
    /** Session ID of the team lead that spawned this agent */
    teamLeadSessionId?: string;
    /** Path to the git worktree for this session */
    worktreePath?: string;
    /** Task ID this session is working on */
    taskId?: string;
    /** Staff agent ID this session belongs to */
    staffId?: string;
    /** Pixel-art accessory ID for the Bobbit sprite overlay */
    accessory?: string;
    /** Whether this session runs inside a Docker sandbox */
    sandboxed?: boolean;
    /** Container ID if using a pooled Docker container */
    containerId?: string;
    /** Whether this is an automated non-interactive session (e.g. verification reviewer) */
    nonInteractive?: boolean;
    /** Which project this session belongs to */
    projectId?: string;
    /** Allowed tools for this session */
    allowedTools?: string[];
    /** Server-side prompt queue */
    promptQueue: PromptQueue;
    /** Error message captured when restoreSession() failed; cleared on successful revive. */
    restoreError?: string;
    /** In-flight persistSessionMetadata promise (awaited before terminate) */
    pendingMetadataPersist?: Promise<void>;
    /**
     * Model literal (`<provider>/<modelId>`) that was passed to pi-coding-agent
     * via `--model` at spawn time. When set, post-spawn `tryAutoSelectModel`
     * skips the redundant `setModel` RPC if it would bind the same model;
     * read-back verification still runs.
     */
    spawnPinnedModel?: string;
    /** Thinking level passed via `--thinking` at spawn time, if any. */
    spawnPinnedThinkingLevel?: string;
    /** True if the last agent turn ended due to a model/API error */
    lastTurnErrored?: boolean;
    /** Error message from the last errored turn (e.g. streaming JSON parse failure) */
    lastTurnErrorMessage?: string;
    /** Number of consecutive auto-retries attempted for transient errors on this turn */
    transientRetryAttempts?: number;
    /** Number of consecutive immediate (tick-0) redrains scheduled by
     * recoverPromptDispatch after a rejected dispatch. Bounded by
     * MAX_RECOVER_DRAIN_RETRIES to stop a busy-guard spin loop. Reset to 0 on a
     * successful dispatch and at each agent_end before the queue drains. */
    recoverDrainAttempts?: number;
    /** Count of consecutive agent turns that ended with stopReason:"error". Resets on any non-error message_end or explicit retry. */
    consecutiveErrorTurns?: number;
    /** Pending auto-retry timer, so we can cancel it if the session terminates */
    pendingAutoRetryTimer?: ReturnType<typeof setTimeout>;
    /** Whether tool calls were executed during the current/last turn */
    turnHadToolCalls?: boolean;
    /** Timestamp when the current streaming turn started */
    streamingStartedAt?: number;
    /** Number of agent turns that have completed (agent_end fired). Used by
     * tests to detect that a prompt has actually been processed end-to-end
     * — polling for `status==idle` alone races with the pre-prompt idle
     * state, so observability of “a turn finished” needs its own counter. */
    completedTurnCount?: number;
    /** Last user prompt text, for retry on fresh-response errors */
    lastPromptText?: string;
    /** Last user prompt images, for retry on fresh-response errors */
    lastPromptImages?: Array<{
        type: "image";
        data: string;
        mimeType: string;
    }>;
    /** Provenance of the last prompt enqueued to this session. Set by
     *  enqueuePrompt / deliverLiveSteer. Defaults to "user" when callers
     *  don't supply a source. Read by TeamManager.subscribeTeamLeadEvents. */
    lastPromptSource?: PromptSource;
    /** Pending grant request from the guard extension's long-poll */
    pendingGrantRequest?: {
        resolve: (result: {
            granted: boolean;
            tools?: string[];
        }) => void;
        reject: (err: Error) => void;
        toolName: string;
        toolGroup: string;
        timer: ReturnType<typeof setTimeout>;
        /** seq/ts of the original `tool_permission_needed` broadcast — replayed
         * verbatim to late-joining clients so we never burn a fresh global seq
         * on a unicast frame. See tests/perm-frame-late-joiner-seq-gap.test.ts. */
        seq: number;
        ts: number;
    };
    /** Tools granted via "one-time" mode — revoked on agent_end */
    oneTimeGrantedTools?: string[];
    /** Whether post-start setup (model, thinking, metadata) has completed */
    setupComplete?: boolean;
    /** Cached PromptParts for serving prompt-sections API */
    promptParts?: PromptParts;
    /**
     * FIFO queue of pending skill-expansion envelopes awaiting echo-back from
     * the agent. Each entry carries the modelText (what the agent will echo as
     * the user message body), the originalText we want the chat UI to display,
     * and the chip ranges. When a user-role message_end arrives whose text
     * equals `modelText`, we splice the matching envelope onto the message:
     * rewrite `content` to `originalText` and attach `skillExpansions`.
     */
    pendingSkillExpansions?: Array<{
        modelText: string;
        originalText: string;
        skillExpansions: SkillExpansion[];
    }>;
    /** Repo path (cached from worktree provisioning). */
    repoPath?: string;
    /** Active branch name. Mirrors the persisted store; stable for the session's lifetime. */
    branch?: string;
    /** Multi-repo: per-repo worktree paths from the pool claim. Stable for the session's lifetime. */
    repoWorktrees?: Array<{
        repo: string;
        repoPath: string;
        worktreePath: string;
    }>;
    /**
     * Shadow ledger of steer texts that have been dispatched to the SDK
     * (`rpcClient.steer()` resolved) but have not yet echoed back as a
     * user-role `message_end`. This is design §6.1 mitigation B: because
     * Bobbit cannot extend the upstream pi-coding-agent RPC bridge to
     * proxy `AgentSession.clearQueue()`, we mirror the SDK's text-match
     * splice logic at our layer for the abort-reconciliation case only.
     *
     * Lifecycle:
     *   - push: after `await rpcClient.steer(text)` resolves in `_dispatchSteer`.
     *   - splice: on `message_end(role:user)` whose body matches the front entry,
     *     mirroring `_processAgentEvent`'s `_steeringMessages.indexOf` removal.
     *   - drain: in `_reconcileAfterAbort` — re-enqueue at front so the next
     *     turn redispatches them as a steered batch.
     *
     * Bounded growth: every entry has a paired SDK echo or an abort-drain;
     * neither path is silently dropped.
     */
    inFlightSteerTexts?: string[];
    /**
     * Latest in-flight `message_update` payload. Set on every `message_update`
     * event with a non-empty `event.message`; cleared on `message_end`,
     * `agent_end`, and `process_exit`. Used to splice the in-flight row into
     * `getMessages` snapshot responses so a snapshot taken while an assistant
     * message is mid-stream still represents the row — the agent flushes to
     * `.jsonl` only on `message_end`, so without this the snapshot drops the
     * row entirely (H3-D convergent loss across tabs). See the H3 design doc.
     */
    latestMessageUpdate?: {
        id?: string;
        message: any;
    };
}
export { spliceInFlightMessage, spliceInFlightSteers } from "./splice-inflight-message.js";
export { broadcastStatus } from "./session-status.js";
/** Push a raw event into the session's EventBuffer (assigning seq/ts) and
 *  broadcast the `{type:"event"}` frame to all clients with seq/ts attached.
 *  This is the single emit path for live agent events — every call site that
 *  used to do `eventBuffer.push(ev); broadcast(clients, {type:"event", data:ev})`
 *  must route through here so envelope fields stay consistent.
 *  See docs/design/streaming-dedup-reorder.md §4.2. */
export declare function emitSessionEvent(session: {
    clients: Set<WebSocket>;
    eventBuffer: EventBuffer;
    pendingSkillExpansions?: Array<{
        modelText: string;
        originalText: string;
        skillExpansions: SkillExpansion[];
    }>;
}, truncated: unknown): void;
/** Snapshot of the active pending tool-permission grant, returned to clients
 * that attach mid-perm so they can replay the SAME seq/ts as the original
 * broadcast — never allocating a fresh sequence number. Pinned by
 * tests/perm-frame-late-joiner-seq-gap.test.ts. */
export interface PendingToolPermissionSnapshot {
    toolName: string;
    group: string;
    roleName: string;
    roleLabel: string;
    lastPromptText?: string;
    seq: number;
    ts: number;
}
export interface SessionManagerOptions {
    /** Override the path to pi-coding-agent cli.js */
    agentCliPath?: string;
    /** Path to a custom system prompt file */
    systemPromptPath?: string;
    /** Color store for session color cleanup on terminate */
    colorStore?: ColorStore;
    /** Role manager for looking up role definitions */
    roleManager?: RoleManager;
    /** Tool manager for generating tool documentation in system prompts */
    toolManager?: ToolManager;
    /** Group policy store for resolving group-level default tool grant policies */
    groupPolicyStore?: ToolGroupPolicyStore;
    /** Preferences store for aigw auto-model detection */
    preferencesStore?: import("./preferences-store.js").PreferencesStore;
    /** Project config store for reading project defaults */
    projectConfigStore?: import("./project-config-store.js").ProjectConfigStore;
    /** Project context manager for per-project store resolution */
    projectContextManager?: ProjectContextManager;
    /** Config cascade for three-layer resolution (builtin → server → project) */
    configCascade?: import("./config-cascade.js").ConfigCascade;
    /** PR status store — single source of truth for goal PR URLs. */
    prStatusStore?: PrStatusStore;
}
export declare class SessionManager {
    private sessions;
    /** Sessions with at least one attached WS client. Keeps heartbeat work proportional to active viewers. */
    private sessionsWithConnectedClients;
    private agentCliPath?;
    private systemPromptPath?;
    /** @internal Test-only session store (used when no PCM is available). */
    private _testStore;
    /** @internal Test-only cost tracker (used when no PCM is available). */
    private _testCostTracker;
    /** @internal Test-only search index (used when no PCM is available). */
    private _testSearchIndex;
    private colorStore?;
    private roleManager?;
    private toolManager?;
    private groupPolicyStore?;
    private preferencesStore?;
    private projectConfigStore?;
    private projectContextManager;
    private prStatusStore;
    private mcpManager;
    private worktreePools;
    sandboxManager: SandboxManager | null;
    sandboxTokenStore: import("../auth/sandbox-token.js").SandboxTokenStore | null;
    configCascade: import("./config-cascade.js").ConfigCascade | null;
    /**
     * Optional inbox nudger. Wired late from `server.ts` boot via
     * `setInboxNudger` so the nudger's `onAgentStart` hook can clear its
     * per-staff `nudgePending` flag when a staff session begins streaming
     * a turn. Stays null on test paths that don't construct a nudger.
     */
    private _inboxNudger;
    private _onPrCreationDetected?;
    private _verificationHarness?;
    private _terminationListeners;
    /**
     * Count of agent-CLI `*.jsonl` transcripts on disk that don't match any
     * persisted `agentSessionFile` (and are newer than the most recent
     * `lastActivity` in the store). Populated by `restoreSessions()` via
     * `scanOrphanedTranscripts()`. Surfaced via `GET /api/health` so the
     * splash UI can show a one-line banner. Zero means "clean".
     */
    orphanedTranscriptsCount: number;
    /** @internal Non-PCM test path only. */
    private _testGoalManager;
    /** @internal Non-PCM test path only. */
    private _testTaskManager;
    private purgeInterval;
    /** Heartbeat timer: re-broadcasts the current `session_status` for every
     *  active session every STATUS_HEARTBEAT_INTERVAL_MS, WITHOUT bumping
     *  `statusVersion`. Self-heals any client that missed a transition frame.
     *  See docs/design/unify-session-status.md §3.4. */
    private _statusHeartbeatTimer;
    private static readonly STATUS_HEARTBEAT_INTERVAL_MS;
    /** Cached aigw model discovery result (url → { models, timestamp }) */
    private _aigwModelCache;
    private static AIGW_CACHE_TTL_MS;
    setOnPrCreationDetected(cb: (session: SessionInfo) => void): void;
    setVerificationHarness(harness: import("./verification-harness.js").VerificationHarness): void;
    /** Subscribe to session termination events. Listeners are invoked synchronously. */
    addTerminationListener(fn: (sessionId: string, info: {
        projectId?: string;
        reason: "terminated" | "archived" | "purged";
    }) => void): void;
    setSandboxManager(manager: SandboxManager | null): void;
    setInboxNudger(nudger: import("./inbox-nudger.js").InboxNudger | null): void;
    /**
     * Subscribe to sandbox container recovery events.
     * Call after both SessionManager and SandboxManager are initialized.
     */
    subscribeSandboxRecovery(): void;
    /**
     * Recover all sandbox sessions after a container has been recreated.
     * Verifies/repairs/recreates worktrees, then re-restores each session.
     */
    private recoverSandboxSessions;
    private _trackConnectedSession;
    private _untrackConnectedSession;
    /**
     * Re-broadcast the current `session_status` for every session that has
     * connected clients, WITHOUT bumping `statusVersion`. Heartbeat. Idempotent
     * on the client (they ignore frames whose version <= lastStatusVersion).
     */
    private _emitStatusHeartbeat;
    constructor(options?: SessionManagerOptions);
    /** Resolve goal tools extension path via toolManager cascade (with fallback). */
    private getGoalToolsExtensionPath;
    /** Resolve team lead extension path via toolManager cascade (with fallback). */
    private getTeamLeadExtensionPath;
    /** Resolve proposal tools extension path via toolManager cascade (with fallback). */
    private getProposalToolsExtensionPath;
    getProjectContextManager(): ProjectContextManager | null;
    /** Resolve the SessionStore for a given project. Requires projectId when PCM is active. */
    getSessionStore(projectId?: string): SessionStore;
    /** Resolve the GoalStore for a given project. Requires projectId when PCM is active. */
    getGoalStoreForProject(projectId?: string): GoalStore;
    /** Resolve the GateStore for a goal. */
    getGateStoreForGoal(goalId: string): GateStore | null;
    /** Resolve SearchService for a project. Requires projectId when PCM is active. */
    getSearchIndexForProject(projectId?: string): SearchService;
    /** Resolve the correct SessionStore for an in-memory session by ID. */
    private resolveStoreForSession;
    /** Resolve the correct SessionStore for any session by ID (in-memory or persisted). Returns null if not found. */
    private resolveStoreForId;
    /** Resolve the correct CostTracker for a session based on its project. */
    private resolveCostTracker;
    /** Resolve the correct SearchService for a session based on its project. */
    private resolveSearchIndex;
    /** Resolve a goal across all project contexts. */
    private resolveGoal;
    /** Whether Docker sandbox mode is enabled in project config. */
    get isSandboxEnabled(): boolean;
    /** Get the sandbox manager (used by team-manager and verification-harness). */
    getSandboxManager(): SandboxManager | null;
    /** Build a PipelineContext from this manager's fields. Requires projectId when PCM is active. */
    buildPipelineContext(projectId?: string): PipelineContext;
    /** Network name for sandbox containers. */
    private static readonly SANDBOX_NETWORK;
    /**
     * Ensure the Docker bridge network for sandboxed containers exists.
     * Idempotent — checks with `docker network inspect` first.
     */
    ensureSandboxNetwork(): Promise<string>;
    /**
     * Remove the sandbox Docker network. Non-fatal if it doesn't exist
     * or has connected containers.
     */
    cleanupSandboxNetwork(): Promise<void>;
    private resolveSandboxCwdOffset;
    /**
     * Apply Docker sandbox wiring to bridge options.
     * Shared by createSession(), restoreSession(), and createDelegateSession().
     * Returns true if sandbox was applied, false if sandbox is not configured.
     *
     * With the new per-project sandbox architecture, this:
     * - Gets the ProjectSandbox for the project
     * - Gets the container ID
     * - Sets up credentials and token (one per project, not per session)
     * - Sets bridgeOptions.containerId
     * - The CWD is the container-internal worktree path (set by caller or /workspace)
     */
    private applySandboxWiring;
    /** Get a CostTracker for a specific project. Requires explicit projectId when PCM is active. */
    getCostTracker(projectId?: string): CostTracker;
    /** Return persisted cumulative cost for a session, without creating a zero-cost record. */
    getSessionCost(sessionId: string): SessionCost | undefined;
    /** Merge authoritative persisted cost into a state snapshot when cost exists. */
    withSessionCostInState(sessionId: string, data: unknown): unknown;
    /** Build the cumulative cost_update payload used for attach/reconnect hydration. */
    getSessionCostUpdate(sessionId: string): Extract<ServerMessage, {
        type: "cost_update";
    }> | null;
    /** Broadcast cumulative persisted cost to connected clients, if this session has cost data. */
    broadcastSessionCost(session: SessionInfo): void;
    private resolveTaskIdForSession;
    getMcpManager(): McpManager | null;
    /**
     * Initialize the worktree pool for a repo. Pre-creates worktrees in the
     * background so new sessions can claim one instantly (~0ms) instead of
     * waiting for `git worktree add` + `npm ci` + `git push` (~10-30s).
     */
    initWorktreePoolForProject(projectId: string, repoPath: string, componentsResolver?: () => import("./project-config-store.js").Component[], targetSize?: number, worktreeRoot?: string, baseRefResolver?: () => string | undefined): void;
    /** @deprecated Use initWorktreePoolForProject instead. */
    initWorktreePool(repoPath: string, _setupCommand?: string, targetSize?: number): void;
    /** Get the worktree pool for a specific project. */
    getWorktreePool(projectId?: string): WorktreePool | null;
    /** Get all worktree pools (for shutdown / API). */
    getAllWorktreePools(): Map<string, WorktreePool>;
    /** Drain and remove a project's worktree pool (for project deletion). */
    removeWorktreePool(projectId: string): Promise<void>;
    initMcp(cwd: string): Promise<void>;
    /** Build a markdown list of available workflows for the goal assistant prompt. */
    private _buildWorkflowList;
    /**
     * Build the full set of CLI args for tool activation, including guard extensions,
     * MCP proxies, and builtin/extension activation.
     *
     * Returns the args array to prepend to bridgeOptions.args.
     */
    /**
     * Resolve the effective allowed tools for a role.
     * If the role has explicit allowedTools, use those.
     * Otherwise, compute from the full policy cascade (honouring the allow default).
     */
    private resolveEffectiveAllowedTools;
    private buildToolActivationArgs;
    private resolveSessionRole;
    /** Generate tool docs and inject into prompt parts before assembly. */
    private assemblePrompt;
    private _assemblePrompt;
    /**
     * Build the skills-catalog list for autonomous activation.
     * Returns undefined when activate_skill is not allowed for the session
     * (signalling "no Available Skills section" to assembleSystemPrompt).
     */
    private computeSkillsCatalog;
    /** Get cached PromptParts for serving prompt-sections API.
     *  If not cached (e.g. dormant session), rebuild from session metadata. */
    getPromptParts(sessionId: string): PromptParts | undefined;
    /** Broadcast queue state to all clients and persist. */
    broadcastQueueUpdate(sessionId: string): void;
    private broadcastQueue;
    /**
     * Enqueue a prompt. If the agent is idle and queue was empty,
     * dispatch immediately. Otherwise add to queue and broadcast.
     * If the agent is idle but queue has items, enqueue and drain.
     *
     * Returns whether this exact prompt was dispatched immediately or merely
     * queued behind existing/busy work. Callers must not infer that from the
     * post-call session status: direct dispatch intentionally marks the session
     * streaming before the RPC resolves.
     */
    enqueuePrompt(sessionId: string, text: string, opts?: {
        images?: Array<{
            type: "image";
            data: string;
            mimeType: string;
        }>;
        attachments?: unknown[];
        isSteered?: boolean;
        /** Original text was already expanded into this when sent to the model. */
        modelText?: string;
        /** Resolved slash-skill expansions, in original-text order. UI-only metadata. */
        skillExpansions?: SkillExpansion[];
        /** Provenance of this prompt. Defaults to "user". Read by TeamManager
         *  on agent_start to decide whether to reset idle-nudge backoff counters. */
        source?: PromptSource;
    }): Promise<{
        status: "dispatched" | "queued";
    }>;
    /**
     * Deliver a live steer to a streaming session.
     *
     * Before calling rpcClient.steer(), aborts any in-flight `bash_bg wait`
     * HTTP handlers for this session so the agent is not stuck inside a
     * tool call while the steer is queued on the SDK side. The bg processes
     * themselves are left running untouched.
     *
     * Returns the underlying rpcClient.steer() promise so callers can await
     * or attach their own error handler.
     */
    deliverLiveSteer(sessionId: string, message: string, opts?: {
        source?: PromptSource;
    }): Promise<unknown>;
    /**
     * Promote a queued message to steered and reorder.
     * If the agent is streaming, all steered+undispatched messages are
     * batched and dispatched immediately via rpcClient.steer() so they
     * are injected at the next tool boundary (PI-10).
     */
    steerQueued(sessionId: string, messageId: string): boolean;
    /**
     * Single dispatch site for steered prompts. Removes rows from promptQueue
     * *before* awaiting rpcClient.steer() so the SDK becomes the sole
     * authority for in-flight steer text. On RPC failure, rows are
     * re-enqueued at the front in original order (steered group still sorts
     * first via PromptQueue.reorder()).
     *
     * Tool-boundary callers may pre-pop rows with dequeueAllSteered() — in
     * that case remove() is a no-op (returns false), broadcastQueue stays
     * idempotent.
     */
    private _dispatchSteer;
    /**
     * Splice an entry from the shadow ledger when its echo arrives.
     * Matches the SDK's text-match removal at agent-session.js:265-280:
     * find the first index whose text equals the user-message body, splice it.
     * Silent no-op for non-matching messages (regular prompts, follow-ups,
     * skill-expansion echoes whose body has been rewritten).
     */
    private _consumeSteerEcho;
    /**
     * Drain the shadow ledger and re-enqueue any unresolved steers at the
     * front of promptQueue as steered rows. Called from agent_end while
     * `wasAborting`, and from `forceAbort` after killing the bridge — both
     * cases where a steer the SDK accepted may never echo because the turn
     * was torn down. The next drainQueue picks the rows up as a steered
     * batch via `_dispatchSteer`, redispatching exactly once.
     */
    private _reconcileAfterAbort;
    /** Reorder queued messages to match the given ID list. */
    reorderQueue(sessionId: string, messageIds: string[]): void;
    /** Remove a queued message. */
    removeQueued(sessionId: string, messageId: string): boolean;
    private markPromptDispatchStreaming;
    private recoverPromptDispatch;
    private dispatchDirectPrompt;
    /**
     * Called when the agent becomes idle (agent_end) or when a new message is
     * enqueued while idle. Dequeue and dispatch the next message if any exist.
     *
     * Always dispatches via `prompt` RPC (not `steer`) because the agent is
     * idle at this point — `steer` is only meaningful mid-turn.
     *
     * Sets status to "streaming" optimistically to prevent a race where another
     * enqueuePrompt call sees idle+empty and dispatches a second concurrent prompt.
     */
    private drainQueue;
    /**
     * Handle agent events that track error state and control queue draining.
     * Called from every event listener before broadcasting.
     * - Tracks message_end with stopReason "error" so we can suppress queue draining.
     * - On agent_end, skips drainQueue if the turn ended with an error.
     */
    private handleAgentLifecycle;
    /**
     * Auto-retry a turn that ended with a transient model/streaming error.
     *
     * Two policies, selected by error class:
     *
     * - Provider overload / rate-limit (`isProviderBackoffError`, e.g.
     *   Anthropic `overloaded_error`, `rate_limit_error`, HTTP 429/529):
     *   effectively unbounded retries with exponential backoff capped at
     *   5 minutes and ±20% jitter. Overload events can legitimately last
     *   10+ minutes; surfacing the error to the user is worse than waiting.
     *
     * - Other transient glitches (malformed tool-call JSON, ECONNRESET, etc.):
     *   bounded 3 attempts at 1s/2s/4s, after which the error surfaces and
     *   the user can manually retry.
     */
    private maybeAutoRetryTransient;
    /**
     * Cancel any pending auto-retry timer for this session and broadcast a
     * synthetic `auto_retry_cancelled` event so UI banners can clear. Safe to
     * call when no timer is pending — no-op in that case.
     */
    private cancelPendingAutoRetry;
    /**
     * Retry after a model/API error. Behaviour depends on context:
     * - Fresh response error (no tool calls): re-sends the original user prompt
     * - Mid-work error (tool calls already executed): sends a system continuation
     */
    retryLastPrompt(sessionId: string, opts?: {
        auto?: boolean;
    }): Promise<void>;
    /**
     * Grant a tool or tool group to a session's role and restart the session
     * so it picks up the new tools. Returns the updated list of allowed tools.
     *
     * @param mode - Grant persistence mode:
     *   - "persistent" (default): updates role YAML permanently
     *   - "session-only": adds to session.allowedTools in memory only (survives until session ends/restarts)
     *   - "one-time": adds to session.allowedTools + tracks for revocation on agent_end
     */
    grantToolPermission(sessionId: string, toolName: string, scope: "tool" | "group", group?: string, mode?: "persistent" | "session-only" | "one-time"): Promise<string[]>;
    /**
     * Called by the guard extension's long-poll endpoint. Creates a pending
     * grant request, broadcasts to UI clients, and returns a promise that
     * resolves when the user grants/denies or after a 5-minute timeout.
     */
    requestToolGrant(sessionId: string, toolName: string, toolGroup: string): Promise<{
        granted: boolean;
        tools?: string[];
    }>;
    /**
     * Called when the user clicks "Deny" in the UI grant dialog.
     * Resolves the pending grant request with `{ granted: false }` so the
     * guard extension's long-poll returns immediately instead of waiting 5 min.
     */
    denyToolPermission(sessionId: string, _toolName: string): void;
    /**
     * Restart a session's agent process so it picks up updated role/tools.
     * Stops the current agent, then restores from the persisted session file
     * which re-applies tool activation with the updated role.
     */
    private _restartSessionWithUpdatedRole;
    /**
     * Snapshot the per-session monotonic counters that the client keeps in
     * lockstep with the server: the streaming-event `seq` (EventBuffer.lastSeq)
     * and the canonical `statusVersion`. Used by `restartAgent` /
     * `_restartSessionWithUpdatedRole` to seed the freshly-built EventBuffer
     * and SessionInfo so the client's `_highestSeq` and `_lastStatusVersion`
     * trackers — which never get reset because the WS stays open across the
     * respawn — keep applying live frames instead of silently dropping them as
     * "duplicates".
     *
     * The numbers we hand back are the high-water marks. The post-restart code
     * primes the new buffer with `seedNextSeq(lastSeq + 1)` and the new
     * SessionInfo with `statusVersion: lastVersion`; the very next live frame
     * therefore lands at seq = lastSeq + 1 / version = lastVersion + 1, which
     * advances both client trackers naturally.
     */
    private _snapshotStreamingFrameOfReference;
    /**
     * Respawn a session's agent process in-place while WS clients stay attached.
     *
     * Owns the snapshot/unsubscribe/stop/restore/re-attach/broadcast dance shared
     * by `restartAgent`, `_restartSessionWithUpdatedRole`, `recoverSandboxSessions`,
     * and the in-memory branch of `ensureSessionAlive`.
     *
     * The streaming frame-of-reference is snapshotted AFTER `unsubscribe()` so a
     * final in-flight `agent_end`-style event cannot race past `lastSeq`. The
     * carry-over fields (`_restartFrameOfReference`, `_overrideAllowedTools`)
     * are stashed on the persisted-session record for `restoreSession()` to
     * consume, then unconditionally cleared in `finally`.
     */
    private _respawnAgentInPlace;
    /**
     * Restart the agent process for a session whose process has died.
     * Stops any remnant process, then restores from persisted state.
     * Re-attaches existing WS clients so the user can keep working.
     */
    restartAgent(sessionId: string): Promise<void>;
    /**
     * Check an event for usage data and record it via the cost tracker.
     * Broadcasts a cost_update to connected clients if cost data is found.
     */
    private trackCostFromEvent;
    /**
     * Restore sessions from disk on startup.
     * Re-spawns agent processes and uses switch_session to resume each one.
     */
    restoreSessions(): Promise<void>;
    private restoreOneSession;
    private addDormantSession;
    private restoreWalkthroughSubmitEnv;
    private restoreSession;
    createSession(cwd: string, agentArgs?: string[], goalId?: string, assistantType?: string, opts?: {
        rolePrompt?: string;
        roleName?: string;
        role?: string;
        accessory?: string;
        env?: Record<string, string>;
        taskId?: string;
        staffId?: string;
        allowedTools?: string[];
        workflowContext?: string;
        worktreeOpts?: {
            repoPath: string;
        };
        reattemptGoalId?: string;
        sandboxed?: boolean;
        projectId?: string;
        sessionId?: string;
        sandboxBranch?: string;
        sandboxBaseBranch?: string;
        sandboxCwdOffset?: string;
        skipAutoModel?: boolean;
        skipAutoThinking?: boolean;
        initialModel?: string;
        initialThinkingLevel?: string;
        preExistingAgentSessionFile?: string;
        parentSessionId?: string;
        childKind?: string;
        readOnly?: boolean;
        walkthroughJobId?: string;
        walkthroughChangesetId?: string;
        walkthroughTargetKey?: string;
    }): Promise<SessionInfo>;
    /**
     * Create a delegate session — a real session that runs a task on behalf of a parent session.
     * The delegate gets a system prompt built from AGENTS.md + instructions.
     * After creation, the instructions are automatically sent as the first prompt.
     * Returns the session info immediately (the prompt runs asynchronously).
     */
    createDelegateSession(parentSessionId: string, opts: {
        instructions: string;
        cwd: string;
        title?: string;
        context?: Record<string, string>;
    }): Promise<SessionInfo>;
    /**
     * Wait for a session to become idle (not streaming).
     * Returns immediately if already idle.
     * Rejects on timeout.
     */
    waitForIdle(sessionId: string, timeoutMs?: number): Promise<void>;
    /**
     * Wait for a session to enter the streaming state.
     * Returns immediately if already streaming.
     * Rejects on timeout (callers typically `.catch(() => {})` to fall through).
     *
     * Symmetric to `waitForIdle` — used after dispatching a prompt to a resumed
     * session that is currently idle, so the caller can confirm the new turn
     * has actually started before racing against `waitForIdle` again.
     */
    waitForStreaming(sessionId: string, timeoutMs?: number): Promise<void>;
    /**
     * Get the final assistant output from a session's messages.
     */
    getSessionOutput(sessionId: string): Promise<string>;
    /** Query the agent for its session file and save metadata to disk */
    /** After compaction, refresh messages and state for all connected clients. */
    refreshAfterCompaction(session: SessionInfo): Promise<void>;
    /**
     * Runs metadata persistence (and retries model/thinking if early setup missed).
     * Called after the first agent turn completes.
     */
    private _finishSessionSetup;
    /**
     * best-ranked model when gateway is configured, otherwise does nothing
     * (pi-coding-agent uses its own built-in default).
     */
    /** Resolve a role-level model override for the session, if any. */
    private resolveRoleModel;
    /** Resolve a role-level thinkingLevel override for the session, if any. */
    private resolveRoleThinkingLevel;
    /**
     * Resolve the model to pin at spawn time for a session, given its role &
     * project. Mirrors `tryAutoSelectModel`'s precedence: role override →
     * `default.sessionModel` pref. Returns `undefined` for the aigw-fallback
     * case so post-spawn discovery + setModel still runs.
     *
     * Public so verification-harness and respawn paths can use the same
     * resolution logic.
     */
    resolveInitialModel(role: string | undefined, projectId: string | undefined): string | undefined;
    /**
     * Resolve the thinking level to pin at spawn time for a session.
     * Mirrors `tryApplyDefaultThinkingLevel`: role override →
     * `default.sessionThinkingLevel` pref → "medium". Returns `undefined`
     * for invalid values so the agent's built-in default applies.
     */
    resolveInitialThinkingLevel(role: string | undefined, projectId: string | undefined): string | undefined;
    /**
     * Resolve the review/QA model to pin at spawn time. Mirrors the
     * verification-harness precedence: role override → `default.reviewModel`.
     */
    resolveInitialReviewModel(role: string | undefined, projectId: string | undefined): string | undefined;
    private tryAutoSelectModel;
    /** Apply default thinking level from preferences (per-model). */
    private tryApplyDefaultThinkingLevel;
    persistSessionMetadata(session: SessionInfo): Promise<void>;
    getSession(id: string): SessionInfo | undefined;
    /**
     * Get the pending tool permission request for a session, if any.
     * Used to send the permission card to newly connecting clients.
     */
    getPendingToolPermission(id: string): /* includes replayed seq: number; ts: number */ PendingToolPermissionSnapshot | undefined;
    /**
     * Register an externally-created RPC bridge as a viewable session.
     * Used for LLM review sub-agents in verification harness so users can watch them live.
     * Returns an unsubscribe function to call when the session ends.
     */
    registerExternalSession(id: string, rpcClient: RpcBridge, opts: {
        title: string;
        cwd: string;
        role?: string;
        goalId?: string;
        teamGoalId?: string;
        projectId?: string;
    }): () => void;
    listSessions(): Array<{
        id: string;
        title: string;
        cwd: string;
        status: string;
        createdAt: number;
        lastActivity: number;
        lastReadAt?: number;
        clientCount: number;
        isCompacting: boolean;
        goalId?: string;
        assistantType?: string;
        goalAssistant?: boolean;
        roleAssistant?: boolean;
        toolAssistant?: boolean;
        delegateOf?: string;
        parentSessionId?: string;
        childKind?: string;
        readOnly?: boolean;
        walkthroughJobId?: string;
        walkthroughChangesetId?: string;
        walkthroughTargetKey?: string;
        role?: string;
        teamGoalId?: string;
        teamLeadSessionId?: string;
        worktreePath?: string;
        taskId?: string;
        staffId?: string;
        accessory?: string;
        nonInteractive?: boolean;
        preview?: boolean;
        reattemptGoalId?: string;
        sandboxed?: boolean;
        projectId?: string;
    }>;
    /**
     * Get all session IDs for a goal, including terminated sessions from the store.
     * Useful for cost aggregation where terminated sessions still have cost data.
     */
    getAllSessionIdsForGoal(goalId: string): string[];
    /** Record that the user viewed this session. Updates lastReadAt only — never lastActivity. */
    markSessionRead(id: string): boolean;
    setTitle(id: string, title: string, opts?: {
        markGenerated?: boolean;
    }): boolean;
    /**
     * Generate an AI-summarized goal title and rename the session.
     * Fire-and-forget — does NOT check titleGenerated (independent of first-message auto-title).
     */
    generateGoalTitle(sessionId: string, goalTitle: string): void;
    private _generateGoalTitleAsync;
    /** Update session metadata fields and persist. */
    updateSessionMeta(id: string, updates: {
        role?: string;
        teamGoalId?: string;
        worktreePath?: string;
        repoPath?: string;
        branch?: string;
        repoWorktrees?: Record<string, string>;
        accessory?: string;
        nonInteractive?: boolean;
        teamLeadSessionId?: string;
        delegateOf?: string;
        parentSessionId?: string;
        childKind?: string;
        readOnly?: boolean;
        walkthroughJobId?: string;
        walkthroughChangesetId?: string;
        walkthroughTargetKey?: string;
    }): boolean;
    /**
     * Ensure the session has an entry in the persistent store.
     * When a session is first created, store.put() is called asynchronously
     * (fire-and-forget) so it may not have completed yet. This ensures
     * draft operations work even before persistence is complete.
     */
    private ensureStoreEntry;
    /** Get a draft for a session by type. */
    getDraft(id: string, type: string): unknown | undefined;
    /** Set a draft for a session by type. Returns false if session not found. */
    setDraft(id: string, type: string, data: unknown): boolean;
    /** Delete a draft for a session by type. */
    deleteDraft(id: string, type: string): boolean;
    /**
     * Assign a role to an existing session by killing the agent, reassembling
     * the system prompt with the role instructions, and respawning with
     * `switch_session` to preserve conversation history.
     */
    assignRole(id: string, role: {
        name: string;
        promptTemplate: string;
        accessory: string;
    }): Promise<boolean>;
    /**
     * Generate a title for a session on the first user prompt.
     * Called immediately when the user sends a message, not after the agent replies.
     */
    tryGenerateTitleFromPrompt(sessionId: string, userText: string): void;
    private getTitleGenOptions;
    private autoGenerateTitleFromText;
    /**
     * Generate a title for any session by id — live or archived. Returns the
     * generated title, or null if no messages were available. Persists the
     * title and broadcasts to any connected clients (live sessions only).
     * Used by `POST /api/sessions/:id/generate-title` for the rename dialog
     * when the user is editing a non-focused session.
     */
    generateTitleForAnySession(id: string): Promise<string | null>;
    autoGenerateTitle(session: SessionInfo): Promise<void>;
    /**
     * Ensure a session's subprocess is alive. If the session is terminated or
     * dormant, attempt to restore it from persisted data.
     * Throws if the session cannot be restored.
     */
    ensureSessionAlive(sessionId: string): Promise<void>;
    /** Write the human-readable model name to a file so shell extensions can read it at commit time. */
    private _writeModelNameFile;
    /** Update the model name file for a session (called from WS handler on setModel). */
    updateModelNameFile(sessionId: string, modelId: string): void;
    /** Persist model provider/id so archived sessions can display model info. */
    persistSessionModel(sessionId: string, provider: string, modelId: string): void;
    /** Persist per-session image generation model override. Validates against the
     * registered image-model registry first; mirrors the WS handler's defence-in-depth
     * check so any code path that lands here can't poison session state with an
     * unknown (provider, modelId). */
    persistSessionImageModel(sessionId: string, provider: string, modelId: string): void;
    /** True when (provider, modelId) is registered as an available image model. */
    isKnownImageModel(provider: string, modelId: string): boolean;
    /** Resolve the image generation model for a session, falling back to the system default. */
    getImageModelForSession(sessionId: string): {
        provider: string;
        id: string;
    } | undefined;
    /** Latest user turn text for request-scoped policy checks such as model override gating. */
    getLastPromptText(sessionId: string): string | undefined;
    terminateSession(id: string): Promise<boolean>;
    /** Get persisted session metadata by ID (live or dormant). */
    getPersistedSession(id: string): PersistedSession | undefined;
    /** Get an archived session's metadata. */
    getArchivedSession(id: string): PersistedSession | undefined;
    /** Archive a session directly in the store (for dormant/store-only sessions). */
    storeArchive(id: string): boolean;
    /** Update metadata on an archived session (stored in the session store). */
    updateArchivedMeta(id: string, updates: {
        teamLeadSessionId?: string;
        parentSessionId?: string;
        childKind?: string;
        readOnly?: boolean;
        walkthroughJobId?: string;
        walkthroughChangesetId?: string;
        walkthroughTargetKey?: string;
    }): boolean;
    /** Parse the .jsonl file for an archived session and return messages. */
    getArchivedMessages(id: string): Promise<unknown[]>;
    /** List archived sessions in the same format as listSessions(). */
    listArchivedSessions(): Array<{
        id: string;
        title: string;
        cwd: string;
        status: string;
        createdAt: number;
        lastActivity: number;
        lastReadAt?: number;
        clientCount: number;
        isCompacting: boolean;
        goalId?: string;
        assistantType?: string;
        delegateOf?: string;
        parentSessionId?: string;
        childKind?: string;
        readOnly?: boolean;
        walkthroughJobId?: string;
        walkthroughChangesetId?: string;
        walkthroughTargetKey?: string;
        role?: string;
        teamGoalId?: string;
        teamLeadSessionId?: string;
        worktreePath?: string;
        taskId?: string;
        staffId?: string;
        accessory?: string;
        preview?: boolean;
        reattemptGoalId?: string;
        sandboxed?: boolean;
        archived: boolean;
        archivedAt?: number;
    }>;
    /** Permanently purge a single archived session immediately. */
    purgeArchivedSession(id: string): Promise<boolean>;
    /** Purge all archived sessions older than 7 days. */
    purgeExpiredArchives(): Promise<void>;
    /** Internal: purge a single archived session — delete files, worktree, store entry. */
    private purgeOneSession;
    /** Remove search index entries for a session. Used when removing a session from the store. */
    private cleanupSearchForSession;
    /**
     * Try to recover a session's .jsonl file when agentSessionFile is empty.
     * The agent CLI stores files as: <sessionsDir>/<cwd-slug>/<timestamp>_<uuid>.jsonl
     * We scan the CWD-derived directory for a .jsonl created close to the session's createdAt.
     *
     * Public so the continue-archived REST handler can resolve the source
     * `.jsonl` path for legacy persisted sessions whose `agentSessionFile`
     * field was never populated.
     */
    recoverSessionFile(ps: PersistedSession): string | null;
    /**
     * Clean up orphaned session worktrees that have no matching active session.
     * Best-effort — logs warnings but never throws.
     */
    cleanupOrphanedSessionWorktrees(repoPath: string): Promise<void>;
    /**
     * List orphaned session worktrees without deleting them.
     * Same detection logic as cleanupOrphanedSessionWorktrees but read-only.
     */
    listOrphanedSessionWorktrees(repoPath: string): Promise<Array<{
        path: string;
        branch: string;
    }>>;
    /**
     * List orphaned non-interactive sessions (e.g. verification reviewers)
     * that have no tracking in the verification harness. Read-only.
     */
    listOrphanedNonInteractiveSessions(): Promise<Array<{
        id: string;
        title: string;
        createdAt: number;
    }>>;
    /**
     * Terminate a list of orphaned non-interactive sessions.
     * Returns the number actually terminated.
     */
    terminateOrphanedSessions(sessionIds: string[]): Promise<number>;
    /**
     * Get statistics about expired archives (past 7-day retention).
     */
    getExpiredArchiveStats(): Promise<{
        count: number;
        totalSizeBytes: number;
    }>;
    /** Start the archive purge schedule — call after restoreSessions(). */
    startPurgeSchedule(): void;
    addClient(sessionId: string, ws: WebSocket): boolean;
    removeClient(sessionId: string, ws: WebSocket): void;
    /**
     * Abort the agent. If the graceful abort doesn't resolve within a timeout,
     * force-kill the agent process and restart it so the session remains usable.
     */
    forceAbort(id: string, gracePeriodMs?: number): Promise<void>;
    /**
     * One-shot migration: heal sessions that lost their `staffId` association
     * before the staffId-persistence fix landed. Delegates to the standalone
     * `backfillStaffIds` helper in `staff-backfill.ts` so the algorithm can
     * be unit-tested without dragging in `SessionManager`'s dependency graph.
     *
     * See `staff-backfill.ts` for the full behavioural contract.
     */
    backfillStaffIds(staffManager: import("./staff-backfill.js").BackfillStaffManager): number;
    shutdown(): Promise<void>;
}
/**
 * Resolve sandbox tokens from the unified sandbox_tokens config key.
 * Falls back to legacy behavior (sandbox_credentials + sandbox_host_token_overrides + sandbox_github_token)
 * when sandbox_tokens is not set.
 */
export declare function resolveSandboxTokens(prefs?: import("./preferences-store.js").PreferencesStore | null, projectConfig?: import("./project-config-store.js").ProjectConfigStore | null, secretsStore?: import("./secrets-store.js").SecretsStore | null): Record<string, string>;
/**
 * Legacy credential resolution from sandbox_credentials + sandbox_host_token_overrides + sandbox_github_token.
 * Used as fallback when sandbox_tokens is not configured.
 */
export declare function resolveLegacySandboxCredentials(prefs?: import("./preferences-store.js").PreferencesStore | null, projectConfig?: import("./project-config-store.js").ProjectConfigStore | null): Record<string, string>;
//# sourceMappingURL=session-manager.d.ts.map