/** * Rebuilds a past session's transcript from OUR OWN recorded events, not from * any vendor's transcript file — which is what makes it work identically for * claude-code and codex (both normalize into the same AnalyticsEvent stream; * see core/collector/normalizer.ts and core/collector/codex-tailer.ts) and * makes it survive the vendor deleting, rotating, or never writing its own * history. * * The fold: `prompt.submitted` opens a turn, `tool.call`s attach to the open * turn, `turn.completed` closes it with assistantText / model / usage. Events * that arrive with no open turn still get a turn of their own (with a null * prompt) rather than being dropped — a record whose recording started * mid-turn is a real case, and silently discarding those events would show a * session as emptier than it was. * * ORDERING — the one non-obvious rule. `AnalyticsEvent.seq` is NOT a * session-lifetime ordering key: it comes from an in-memory per-boot counter * (core/collector/seq.ts), so it restarts at 1 on every harness boot and every * fresh pty. A real resumed session reads seq 1, 2 then 1, 1, 1. Sorting by * `seq` alone would interleave a resume's events into the middle of the first * run. Sort by `(ts, seq)`: wall-clock time orders the epochs, and `seq` only * breaks ties inside one of them. A restart is a new epoch, never loss. * * HONESTY — what this can never recover, surfaced as `limitations` on the * record so the UI can say it out loud: * - `tool.call` stores a size-capped `toolResponseSummary`; large tool * outputs are gone from our log. * - `turn.completed` carries only the Stop hook's LAST assistant message * (see the header of core/collector/transcript.ts for why), so assistant * narration *between* tool calls is absent. The tool-call stream fills the * chronology, not the prose. * - Codex's rollout has no equivalent of that field at all, so its turns * carry tool calls and prompts but no assistant text. * * RETENTION — why a record can outlive the events it was folded from. The * ndjson is capped at 50 MB / 30 days (collector/store-retention.ts), so a * record built only from it would silently disappear a month later. At session * end a compacted copy is written to `~/.sapiom/harness/records/` and the * reader falls back to it once the log no longer covers the conversation * (core/record-archive.ts; the rule is spelled out on `read` below). */ import type { AnalyticsEvent, HarnessKind, SessionRecord } from "../shared/types.js"; import type { EventReader } from "./collector/store.js"; /** * Order events for folding: `ts` first, `seq` only as a tiebreak within the * same millisecond. See the module header for why `seq` alone is wrong. * Array.prototype.sort is stable, so events matching on both keep file order. */ export declare function sortEventsForFold(events: readonly AnalyticsEvent[]): AnalyticsEvent[]; export interface FoldOptions { /** The id the record is addressed by (the earliest session when several * harness sessions were merged). Defaults to the first event's session. */ harnessSessionId?: string; /** Every harnessSessionId folded in, in first-seen order. Defaults to the * distinct session ids present in `events`. */ mergedSessionIds?: readonly string[]; /** Fallback when no event carried a harness kind (an empty record). */ harness?: HarnessKind; } /** * Fold an event stream into an ordered {@link SessionRecord}. Pure and * synchronous: hand it the events for one session (or for a set of sessions * that share an agent session — a resumed conversation) and it returns the * transcript. Events for other sessions are tolerated and folded in; the * caller is responsible for filtering, which `EventStore.read` does by index. */ export declare function foldSessionRecord(events: readonly AnalyticsEvent[], options?: FoldOptions): SessionRecord; /** * Optional last-turn enrichment from a vendor transcript (claude-code only). * Strictly an enhancement: it may return null for any reason at all (no file, * wrong harness, unreadable) and the record still renders. Never a dependency. */ export type FinalTurnEnricher = (record: SessionRecord) => Promise<{ model: string | null; assistantText: string | null; usage: { inputTokens: number | null; outputTokens: number | null; } | null; } | null>; /** * The durable archive, as the reader needs it. Structural on purpose: the real * implementation is `createRecordArchive` (core/record-archive.ts), and a test * can hand in two functions. */ export interface ArchivedRecordSource { read(id: string): Promise; list(): Promise; } export interface SessionRecordReader { /** * The record for `id`, which may be a harnessSessionId or the agent's own * session id (history rows the registry never tracked only have the latter). * Reads from whichever source still holds the whole conversation — the live * event log, or its archived copy once retention has eaten into the log (see * {@link createSessionRecordReader} for the rule). Null when neither holds * anything for it — the honest answer, and what the route turns into a 404. */ read(id: string): Promise; /** * The record folded from the event log ALONE, never from the archive. This is * what gets archived (archiving an archive would compact an excerpt of an * excerpt and re-stamp it as fresh), and what a caller wanting the * uncompacted record while the events are still there should use. */ readFromEvents(id: string): Promise; /** * Human-turn counts, keyed by BOTH harnessSessionId and agentSessionId so a * history row can be looked up by whichever it has. Exact and cheap at any * file size (no transcript scan, no per-row I/O): the event index for * conversations the log still holds, the archive's own counts for the rest — * so a row's count survives the sweep along with the record it links to. */ turnCounts(): Promise>; /** * Every conversation the event log holds, by primary id, most recently active * first. The archive's backfill pass walks this to find conversations that * were never archived (see backfillSessionRecords). */ conversationIds(): Promise; } /** * Reader over the local event store, and — when one is wired in — over the * durable record archive that outlives it (core/record-archive.ts). `read` * touches only the byte spans the index attributes to the session, so opening a * record never rescans the (up to 50 MB) ndjson; see `read`'s own comment for * how it chooses between the two sources. * * MEMORY CEILING — stated, because a global `(ts, seq)` order can't be produced * from a stream: `read` holds the whole session's events at once. That is ONE * copy, not two — `sortEventsForFold` copies the array of references, and the * turns reference the same payload strings the events do. Measured against a * 50 MB log owned by a single session at real event density (~4.3 KB/event): * 12k events, ~50 MB of record content against ~52 MB on disk (see * session-record.perf.test.ts, which asserts that ratio so a regression that * duplicates the data or buffers the raw file gets caught). * * So the ceiling is "one session's share of a file capped at 50 MB", which for * the pathological single-session case is ~50 MB of transient heap in a local * server. That is deliberate and bounded rather than unbounded; if a future * caller needs a tighter bound (H3's rehydration reading only recent context, * say), the fix is a `limit`/most-recent-N on the wire, not a change of order. */ export declare function createSessionRecordReader(store: EventReader, options?: { enrichFinalTurn?: FinalTurnEnricher; archive?: ArchivedRecordSource; }): SessionRecordReader; /** * Fills the LAST turn's model / usage / assistant text from Claude Code's own * transcript, when that file happens to still be on disk — a pure enhancement * over what our events recorded, wired in via * {@link createSessionRecordReader}'s `enrichFinalTurn`. * * Everything about it is best-effort by design: wrong harness, no cwd, no * agent session id, transcript deleted or rotated → null, and the record * renders from our events alone. Reuses `readLastAssistantTurn`, so it reads * only the tail of a transcript that can be hundreds of MB. */ export declare function createClaudeTranscriptEnricher(options?: { homeDir?: string; }): FinalTurnEnricher; //# sourceMappingURL=session-record.d.ts.map