/** Subset of Claude Code's JSONL event shape we care about. */ export interface TranscriptEvent { type?: string; subtype?: string; uuid?: string; sessionId?: string; timestamp?: string; message?: { role?: string; content?: unknown; /** Claude's API stop reason. `tool_use` is an intra-turn pause; terminal * reasons such as `end_turn` / `stop_sequence` close the logical turn. */ stop_reason?: string | null; }; /** API-error records. When the model call fails, Claude Code writes a * `type:"assistant"` line with `isApiErrorMessage:true` and a machine * `error` code (e.g. "rate_limit", "server_error", "authentication_failed", * "unknown") plus the HTTP `apiErrorStatus`. These carry a human-readable * text block ("You've hit your session limit · resets 10:40pm"), so they * must be excluded from assistant-reply forwarding (they are not a model * answer) and, for rate_limit, routed to usage-limit detection instead. */ error?: string; errorDetails?: unknown; isApiErrorMessage?: boolean; apiErrorStatus?: number; /** Present on `type:"attachment"` lines. The bridge attribution queue * treats `attachment.type === "queued_command"` as a turn-start signal — * Claude writes one of these the moment it dequeues a type-ahead * submission, immediately before the assistant's reply for that turn * starts streaming. `prompt` carries the same content the user typed; * shape is usually `string` but we tolerate the message-style array form * via stringifyUserContent. */ attachment?: { type?: string; prompt?: unknown; commandMode?: string; }; } /** * True when an event is Claude Code's structured rate-limit record: an * `isApiErrorMessage` line whose machine `error` code is "rate_limit" (429). * This is the authoritative "we are rate limited" signal — far more reliable * than scraping the TUI, and it lands exactly at the turn's terminal boundary. * The caller turns this into a `limited` session state via * structuredRateLimitState(); it must NOT be forwarded as an assistant reply. */ export declare function isTranscriptRateLimitEvent(ev: TranscriptEvent): boolean; /** Concatenated text blocks of an API-error record, used to recover a human * retry clock ("... resets 10:40pm") when present. Returns '' if none. */ export declare function apiErrorMessageText(ev: TranscriptEvent): string; /** Provider-neutral terminal semantics derived from one Claude transcript * event. The classifier is deliberately fail-closed: an `unknown` API error is * retryable only when its bounded text matches a verified transient signature. */ export type ClaudeTerminalOutcome = { status: 'completed'; } | { status: 'failed'; errorCode: string; retryable: boolean; } | { status: 'ambiguous'; errorCode: string; retryable: false; } | { status: 'rate_limited'; errorCode: 'provider_rate_limited'; retryable: false; }; export declare function classifyClaudeTerminalEvent(ev: TranscriptEvent): ClaudeTerminalOutcome | undefined; /** * Authoritative Claude Code end-of-turn markers observed in its JSONL: * * - the final non-sidechain assistant message carries a non-tool stop reason; * - current Claude versions additionally append `system/turn_duration`. * * Both may be present for the same turn, so consumers must deduplicate by the * durable turn identity. `tool_use` and `pause_turn` are explicitly excluded: * Claude is waiting on a tool/continuation and has not returned to a new turn. */ export declare function isClaudeTurnTerminalEvent(ev: TranscriptEvent): boolean; /** Extract the user-typed prompt text for a "turn start" event — works for * both legacy `role:user` events (text in `message.content`) and the * type-ahead `attachment(queued_command)` form (text in `attachment.prompt`). * Returns '' when neither shape carries usable content. Used at three * layers: BridgeTurnQueue.ingest (fingerprint-match the right pending Lark * turn), worker emit (local-turn user-text resolution), and tests. */ export declare function extractTurnStartText(ev: TranscriptEvent | null | undefined): string; export interface DrainResult { events: TranscriptEvent[]; /** Byte offset to pass back on the next drain. */ newOffset: number; /** Trailing partial line (no newline yet) — kept so the next drain can * prepend it. Internal helper for chained drains; callers usually only * need to remember `newOffset`. */ pendingTail: string; } /** * Read everything from `path` starting at `fromOffset` and return parsed * JSONL events plus the new file offset. * * - Returns `{ events: [], newOffset: 0, pendingTail: '' }` if the file * doesn't exist (caller treats this as "nothing yet"). * - Detects truncation (size < fromOffset): resets to 0 and re-drains so a * rotated/cleared transcript doesn't silently swallow new lines. * - Skips malformed JSON lines (logs nothing — robustness over noise). * - The trailing partial line (no `\n` yet) is *not* parsed and *not* * counted toward `newOffset`, so the next drain re-reads it. */ export declare function drainTranscript(path: string, fromOffset: number): DrainResult; /** * Filter to assistant text events. Returns only events where: * - type === 'assistant' OR message.role === 'assistant' * - content has at least one text block * - uuid is present * * Sub-agent / sidechain events (isSidechain === true) are excluded so that * spawn-internal Task agent chatter doesn't leak to Lark. */ export declare function pickAssistantTextEvents(events: TranscriptEvent[]): TranscriptEvent[]; /** * Extract the visible text from one assistant event. Walks all `type:'text'` * blocks in `message.content` (or the bare string) and joins them with * blank lines. Returns '' if no text blocks. */ export declare function extractAssistantText(event: TranscriptEvent): string; /** Convenience: filter+extract a list of events into a single concatenated string. */ export declare function joinAssistantText(events: TranscriptEvent[]): string; /** * The turn's FINAL answer: the contiguous run of this turn's assistant-text * events after its last tool_use. A long agentic turn writes many interim * narration blocks between tool calls; joining them all (joinAssistantText) * makes the fallback both post a narration collage AND look "materially * longer" than the model's own explicit `botmux send`, defeating the * bridge-fallback gate. Walking back from the turn's last text event until a * tool_use / tool_result boundary yields just the closing answer. * * Crossed (not boundaries): thinking-only assistant lines and non-message * meta lines (`last-prompt`, `ai-title`, system, attachments) — Claude Code * interleaves these freely inside a closing answer. Events from other turns * never contribute: only uuids in `turnAssistantUuids` are collected. * Returns '' for a turn with no text after its last tool_use (nothing worth * falling back with — e.g. the turn ended in a `botmux send` call). */ export declare function trailingAssistantText(events: TranscriptEvent[], turnAssistantUuids: readonly string[]): string; /** True when a `type:'user'` (or `message.role:'user'`) event represents a * *real* prompt the human typed — not Claude Code's internal machinery * (tool_result, slash-command wrappers, isMeta/isCompactSummary markers, * sidechain spawn events). The bridge attribution queue and the adopt * preamble extractor share this predicate to ensure they're seeing the * same notion of "user input". */ export declare function isMeaningfulUserEvent(ev: TranscriptEvent | null | undefined): boolean; /** True when a `type:'attachment'` line carries a queued-command payload * representing a real submitted prompt. Claude writes one of these when it * dequeues a type-ahead submission (right before the assistant's reply for * that turn starts streaming) — the bridge attribution queue treats it * exactly like a `role:user` event for turn-start purposes. Filters mirror * isMeaningfulUserEvent's defenses (sidechain, empty / synthetic-prefix * prompts) so a queued slash command can't false-start a Lark turn. */ export declare function isMeaningfulQueuedCommand(ev: TranscriptEvent | null | undefined): boolean; export interface AdoptPreamble { /** The most recent meaningful user prompt's text (post-stringify, no * whitespace collapse — preserves the prompt's actual formatting). */ userText: string; /** All assistant visible-text emitted between that user prompt and the * end of the events list, joined with blank lines. tool_use blocks are * excluded; sidechain assistant events are excluded. */ assistantText: string; } /** Walk the events forward and return the last *completed* user/assistant * exchange. "Completed" here means: a meaningful user prompt followed by * at least one assistant event with visible text. tool_use / tool_result * events do NOT reset the turn — they're intra-turn machinery, so a * prompt → tool_use → tool_result → assistant text sequence still counts * as a single turn. Returns null when there's no meaningful user yet, or * the last user wasn't followed by any visible assistant text (Claude is * mid-tool-use when /adopt fired). * * Used by adopt-bridge to surface "the previous round" to the Lark thread * so the user has context for continuing the conversation. */ export declare function extractLastAssistantTurn(events: TranscriptEvent[]): AdoptPreamble | null; /** * True when a user-role event carries ONLY tool_result blocks — Claude * Code's representation of "tool returned this output" between an * assistant tool_use and the assistant's continuation. Both the bridge * attribution queue and the on-disk fingerprint search must skip these: * * - the queue would treat tool output as fresh local input and disable * collection mid-turn, * - the fingerprint search would false-positive on log content that * happens to contain the Lark fingerprint substring (e.g. a short * "hello" message hijacked by an unrelated jsonl whose tool_result * dumped a log line containing "hello"). Re-exported by * bridge-turn-queue.ts so both consumers share the same predicate * and never drift apart. */ export declare function isPureToolResultUserEvent(content: unknown): boolean; /** * Stringify a transcript user event's content to a flat string. Handles * both legacy bare-string content and the array-of-blocks form. * * Lives here (not in bridge-turn-queue.ts) so the in-process attribution * state machine and the on-disk fingerprint search use *exactly* the * same text — otherwise multi-line / array-content Lark messages stop * matching one path or the other and bridges silently break. */ export declare function stringifyUserContent(content: unknown): string; /** * Collapse whitespace + trim. Same normalisation applied on both sides * of the fingerprint compare (the Lark message that produces the * fingerprint, and the transcript user content we search through), * so newlines / tabs / double-spaces don't break the match. */ export declare function normaliseForFingerprint(s: string): string; /** * Find the most recently-modified `.jsonl` file in a Claude Code project * directory. * * `acceptCandidate` lets callers narrow the candidate set — the bridge's * quiet-mtime fallback passes a trust-set predicate so a sibling Claude * pane writing in the same project dir cannot hijack the watcher. * Without it any actively-written sibling jsonl wins the mtime race and * the bridge enters a flap loop with the pid resolver pulling it back. * * Returns null when the directory doesn't exist, has no jsonl files, or * every candidate was rejected by `acceptCandidate`. */ export declare function findLatestJsonl(dir: string, opts?: { acceptCandidate?: (path: string) => boolean; }): string | null; /** * Search every `.jsonl` file in `dir` for one whose contents include the * given fingerprint. Used by the bridge watcher to detect a session * switch (`/clear` / `/resume`) caused by the user's pane: when a Lark * message is pending and its content fingerprint shows up in a NEW jsonl * file, that file is the user's current session and we should switch. * * Pinning the switch decision to fingerprint match (rather than mtime) * avoids hijacking by sibling Claude Code panes in the same project * directory — they'll write busy jsonls but won't ever contain our Lark * fingerprint. * * Optional `excludePath` skips the file we're already watching so the * caller's "did it change?" comparison is cheap. * * Reads only the trailing 1 MB of each candidate (fingerprints land near * the end of the jsonl when Claude has just written them) — long-lived * sessions can grow to tens of MB so a full read would be wasteful. * Callers should still gate on "an unstarted pending turn exists" rather * than calling this on every poll tick. */ export interface JsonlFingerprintSearchOptions { /** Skip the file the caller is already watching/checking. */ excludePath?: string; /** Ignore older files when the caller is looking for a just-written submit. */ minMtimeMs?: number; /** Drop events whose `timestamp` field is older than this (millis since * epoch). Defends against short fingerprints ("hello", "test") matching * old user lines in unrelated sibling jsonls — file mtime alone isn't * enough since a sibling Claude pane could be actively writing. */ minEventTimestampMs?: number; /** Also match Claude Code type-ahead enqueue events, whose content is not role:user. */ includeQueueOperations?: boolean; /** Called on each candidate that already passed the fingerprint match. * Returning `false` skips the candidate and continues searching older * files in the directory (mtime-descending walk). Used by the bridge * watcher to reject sibling-pane jsonls whose sessionId we don't trust, * without losing the chance to find a legitimate /clear rotation buried * under a busier sibling. Default (no callback): accept the first * fingerprint match like the original behaviour. */ acceptCandidate?: (path: string) => boolean; } /** Scan a single jsonl file's tail for a Lark message fingerprint. Same * parsing rules as `findJsonlContainingFingerprint` (decode role:user content, * optionally also queue-operation/enqueue, normalise whitespace, then * substring-match the fingerprint). Used by the claude-code adapter when * the pid resolver has just switched to a rotated jsonl that may already * contain the just-submitted user event. */ export declare function jsonlContainsFingerprint(path: string, fingerprint: string, opts?: { includeQueueOperations?: boolean; minEventTimestampMs?: number; }): boolean; export declare function findJsonlContainingFingerprint(dir: string, fingerprint: string, excludePathOrOptions?: string | JsonlFingerprintSearchOptions): string | null; /** * Stronger sibling-pane recovery anchor than the substring fingerprint * search. Walks every `.jsonl` in `dir` and returns the paths whose * trailing 1MB contains a user/queue event whose normalised text is * EXACTLY equal to `normalisedContent` (not a substring), respecting * `excludePath`, `minMtimeMs`, `minEventTimestampMs`, * `includeQueueOperations`, and `acceptCandidate` the same way as * `findJsonlContainingFingerprint`. * * Returns *all* matches in mtime-descending order — callers must * abstain when the result has length > 1, since multiple files containing * the same exact normalised content cannot be disambiguated without * stronger evidence (and forcing a switch would risk picking the wrong * pane). The caller's typical pattern is: * * - 1 match → switch to it (legitimate post-/clear recovery) * - 0 matches → no recovery this tick; wait for stronger signal * - >1 match → log and abstain; surface a diagnostic to the user * * Used by the bridge fingerprint fallback's recovery path for in-pane * `/clear`: substring matches risk hijacking on short fingerprints (the * literal text "test" matches "run tests" / "test bridge"), but full * equality on a Lark message we just wrote is a much stronger anchor. */ export declare function findJsonlsContainingExactContent(dir: string, normalisedContent: string, options?: JsonlFingerprintSearchOptions): string[]; /** * Partition transcript events into history (timestamp ≤ cutoff) and live * (timestamp > cutoff, or no parseable timestamp). Used by the bridge * watcher when it switches to a new jsonl that may contain pre-existing * conversation: anything older than the cutoff (e.g. iTerm-typed turns * the user produced before the Lark mark fired) belongs in the seen-set * via `BridgeTurnQueue.absorb` so the worker doesn't replay them as * "🖥️ 终端本地对话" cards. Anything newer is fed through `ingest()` so * the freshly-written Lark user event can match its pending fingerprint. * * Events with malformed / missing timestamps fall into `live`: better * to forward an unattributable event once than to silently drop a real * reply because Claude omitted a timestamp. */ export declare function splitTranscriptEventsByCutoff(events: TranscriptEvent[], cutoffMs: number): { history: TranscriptEvent[]; live: TranscriptEvent[]; }; /** * Read the first event timestamp out of a jsonl. Reads only the leading * 4 KB — Claude's `file-history-snapshot` and `SessionStart` events both * land in the first few hundred bytes. Returns the parsed millis, or * undefined when no parseable timestamp is found in the leading chunk * (corrupted file, partial first line, format change). * * NOTE: not currently wired into the bridge rotation flow. The bridge * fingerprint fallback (`decideFingerprintSwitch` in * `bridge-rotation-policy.ts`) deliberately rejects candidates outside * the pid-derived trust set rather than relying on freshness heuristics * — file-creation timestamps cannot prove ownership across panes in * the same project dir. Kept here as a reusable primitive for * diagnostics and future /clear-recovery work. */ export declare function readFirstEventTimestamp(path: string): number | undefined; //# sourceMappingURL=claude-transcript.d.ts.map