/** * The chat transcript AT REST (DES-L5 §5-d; D-13 / BC-33; F-RC1-112 = F-RECON-018, crew#503). * * The engine keeps a chat's warm seats and their conversation memory, but no transcript: every * `chatDelta` / `chatReply` is seen exactly once, on `/ws`, by whoever is connected at that moment. * P6 reproduced the consequence — a reload (or a second tab) of a LIVE chat rendered a blank thread, * and the reply nobody was watching for was simply gone. The daemon sees every frame once too, at * the ONE hook that already stamps `turn_id` (`server.ts` → `chatTurns.decorate`), so the record is * written there: one append-only JSONL file per chat under the state home, served back on * `GET /chats/:id` as `messages` (api-types 0.38.0 `ChatTranscriptRecord`, append order). * * Retention = the chat's LIFETIME (D-13, as designed — no age sweep, no second mechanism): * - the file is DROPPED on the engine's `chatClosed` (requested, idle and pool_cap alike — the one * arm `server.ts` already folds for the scope index); * - the directory is CLEARED at daemon boot (orphaned files), EXCEPT transcripts held for a * promoted run that is still non-terminal — those are preserved so Continue-in-Build prefill * remains reproducible across a daemon restart (crew#619); * - only frames a live turn STAMPED (`turn_id` present) are persisted, so a straggler `chatReply` * after the close cannot recreate a file, and `turnId` is never null on a record. * * Privacy posture: the directory is 0700 and each file 0600 (the transcript is the operator's * conversation); the file name is the chat id, admitted by the SAME guard the scratch root applies * (`CHAT_ID` — one sanitizer, no second regex), so an arbitrary `GET /chats/:id` can never name a * path. `messages` is unbounded — a reply may be up to the engine's 8 MB cap (wave 1, noted). * * Registered in `tests/fixtures/state-home-subtrees.json` as `chats` (crew half; the core half — * the worker Read fence — landed with core-ts 0.7.26), and named in `state-home-registry.ts`. */ import type { ChatAttachment, ChatCitationItem, ChatTranscriptRecord, CoreEvent, DecisionView } from '../core/types.js'; /** The state-home entry (`/chats/`) — the name the fixture registers. */ export declare const CHAT_TRANSCRIPTS_DIRNAME = "chats"; export { rewriteHostPaths, withRootAliases, type ChatRepoRoot } from '../core/host-paths.js'; import { type ChatRepoRoot } from '../core/host-paths.js'; /** Where the transcripts live: under the daemon's state home, like every other crew store. */ export declare function defaultChatTranscriptsDir(): string; export declare class ChatTranscriptStore { private readonly dir; private readonly now; /** Per-chat roots registered at open time, used to rewrite absolute host paths (crew#618). */ private readonly chatRoots; /** DC-S4b: the `chatDelta` fence filter (per chat + seat) — a `wicked-decisions` block never streams. */ private readonly fence; /** DC-S4b: the block body cut from a seat's latest `chatReply`, until the recorder takes it. */ private readonly blocks; constructor(opts?: { dir?: string; now?: () => number; }); /** * Register the repo roots for a chat so that absolute host paths in seat replies are rewritten * to repo-relative form before being stored (crew#618). Must be called at chat open time, after * `ChatScopeIndex.set()` confirms the chat is live. Idempotent: a second call for the same * `chatId` replaces the previous roots. */ registerRoots(chatId: string, repos: ReadonlyArray): void; /** The directory this store writes under (diagnostics / tests). */ get directory(): string; /** The live transcript's path (the history copies it when the chat ends — `chat-history.ts`). */ pathOf(chatId: string): string | null; /** `/.jsonl` — or `null` for an id the chat-id guard refuses (never a path). */ private fileOf; private append; /** The operator's message a send delivered, with the seats the engine reached (the 202's `seats`). */ appendUser(chatId: string, turnId: string, text: string, seats: readonly string[], attachments?: readonly ChatAttachment[]): void; /** DC-S7: a crew-authored preface the seats received with that turn's message — disclosed, verbatim. */ appendSystem(chatId: string, turnId: string, text: string): void; /** * Fold one STAMPED frame (call with `chatTurns.decorate(event)`'s result): a seat's `chatReply` * carrying `turn_id` becomes a `seat` record — ok or not (an eviction's `ok: false` text names the * budget). Every other frame, and a reply no live turn stamped, is ignored. * * Absolute host paths in the reply text are rewritten to repo-relative form (crew#618) using the * roots registered for the chat via {@link registerRoots}. */ observe(stamped: CoreEvent): void; /** * The citation verdicts of a reply already recorded (crew#561): appended as their OWN record, * because the transcript is append-only and verification finishes AFTER the reply is stored. A * reader folds it onto the `seat` record with the same `turnId` + `cliKey` — which is what keeps * a fabricated SHA marked across a reload, instead of rendering as plain text again. * * Only a STAMPED frame is recorded: without a `turn_id` there is no reply to fold it onto (the * same rule {@link observe} applies), and a straggler must never recreate a dropped file. */ recordCitations(frame: { chat: string; cliKey: string; turn_id?: string; verified: number; unverifiable: number; corrected: number; unchecked: number; items: ChatCitationItem[]; }): void; /** Every record of the chat in append order; `[]` for a chat with no file (or a refused id). */ read(chatId: string): ChatTranscriptRecord[]; /** * Rewrite absolute host paths in a `chatReply` frame's `text` field, returning a NEW event * object when the text changed or the SAME object when not applicable (crew#618). * * Call BEFORE `observe` and `broadcast` so the live `/ws` frame already carries the * repo-relative form that studio renders and promotes from (Acceptance 1). */ rewriteEvent(event: CoreEvent): CoreEvent; /** * DC-S4b: the `wicked-decisions` block body {@link rewriteEvent} cut from this seat's latest reply, * handed over ONCE (a second call answers `null`). Call right after `rewriteEvent` for the reply. */ takeDecisionsBlock(chatId: string, cliKey: string): string | null; /** * DC-S4b: the decisions recorded from one turn's operator message, appended as their OWN record * (the `citations` pattern: the transcript is append-only and derivation finishes after the * replies are stored). A reader folds it onto the turn's `user` record. Only a chat that has a * transcript gets one: a record alone must never recreate a dropped file. */ recordDecisions(frame: { chat: string; turnId: string; items: DecisionView[]; }): void; /** The chat is gone (`chatClosed`, any reason): its transcript goes with it. */ drop(chatId: string): void; /** * Daemon boot: remove every transcript NOT in `retainedChatIds` (crew#619 — promoted-run * transcripts must survive a restart so Continue-in-Build prefill is reproducible). Pass an * empty set to clear everything. */ clearOrphaned(retainedChatIds: ReadonlySet): void; } //# sourceMappingURL=chat-transcripts.d.ts.map