import { type CreateSessionRequest, type HarnessAdapter, type HarnessKind, type HarnessSession, type LaunchOpts } from "../shared/types.js"; import type { ProjectAgentSession } from "@sapiom/agent-map"; import type { IngestCredentialProvider } from "./ingest-credentials.js"; export { AdapterNotFoundError, AgentSessionIdentityReservedError, ExternalHarnessError, McpSessionRestartUnavailableError, McpCredentialGenerationChangedError, SessionAlreadyLiveError, SessionNotReadyError, SessionNotResumeableError, UnknownSessionError, } from "./errors.js"; /** An optional server-owned submit guard rejected immediately at the PTY * boundary. Generic callers omit the guard and retain existing behavior. */ export declare class SessionInputGuardRejectedError extends Error { readonly staged: boolean; constructor(staged: boolean); } export declare class ProjectSessionScopeUnavailableError extends Error { readonly sessionId: string; readonly code = "PROJECT_SESSION_SCOPE_UNAVAILABLE"; constructor(sessionId: string); } type IPty = import("node-pty").IPty; type PtyForkOptions = import("node-pty").IPtyForkOptions; export type PtySpawnFn = (file: string, args: string[], options: PtyForkOptions) => IPty; /** * node-pty ships prebuilt native binaries (including a tiny `spawn-helper` * on macOS/Linux) rather than compiling from source. Observed in the wild: * a pnpm-managed install can extract that helper without its executable bit * set, which fails every single spawn with an opaque "posix_spawnp failed" * — nothing to do with the harness's own code, but fatal to every session * launch. Best-effort self-heal before the first real spawn; silently a * no-op if the file's missing (wrong platform/arch) or already executable. * Exported so scripts/e2e-live.ts's preflight check shares this exact fix * instead of duplicating it. */ export declare function ensureSpawnHelperExecutable(): Promise; /** * Reduce a dying session's raw terminal bytes to the human-readable text worth * persisting as {@link HarnessSession.exitTail}: strips ANSI escapes, drops * carriage-return redraws and other control bytes, collapses blank runs, and * keeps only the final {@link EXIT_TAIL_BYTES}. Best-effort — a window sliced * mid-escape can leave a fragment — but the abnormal-exit case it serves is * almost always a plain error banner rather than a full-screen TUI, so the * common result is clean. Returns null when nothing readable remains, so the * UI collapses the section instead of showing an empty box. * * Exported for direct unit testing. */ export declare function sanitizeExitTail(raw: string): string | null; export type SessionStatusListener = (session: HarnessSession, context: SessionStatusContext) => void; export type SessionDataListener = (chunk: string) => void; /** See `onActivity()`. */ export type SessionActivityListener = (harnessSessionId: string) => void; /** Server-private identity of the credential used to generate one MCP config. */ export interface McpCredentialLaunch { generation: number; credentialBearing: boolean; } export type LaunchOptsBuildResult = Omit & { /** Removed before LaunchOpts reaches an adapter; never leaves the server. */ mcpCredentialLaunch?: McpCredentialLaunch; }; /** * Builds the harness-specific part of LaunchOpts (generated system-prompt / * mcp-config / settings file paths). SessionManager owns cwd + harnessSessionId; * this lets the server layer inject config-file generation (profiles, MCP * wiring) without SessionManager needing to know how those files are produced. * May return synchronously or a Promise — generating the config files is * inherently async (they're written to disk), so `create()`/`resume()` await * whichever shape is handed in; existing sync test doubles keep working * unchanged (`await` on a plain value just resolves immediately). */ export type LaunchOptsBuilder = (harnessSessionId: string, req: Pick, context?: { promptAppendix?: string; /** Native CLI notice shown before a fresh session's first prompt. */ sessionStartSystemMessage?: string; agentMapIdentity?: ProjectAgentSession; resume?: boolean; }) => LaunchOptsBuildResult | Promise; export interface SessionManagerOptions { adapters: Partial>; /** Base URL the harness server is reachable at, e.g. http://127.0.0.1:4100. */ ingestUrl: string; /** Per-session capability issuer; shared ingest tokens are not supported. */ ingestCredentials: IngestCredentialProvider; /** Forwarded to sessions as SAPIOM_COLLECTOR_URL when set. */ collectorUrl?: string; /** Absolute path to the session registry file. Defaults to HARNESS_PATHS.sessions (expanded). */ sessionsPath?: string; /** Injectable for tests. Defaults to a lazily-loaded node-pty. */ spawnPty?: PtySpawnFn; /** Async node-pty loader seam used only when `spawnPty` is absent. Production * uses the module loader; tests use this to prove authorization is checked * after that final await and before PTY admission. */ loadSpawnPty?: () => Promise; buildLaunchOpts?: LaunchOptsBuilder; /** Live credential generation checked synchronously immediately before spawn. */ currentCredentialGeneration?: () => number; /** * Issues the one project identity a session belongs to. Called before every * spawn to revalidate cwd containment and the current principal. Throws * `ProjectSessionScopeUnavailableError` when no project can own `cwd`. */ resolveAgentMapIdentity: (sessionId: string, cwd: string, persisted?: ProjectAgentSession) => Promise; /** * Called once on load for a persisted session that predates project * identity. Joins the deepest already-open root containing `cwd` and never * mints a project for the cwd itself, so a session left behind by a removed * project cannot bring it back. Throws `ProjectSessionScopeUnavailableError` * when no open root owns `cwd`. */ migrateAgentMapIdentity: (sessionId: string, cwd: string) => Promise; /** Claims new-project lifecycle metadata after trusted scope resolution. */ prepareProjectSession?: (identity: ProjectAgentSession, request: CreateSessionRequest) => Promise<{ initialTitle?: string; }>; /** Content-free observability for persisted identity normalization. */ onProjectAgentIdentityMigration?: (event: { sessionId: string; outcome: "migrated" | "rejected" | "dropped"; }) => void; /** * Serializes coordinator ownership before a PTY generation is published. * `null` retracts a prepared epoch when pre-publication setup fails. */ onRuntimeEpochTransition?: (session: HarnessSession, runtimeEpoch: string | null) => Promise | void; now?: () => string; generateId?: () => string; /** Test seam for deterministic registry persistence failures. Production * uses the atomic mode-preserving writer below. */ writeSessionRegistry?: (file: string, serialized: string) => Promise; /** Test seam for faults after the private identity ledger becomes durable. */ writeAgentSessionOwnerRegistry?: (file: string, serialized: string) => Promise; /** * Writes HARNESS_CONTEXT_FILE for a session — the caller (server/index.ts's * `writeSessionContext`) owns resolving the session's `boundWorkflowPath` * against the live workflow registry and serializing the full workspace * state; this layer just decides *when* to call it. Called unconditionally * from `create()`, before the pty is spawned, so every session gets the * file regardless of entry point (REST, `autoCreateSession`) — no entry * point can skip it by calling `create()` directly. Defaults to a no-op so * tests that pass a fake `cwd` (e.g. `/tmp/proj`) never touch the real * filesystem unless they opt in. */ writeWorkspaceContext?: (session: HarnessSession) => Promise; /** * Validates or migrates HARNESS_CONTEXT_FILE before a resumed coding agent * is spawned. The caller owns the current/legacy/invalid/missing schema * policy and live registry resolution; this layer guarantees the operation * is awaited in the same pre-spawn window as the regenerated system prompt. * Defaults to a no-op for filesystem-free unit tests. */ prepareWorkspaceContext?: (session: HarnessSession) => Promise; /** * Injectable for tests (fake ptys carry fake pids that must never be * probed against real OS processes). Defaults to `defaultIsPidAlive`. */ isPidAlive?: (pid: number) => boolean; /** * Host platform, injectable so the Windows-only bracketed-paste assumption * (see `submitInput`) is provable from POSIX CI. Defaults to the real one. */ platform?: NodeJS.Platform; } export interface TrustedSessionCreateOptions { /** Future E5 seam for a server-authored planned builder assignment. */ agentMapIdentity?: (sessionId: string) => ProjectAgentSession; /** Server-owned initial title. Generic POST /sessions cannot set it. */ initialTitle?: string; /** Focused trusted context composed into the existing system prompt. */ promptAppendix?: (sessionId: string) => string; /** Server-authored native CLI orientation for a newly created session. */ sessionStartSystemMessage?: (sessionId: string) => string; } export interface TrustedSessionResumeOptions { /** Recomputed focused context for the resumed process. */ promptAppendix?: string; } export type AdapterIdentityState = "not-required" | "pending" | "ready" | "ambiguous" | "unavailable"; export interface SessionStatusContext { /** Exact live/retiring PTY generation, or null before a PTY exists. */ runtimeEpoch: string | null; } export type TrackedSessionInputResult = Readonly<{ accepted: boolean; phase: SessionInputWritePhase; error?: unknown; }>; export type SessionInputWritePhase = "not-written" | "text-staged" | "enter-written"; export interface SessionInputWriteLifecycle { /** Durable transition that must commit before the first PTY byte. */ beforeFirstWrite?: () => Promise; /** Synchronous final admission fence checked immediately before each write. */ canWriteNow?: () => boolean; /** Durable positive evidence that the writer returned before attempting * Enter. Errors at the Enter write are intentionally excluded. */ onNotSubmitted?: () => Promise; /** Synchronous byte-boundary observation for durable delivery recovery. */ onWritePhase?: (phase: SessionInputWritePhase) => void; } /** A prior partial PTY write could not be safely removed from the composer. */ export declare class SessionInputIsolationError extends Error { readonly code = "SESSION_INPUT_ISOLATION_REQUIRED"; constructor(); } export declare class SessionManagerClosingError extends Error { readonly code = "SESSION_MANAGER_CLOSING"; constructor(); } /** A real terminal write preempted a lower-priority background injection. */ export declare class SessionBackgroundInputPreemptedError extends Error { readonly staged: boolean; readonly code = "SESSION_BACKGROUND_INPUT_PREEMPTED"; constructor(staged: boolean); } export declare class SessionManager { /** * Internal tracked variant for retry-safe coordinator delivery. It never * turns an ambiguous write exception into zero-byte proof: callers receive * the furthest phase observed at the exact PTY boundary. */ submitInputTracked(id: string, text: string, options?: Readonly<{ canWrite?: () => boolean | Promise; lifecycle?: Omit; background?: boolean; }>): Promise; /** * Cancel only a lower-priority server-owned background submission. Unlike * write(), this does not forward bytes or preempt an ordinary user/API * submission. It is safe to call before staging begins; the coordinator's * submit guard covers that side of the race. */ preemptBackgroundInput(id: string): boolean; /** Abandon a fully staged line. Ctrl-C is a safe fallback here because a * successful full bracketed-paste write already carried its closing marker. */ private abandonStagedLine; /** * Recover only a previously poisoned composer, before any new user or * server-owned text is written. Closing bracketed paste (when required) and * abandoning the line are both non-submitting operations. Failure leaves * the handle poisoned and no caller payload is forwarded. */ private resetUnsafeComposer; private markComposerUnsafe; /** Close admission before a server shutdown snapshots live PTYs. */ beginShutdown(): void; /** Kill only the exact PTY generation a losing coordinator created. */ killIfRuntime(id: string, runtimeEpoch: string): Promise; /** Retire credential-bearing PTYs before scheduling keyless resumes. */ relaunchCredentialBearingSessions(throughGeneration: number): Promise; private trackCredentialRemovalResume; /** * Fail-closed admission for already-authenticated ingest work. A terminal * event may finish after its PTY retires, but a replacement handle always * takes ownership immediately and rejects every earlier epoch. */ acceptsIngestRuntimeEpoch(id: string, runtimeEpoch: string): boolean; /** True only for the exact PTY generation that is live right now. */ isCurrentRuntimeEpoch(id: string, runtimeEpoch: string): boolean; /** Server-only acknowledgement from an adapter-owned identity broker. */ setAdapterIdentityState(id: string, runtimeEpoch: string, state: Exclude): boolean; /** Exact-runtime adapter identity used by trusted background delivery. */ getAdapterIdentityState(id: string, runtimeEpoch: string): AdapterIdentityState; /** Opaque identity for the exact live PTY generation behind `id`. */ getRuntimeEpoch(id: string): string | null; private closing; /** Handle-local poison from a partial write whose composer cleanup could not * be proven. A replacement PTY is clean by construction; the same handle * must complete this reset before any later text or Enter is allowed. */ private readonly unsafeComposers; /** One text→Enter transaction may be staged per session. */ private readonly stagedInputs; /** Monotonic raw-input observations used to preempt background injection. */ private readonly terminalInputEpochs; /** Adapter-owned correlation for transcript-backed runtimes. This is kept * separate from terminal readiness: a TUI can be interactive before its * exact vendor transcript has been identified. */ private readonly adapterIdentityStates; /** Last cleanly retired PTY generation. It may finish already-admitted ingest * work while the session is exited, but loses immediately to a replacement. */ private readonly retiredRuntimeEpochs; private readonly onRuntimeEpochTransition; private readonly loadSpawnPty; private readonly issueIngestCredential; private readonly adapters; private readonly ingestUrl; private readonly revokeIngestToken; private readonly collectorUrl; private readonly sessionsPath; /** Server-private, digest-only tombstones for every vendor identity ever * accepted by a HarnessSession. Keeping this outside sessions.json avoids * leaking historical aliases through the browser DTO. */ private readonly agentSessionOwnersPath; private readonly spawnPty; private readonly buildLaunchOpts; private readonly currentCredentialGeneration; private readonly resolveAgentMapIdentity; private readonly migrateAgentMapIdentity; private readonly onProjectAgentIdentityMigration; private readonly rejectedProjectSessionMetadata; private readonly now; private readonly generateId; private readonly writeSessionRegistry; private readonly writeAgentSessionOwnerRegistry; private readonly writeWorkspaceContext; private readonly prepareWorkspaceContext; private readonly isPidAlive; private readonly platform; private readonly sessions; private readonly ptys; private readonly outputListeners; private readonly credentialRemovalRelaunches; private readonly statusEmitter; private readonly activityEmitter; /** Epoch ms of the last `onActivity` broadcast per session — see `recordActivity()`. */ private readonly lastActivityBroadcast; private writeQueue; private writeSeq; /** While a vendor-pointer/add-row transaction is committing its private * candidate snapshot, every ordinary registry write waits here and captures * state only after the candidate was published or rejected. */ private sessionRegistryIdentityFence; private readonly agentSessionOwners; /** Serializes the full authorize -> reserve -> pointer commit transition. * A file-level atomic rename alone is insufficient when two starts race the * in-memory ownership check before either write begins. */ private agentSessionIdentityQueue; private agentSessionOwnerWriteSeq; private initialized; constructor(options: SessionManagerOptions); /** * Loads the persisted registry. Any session left "starting"/"running" from * a previous process is marked "exited" — ptys don't survive a restart. */ init(): Promise; list(): HarnessSession[]; get(id: string): HarnessSession | undefined; /** True only when this process owns the live PTY behind the record. */ isLive(id: string): boolean; /** Recompute browser-safe state from private live-runtime stamps. */ reconcileMcpCredentialGeneration(currentGeneration: number): void; /** Replace one stale coding-agent runtime when its conversation is resumable. */ restartForMcpCredentials(id: string): Promise; private getAdapter; /** Recheck that the resolved project scope is still current before spawning. */ private revalidateAgentMapIdentity; create(req: CreateSessionRequest, trusted?: TrustedSessionCreateOptions): Promise; /** * Registers a purely historical (never-launched-by-this-harness) session so * it can subsequently be resumed via `resume()`. Called by * `POST /api/sessions/adopt` when a user opens a transcript-only history row * whose transcript the agent genuinely still holds — that row then resumes * for real instead of silently starting a fresh session in the directory, * which is what happened for as long as nothing called this. * * The record is born `exited` with `createdAt === lastActiveAt` (nothing has * run under our management yet). `resume()` is what gives it a pty, and the * adopt route pre-verifies resumability before registering, so this never * mints a record that can't be resumed. */ registerHistorical(input: { agentSessionId: string; harness: HarnessKind; cwd: string; title: string; lastActiveAt: string; }): Promise; /** * Resumes a stored conversation after the adapter confirms it can reopen it. * Claims the starting state before preparation. Setup failures run normal * exit cleanup and preserve the previous last-activity timestamp. */ resume(id: string, trusted?: TrustedSessionResumeOptions): Promise; /** * Close the session at the user's request. Returns kill()'s result: whether * a live or stale session was transitioned to exited. */ close(id: string): Promise; /** * Signals the session's pty to exit and returns a Promise that resolves * once the process is **actually gone** — not fire-and-forget. * * Resolution source (either one unblocks the promise): * 1. node-pty's own `onExit` event → markExited() → `handle.exited` resolves. * 2. Synthesized exit: kill()'s escalation fallback (SIGTERM → SIGKILL → * pid liveness check) → markExited() → `handle.exited` resolves. * 3. Synthesized exit from an external `sweepDeadSessions()` call that * happens to run during the escalation window → same path. * * The promise is bounded: after `KILL_ESCALATION_MS` the escalation sends * SIGKILL; after a further `KILL_ESCALATION_CONFIRM_MS` it synthesizes the * exit from an OS-level pid check regardless of node-pty's event. So the * worst-case resolution time is `KILL_ESCALATION_MS + KILL_ESCALATION_CONFIRM_MS` * (2500 ms at current constants), never infinite. * * Existing fire-and-forget callers keep working: an unawaited Promise is * fine and produces no floating-promise lint warnings when suppressed with * `void`. * * Returns false (resolved immediately) when the session has no live pty. * Returns true (resolved on actual death) when a pty was signalled. */ kill(id: string): Promise; /** * Kills every currently-live pty and returns a Promise that resolves when * all of them have actually exited (real or synthesized). Bounded by the * same escalation window as `kill()` — never hangs. * * Call this on server shutdown so the process actually exits instead of * waiting on orphaned claude/codex children. A bounded timeout can be layered * on top via `Promise.race` when callers need a hard deadline: * `await Promise.race([sessionManager.killAll(), sleep(5_000)])` */ killAll(): Promise; /** * Defensive liveness backstop, run periodically by the server: any * non-exited session whose pty process is provably gone gets its exit * synthesized. The specific transitions are already reconciled at their * source (create/resume pre-pty failures, kill()'s missed-exit fallback), * but node-pty has been observed to simply never fire `onExit` for a * process that died moments after spawning (see `kill()`) — and a process * that dies *on its own* that way has no kill()-style fallback watching * it. This sweep is the catch-all for that and any transition not yet * root-caused: a stale record shows as a ghost tab (non-exited status, no * live pty) until something reconciles it. */ sweepDeadSessions(): void; /** * Forward terminal bytes without submitting a separate prompt. * @throws SessionInputIsolationError when prior partial input cannot be cleared. */ write(id: string, data: string): boolean; /** * Inject a discrete prompt (macros, the Visualize button, `/api/sessions/:id/input`) * with proper submit semantics — distinct from `write()`, which is a raw * passthrough for live keystrokes from the terminal WS and must never add * this delay/splitting behavior. See `SUBMIT_DELAY_MS` for why non-empty * submitted text can't just be written as `${text}\r` in one call. * * Gated on readiness (see `HarnessSession.ready` / `isReadyEnough`): a * "running" pty can still be sitting on a blocking prompt that swallows * whatever's written to it. Briefly waits out `READY_GRACE_MS` for the * ordinary "fired a beat too early" case; throws `SessionNotReadyError` * — never silently proceeds — if the session still isn't ready after * that, so the caller (rest.ts, macros.ts) can surface a clear reason * instead of the input just vanishing. */ submitInput(id: string, text: string, submit?: boolean, canWrite?: () => boolean | Promise, background?: boolean, lifecycle?: SessionInputWriteLifecycle): Promise; resize(id: string, cols: number, rows: number): boolean; /** * Subscribe to a session's output. Replays the retained scrollback buffer * synchronously before returning so a reconnecting WS client sees recent * output immediately. A session that is `starting` without a pty yet is * also attachable; its listener receives the new pty's output once it * spawns. Returns undefined otherwise when the session has no live pty. */ attach(id: string, listener: SessionDataListener): (() => void) | undefined; onStatusChange(listener: SessionStatusListener): () => void; /** * Subscribe to a session producing terminal output — throttled (see * `ACTIVITY_BROADCAST_THROTTLE_MS`), not one event per pty.onData chunk. * Fires for every session regardless of whether anything has its * /ws/terminal socket open, unlike `attach()`. */ onActivity(listener: SessionActivityListener): () => void; /** * Leading-edge throttle: broadcasts immediately on the first byte after a * quiet period, then drops everything else for this session until * `ACTIVITY_BROADCAST_THROTTLE_MS` has elapsed — a busy TUI's onData fires * far more often than that, and callers (the SPA's per-tab pulse) only * care that the session is active right now, not each individual chunk. */ private recordActivity; /** * Maintain a readiness-only view of terminal output. Codex's Ratatui loop * emits a ~500-byte cursor/erase repaint every 80ms even when the visible * screen is unchanged. Counting those chunks as startup progress both * prevents the 700ms settle window from ever completing and pushes a real * sign-in/trust prompt out of the raw 4KB tail. Synchronized-output markers * give us an atomic repaint boundary: retain the latest repaint containing * visible text, keep a bounded union for diff-rendered blocking prompts, * and ignore control-only frames. Non-Ratatui adapters fall back to an * ordinary rolling buffer. */ private recordReadinessOutput; private hasVisibleReadinessContent; private noteReadinessContent; private commitReadinessOutput; setAgentSessionId(id: string, agentSessionId: string, source?: unknown, runtimeEpoch?: string): Promise; /** Resolve a current or historical vendor-id tombstone without disclosing * the raw alias anywhere outside this server process. Used by the generic * adopt route to preserve the scoped planner boundary after a rotation. */ getAgentSessionOwner(agentSessionId: string): HarnessSession | undefined; /** Whether the private ledger retains this current or historical alias, * including a fail-closed tombstone whose original registry row is absent. */ isAgentSessionIdentityReserved(agentSessionId: string): boolean; private setAgentSessionIdLocked; /** * Fold trusted host/UI input into one bounded command line. Only callers * that already crossed the boot-token terminal WS or protected/internal * submitInput boundary reach this method; raw ingest tokens cannot arm it. * Unsupported controls and pasted/multiline content fail closed for the * current line. Picker navigation after an exact `/resume` does not revoke * the already-armed one-shot authorization. */ private observeTrustedTerminalInput; /** A protected discrete submit owns its final synthetic CR separately from * the text phase. Embedded CR/LF is multiline prompt content, never a series * of trusted Enter gestures: invalidate the whole submitted text so neither * raw nor bracketed transport can smuggle an inner `/clear` or `/resume`. */ private observeTrustedSubmittedText; private rotationForTrustedLine; /** * Marks a session's TUI as genuinely interactive — see `HarnessSession.ready`. * Called either from the ingest pipeline when a SessionStart(-equivalent) * event is processed for this session (real hook for Claude Code, tailer- * translated for Codex), or from an adapter-declared readiness fallback. * Idempotent; a session that's exited or already ready is a silent no-op. */ setReady(id: string, runtimeEpoch?: string): void; /** * Whether `id` should be treated as ready to receive programmatic input * right now. A real/fallback `session.ready` signal normally suffices; an * immediate-fallback adapter gets one last recognized-blocker scan because * readiness is latched while its TUI can still paint a late onboarding * frame. Before readiness, immediate and legacy detect-only adapters require * at least `READY_SETTLE_MS` of quiet output (or the bounded liveness * ceiling) and a clean recent-screen view. Claude Code has no immediate * shortcut — its SessionStart hook (or preserved 20-second hook-timeout * path) controls readiness. */ private isReadyEnough; private isImmediateFallbackSettled; /** The best available current frame, excluding older diff repaint history. * Used after readiness has latched so ordinary agent output that quotes old * onboarding copy cannot permanently gate later writes. */ private currentReadinessOutput; /** A bounded union of recent diff repaints, including an unfinished current * repaint. This is intentionally only used before immediate readiness. */ private recentReadinessOutput; /** Preserve a recognized blocker across Ratatui's partial row repaints. The * latch is cleared only by a positive empty-composer match in a clean current * frame; merely failing to re-render every phrase is not evidence that the * screen was dismissed. */ private refreshImmediatePromptState; private hasImmediateBlockingPrompt; /** The original readiness-buffer check retained for Claude's hook-timeout * and legacy detect-only adapters. Their fallback semantics must not change * merely because Codex needs partial-frame reconstruction. */ private hasRetainedBlockingPrompt; /** Scan only the best available current frame; dismissed full-screen prompts * remain in older scrollback forever and must not block already-ready input. */ private hasCurrentBlockingPrompt; /** * Polls `isReadyEnough` until it's true, the session's pty goes away * (killed/exited — it's never going to become ready now), or `timeoutMs` * elapses. Used by `submitInput()` only — `write()` (raw terminal * keystrokes) must never wait on this, since a human answering the very * prompt this is waiting out is exactly how a session becomes ready. */ private waitUntilReady; /** Waits for all in-flight registry writes to settle. Useful before process * shutdown (and in tests that assert against the on-disk registry). */ flush(): Promise; /** * A session's default name is owned by the record, assigned once at * creation: the folder basename, counting up past every sibling ever * created in that folder so an exit or resume never relabels a neighbour. */ private defaultTitle; setTitle(id: string, title: string): void; /** Binds (or, with `null`, unbinds) the session's current workflow * selection. The caller (rest.ts) owns validating `workflowPath` against * the workflow registry and mirroring the binding into * HARNESS_CONTEXT_FILE — this only updates the in-memory/persisted * registry entry and broadcasts the change like any other status update. */ setBoundWorkflowPath(id: string, workflowPath: string | null): void; private spawn; /** * Publishes adapter-declared fallback readiness so the SPA's held first * prompt can be released even when the harness's real lifecycle signal * cannot arrive yet (Codex) or never arrived (Claude Code with a broken * SessionStart hook). * * Both modes are conservative on purpose: * - requires SOME output — a pty that hasn't drawn anything yet isn't an * interactive TUI, it's still starting; * - keeps waiting while an adapter-recognized blocking prompt is on screen, * so known trust/login/setup screens are not answered by injected input; * - dies with the handle (exit clears the interval; a replaced handle is * detected via the ptys map, same idempotency rule as markExited()). * * `"immediate"` (Codex) reuses the same quiet-window/ceiling and frame rule * as `isReadyEnough()`, but proactively calls `setReady()` once output is * stable or reaches the bounded liveness ceiling. That status event closes * the first-prompt deadlock: * Codex writes no rollout/session_meta before turn one, while the SPA waits * for `ready` before submitting turn one. Only an EXPLICIT declaration arms * this persistent state transition; detect-only legacy adapters retain their * request-time `isReadyEnough()` compatibility path without being promoted. * * `"hook-timeout"` (Claude Code) retains its generous 20s delay: a healthy * hook (1–3s) always wins the race, so behaviour only changes on machines * where the hook is already broken. */ private armReadyFallback; /** * Transitions a session to "exited". Shared by node-pty's own `onExit` * callback and `kill()`'s missed-event fallback (see `kill()`) — both are * racing to be the one that reports a given pty's death, so this is * idempotent: a stale/duplicate call (`this.ptys.get(id) !== handle`, * i.e. this handle was already replaced or already reported exited) is a * silent no-op rather than double-transitioning or clobbering a newer * session/handle that's since taken its place (e.g. a resume). */ private markExited; /** * The single place a session record flips to "exited" — shared by * `markExited()` (live-pty deaths), `create()`/`resume()`'s pre-pty * failure reconciliation, `kill()`'s stale-record path, and * `sweepDeadSessions()`. Returns the persist promise so callers that need * the registry durably updated before rethrowing (create/resume) can * await it; event-driven callers fire-and-forget it like any other write. */ private transitionExited; private emitStatus; private agentSessionIdentityDigest; private serializeAgentSessionIdentity; /** Load the private alias ledger before accepting ingest. Missing is the * one recoverable case (upgrade from an older build): current pointers seed * it exactly once, with sessions.json order deterministically choosing the * first owner of a legacy duplicate. Malformed/unreadable ledger state still * fails boot rather than silently forgetting rotation tombstones. */ private loadAgentSessionOwners; private reserveAgentSessionIdentity; private persistAgentSessionOwners; private persistIdentityCandidate; private enqueueRegistryWrite; /** Serializes writes so overlapping persist() calls can't interleave and * corrupt the registry file; a failed write doesn't poison later ones. */ private persist; private readonly prepareProjectSession; /** Publish a project's sessions in creation order. */ private readonly projectCreateQueues; private readonly pendingCreates; private serializeProjectCreate; /** Keep ordinary project roots discoverable during asynchronous launch * preparation, before their session rows can be published. Server-only. */ listPendingCreates(): Pick[]; private createWithId; } //# sourceMappingURL=session-manager.d.ts.map