import { EventEmitter } from 'node:events'; import { TmuxControl } from './tmux-control.js'; import { type CortexAgentContext } from './spawn-args.js'; import type { NormalizedEvent } from '../normalize/event-types.js'; /** Subset of JsonlTail surface the session depends on — lets tests inject a mock. */ export interface JsonlTailLike extends EventEmitter { start(): Promise; stop(): Promise; readonly path?: string; } export interface TuiSessionDeps { tmux: TmuxControl; /** Factory creating a tail for the given jsonl path. Defaults to real JsonlTail in production. */ tailFactory: (jsonlPath: string) => JsonlTailLike; /** @deprecated No longer used — the jsonl file appears only after the first submit, so there is * nothing to wait for at spawn. Kept so existing callers/tests construct without changes. */ waitForJsonlMs?: number; /** Fast-fail window (ms) for a fresh turn producing no jsonl output. Tests override with a small * value. Defaults to JSONL_FIRST_EVENT_TIMEOUT. */ firstEventTimeoutMs?: number; /** Delay (ms) between pasting the prompt and sending Enter. Tests set 0 to submit synchronously. * Defaults to PASTE_SUBMIT_DELAY_MS (needed so Claude's Ink TUI registers the paste). */ pasteSubmitDelayMs?: number; /** Max time (ms) to poll capture-pane for the Claude TUI readiness marker after a fresh spawn, * before the first paste. Tests set 0 to skip the wait (mocked tmux never renders a pane). * Defaults to PANE_READY_TIMEOUT. */ paneReadyTimeoutMs?: number; /** Poll interval (ms) for the pane-readiness wait. Defaults to PANE_READY_POLL_MS. */ paneReadyPollMs?: number; } export interface ClaudeTuiSessionConfig { channel: string; sessionId: string; /** Pool key used to deduplicate sessions per channel/thread (DR-0008). */ sessionKey: string; /** Working directory of the claude process — must match the cwd used in past sessions for --resume. * Determines jsonl path via Claude's `~/.claude/projects//.jsonl` convention. */ cwd: string; /** True iff the session is being resumed (`--resume`) rather than freshly created (`--session-id`). */ needsResume: boolean; tools?: string | null; systemPrompt?: string | null; appendSystemPrompt?: string | null; model?: string | null; claudeAgent?: string | null; pluginDirs?: string[] | null; outputStyle?: string | null; extraOption?: Record | null; mcpConfigPath?: string; callbackSource?: string | null; scheduleTaskId?: string | null; anthropicBaseUrl?: string; extraEnv?: Record; context?: CortexAgentContext; deps: TuiSessionDeps; } export interface TuiAgentResult { sessionId: string; total_cost_usd: number | null; num_turns: number | null; rateLimited: boolean; rateLimitMessage: string | null; planFilePath: string | null; enteredPlanMode: boolean; exitedPlanMode: boolean; askUserQuestions: Array<{ toolUseId: string; questions: any[]; }>; finalOutput: string | null; } export interface SendMessageOptions { onProgress?: ((progress: { num_turns: number; total_cost_usd: number | null; duration_ms: number | null; }) => void) | null; onAssistantMessage?: ((text: string) => void) | null; onToolUse?: ((name: string, input: any) => void) | null; /** Streaming callback: fires for EVERY NormalizedEvent including turn_complete. Used by adapter-level * wrappers (ClaudeAdapter.spawn) to plumb events into the per-turn event stream. */ onEvent?: ((event: NormalizedEvent) => void) | null; files?: any[]; } /** * TUI-mode Claude session. Each instance owns one tmux session and one jsonl tail. Turns run * serially through {@link sendMessage}; cancellation via {@link cancelCurrentTurn} sends Esc + C-u * to interrupt the in-flight model response and clear the prompt buffer. * * @see DR-0012 §3.2 — turn lifecycle: paste → Enter → await turn_complete → resolve. */ export declare class ClaudeTuiSession { readonly channel: string; readonly sessionId: string; readonly sessionKey: string; readonly cwd: string; readonly tmuxName: string; readonly jsonlPath: string; private readonly tmux; private readonly tailFactory; private readonly firstEventTimeoutMs; private readonly pasteSubmitDelayMs; private readonly paneReadyTimeoutMs; private readonly paneReadyPollMs; private readonly config; private tail; private normalizer; private alive; private needsResume; private currentTurn; private idleTimer; private maxTimer; private turnIdleTimer; private firstEventTimer; constructor(config: ClaudeTuiSessionConfig); isAlive(): boolean; private ensureSpawned; /** * Poll capture-pane until the Claude TUI prompt is interactive (or the timeout elapses). Returns * as soon as {@link PANE_READY_MARKER} appears. On timeout it logs and returns anyway — better to * attempt the paste than to hard-fail, and the first-event watchdog still bounds a dead session. * Tests pass paneReadyTimeoutMs=0 to skip entirely (mocked tmux renders no pane). */ private waitForPaneReady; sendMessage(userMessage: string, options: SendMessageOptions): Promise; /** * Cancel the currently in-flight turn. Sends Escape (interrupts model generation), then C-u * (clears any text still in the prompt buffer — Esc alone does NOT clear it; without this step * the next sendMessage would concatenate with the stale buffer contents). * * @see DR-0012 §3.5 — full cancel protocol. */ cancelCurrentTurn(): Promise; private handleRawEvent; private handleNormalizedEvent; private completeTurn; private resetIdleTimer; private startTurnIdleTimer; private bumpTurnIdleTimer; /** * One-shot fast-fail watchdog for the window between submitting a prompt and the first jsonl * event. Replaces the old 5s file-appear wait that used to fail fast at spawn — now that the * tail is non-blocking, a Claude that never starts would otherwise hang until TURN_IDLE_TIMEOUT * (60 min). On fire: reject the pending turn with a descriptive error and kill the session. * Disarmed by {@link clearFirstEventWatchdog} on the first jsonl event of the turn. */ private armFirstEventWatchdog; private clearFirstEventWatchdog; close(): void; kill(): boolean; } /** * Mirror Claude Code's convention: jsonl session transcript lives at * ~/.claude/projects//.jsonl * where dash-encoded-cwd is the absolute cwd with BOTH `/` AND `.` replaced by `-` * (leading slash → leading `-`; dotfiles like `.cortex` → `--cortex`). * * Empirically verified against `~/.claude/projects/` directory contents on Claude 2.1.141+: * `/home/fangxin/.cortex` → `-home-fangxin--cortex` * `/home/fangxin/Cortex` → `-home-fangxin-Cortex` * `/tmp/cortex-spike-tui` → `-tmp-cortex-spike-tui` * * The DR-0012 spike ran in `/tmp/cortex-spike-tui` (no dots), so the dot-encoding rule was * missed initially — without it, sessions under `~/.cortex/` (the default DATA_DIR) would * have JsonlTail watching a non-existent path and timing out on first turn. */ export declare function computeJsonlPath(cwd: string, sessionId: string): string; /** * Decide whether a TUI session should spawn with `--resume ` (vs `--session-id `). * * `--resume` only succeeds when a Claude transcript already exists for that id. A *fresh* TUI * session pre-registers its channel→sessionId mapping BEFORE the first Claude turn (so transcript * replay / session naming work), which makes the orchestrator's generic "a session mapping exists * ⇒ resume" heuristic ask to resume an id that has no transcript yet — Claude then exits with * "No conversation found with session ID: ". Gating the resume request on the transcript * actually existing keeps the first turn on `--session-id` (create) and lets only later turns / * reconnects use `--resume`. Self-healing: a deleted transcript also correctly falls back to create. */ export declare function resolveTuiResume(requestedResume: boolean, jsonlPath: string, exists?: (p: string) => boolean): boolean; /** * Convenience factory: real JsonlTail wired to the path. Used by production code; tests inject * a custom factory instead. */ export declare function defaultTailFactory(jsonlPath: string): JsonlTailLike;