Skip to content

Transcript

The thread transcript is a local recall surface: a searchable record of conversational text written by the human, by the agent, and by kiki itself.

It is a memory aid for the human. After explicit provider-egress consent — permission to send text to the model provider — it also serves the same thread's resumed agent. Code truth stays in jj (the jujutsu version-control system) and the filesystem.

Transcript capture, transcript reads, and transcript-backed reopen catch-up are v1.x work, not part of the acceptance slice (the minimal first release). The acceptance-slice cascade protocol does not depend on a transcript row being written. This chapter fixes the privacy and data-model boundary for when the feature ships.

Storage

Transcript rows live in ~/.config/kiki/repos/<repo_id>/state.db. The source repo's filesystem holds no transcript state — kiki centralizes all per-repo runtime under ~/.config/kiki/.

Transcript rows are stored locally and are never published. They must not feed:

  • kk publish PR drafting
  • auto-describe
  • auto-rename
  • any externally visible artifact

This prohibition holds even when the transcript would make the generated artifact better. The transcript contains dead ends, local reasoning, tool failures, and quoted material the user did not choose to publish.

Local storage is a storage promise, not an egress promise. Sending anything to a model provider is a separate, consented step: if the user explicitly requests reopen catch-up, kiki sends the selected catch-up text to the configured harness/model provider as agent input. Reopen catch-up defines the required consent boundary.

Row model

Each captured row records:

  • thread_id
  • optional change_id, commit_id, and op_id — jj anchors that tie the row to a point in repo history
  • session_id
  • monotonic per-thread seq — a counter that only increases
  • captured_at
  • author: human, agent, or kk
  • direction: inbound_to_agent, outbound_from_agent, or local_record
  • text
  • dedup_key
  • synthesized
  • anchor_unknown

Kiki-authored cascade, reopen catch-up, and hard-escalation messages carry author=kk, direction=inbound_to_agent, and synthesized=true.

Full-text search and deduplication

thread_messages has an FTS5 virtual table (SQLite's full-text index) over text; kk thread transcript --search <query> uses that index.

dedup_key is the idempotency key: inserting the same row twice has no effect. Harness-projected rows use a harness-derived row id, such as Claude Code's JSONL uuid. Cascade-injection rows use cascade:<intent_id> with no session component, because the same intent payload may be re-delivered across sessions.

Capture

The first capture adapters project rows from harness log files: Claude Code session JSONL and Codex rollout JSONL. A managed Codex thread's rollouts live under its launch-scoped CODEX_HOME, so discovery is deterministic — kiki always knows where to look.

Captured:

  • user text rows
  • assistant text rows

Not captured initially:

  • token-streaming deltas
  • structured tool calls
  • structured tool outputs
  • extended-thinking blocks

Backfill — importing rows written while kiki was not watching — must anchor rows using per-workspace jj op history. If no reliable anchor exists, kiki inserts the row with anchor_unknown=true.

Backfill must not stamp old messages with whatever @ (the current working-copy revision) happens to be when kiki restarts. That would make later change-aligned queries misleading.

Recovery and idempotency

Transcript capture may lag while kkd is down, but it must not silently miss or duplicate messages.

For each (thread_id, session_id), kiki stores a transcript_offsets row containing (byte_offset, last_row_uuid). On daemon restart, kiki resumes from that offset. If the JSONL file has rotated or is shorter than the stored offset, kiki reads from the top and relies on INSERT OR IGNORE over dedup_key to skip rows it already has.

kk reopen may reuse a harness session id or create a new one, depending on harness behavior. A reused session resumes from its stored offset. A fresh session starts a new offset row. The thread transcript is the union of all session ids, ordered by thread-local seq.

Cascade-injection rows are not projected from harness JSONL. When transcript capture is enabled, the transcript projector records the durable MarkDelivered event using dedup_key=cascade:<intent_id> plus the payload and anchor embedded in that sync_intent. A retry must re-emit the saved payload byte-identically. If @ advances between materialization and delivery, the row still binds to the saved anchor. Failure to project this optional row cannot fail or roll back cascade acknowledgement.

Reopen catch-up

Plain kk reopen does not send transcript text to the agent. kk reopen --catch-up explicitly requests a catch-up composed from recent rows where synthesized=false. That filter prevents recursive catch-up quoting.

A closed thread has no live thread credential. BeginApproval therefore discloses that catch-up was requested but returns no transcript text. After the exact reopen approval is claimed into its durable journal, and before the harness starts, the daemon releases the preview — only to the same enrolled foreground presenter, and only as an authorized, reversible step. If the human declines provider egress, kiki drops the catch-up and continues as a plain reopen, unless the human cancels the whole operation. No transcript text reaches the harness first.

Before the first provider-bound catch-up, kiki shows that selected local transcript text will be sent to the configured model provider, and asks for foreground consent. The user may approve once, remember approval for that provider and repo, or cancel. A remembered preference is revocable, and a change in provider identity invalidates it. Remembering privacy consent suppresses that prompt only; it does not bypass the separate authority required to reopen a thread. Under the v1 authority contract, non-interactive reopen remains unavailable.

Kiki should offer a preview before consent. Kiki does not claim to scrub secrets or reliably redact the catch-up. Consent covers model-provider egress for reopen context only. It does not permit transcript use in publication, metadata generation, telemetry, or unrelated agent prompts.

The catch-up itself is captured as synthesized=true.

Before invoking the harness with a kiki-prepended catch-up or hard-escalation message, kkd inserts a pending_kkd_prepends sidecar row keyed by (thread_id, sha256(text)) with a short TTL (time to live). The JSONL projector, when installed, uses that sidecar to mark the eventual harness-emitted user row as author=kk, direction=inbound_to_agent, and synthesized=true.

The sidecar match intentionally omits session_id, because a fresh-session reopen may not have a session id until after the harness starts. Duplicate (thread_id, text_sha256) rows are allowed and consumed first-in, first-out, so two byte-identical prepends in the TTL window produce two synthesized rows.

Catch-up source queries must exclude synthesized=true rows. This prevents reopen catch-ups from quoting older catch-ups, cascade messages, or hard-escalation framing back into themselves.

Read API

The v1.x human CLI reads transcripts through kk thread transcript. Query modes cover by-change reads, full-text search, change ranges, tail reads, and filtering by anchored, tombstoned, or synthesized rows; the flag surface lives in Commands.

Agent transcript reads over MCP (the tool-call interface agents use) are v1.x polish. When they ship, they are same-thread only.

An MCP transcript result travels over local IPC on its first hop, but it normally becomes model-provider input when the harness returns tool output to a hosted model. Same-thread authority is therefore necessary but not sufficient. Before enabling transcript MCP for a managed harness, kiki requires remembered provider-egress consent for purpose transcript_mcp and for that harness's current provider identity. MCP itself cannot open the foreground prompt or grant consent: without a matching record it returns ConsentRequired and no transcript content. The human grants or revokes that purpose through the foreground privacy-consent CLI/TUI surface.

Provider-egress consent is a privacy policy record. It is not a HumanApproval and not configuration. Each durable record binds:

  • repo_id;
  • thread_id;
  • normalized provider identity: harness, provider/service, endpoint origin, and a non-secret account or tenant fingerprint when available;
  • purpose: catch_up or transcript_mcp;
  • disclosure-policy version, grant time, and optional revocation time.

The foreground presenter shows the provider identity, purpose, thread scope, and the absence of secret scrubbing before granting. A remembered grant applies only to that exact tuple. A change in thread, provider, endpoint, account/tenant, purpose, or disclosure version fails closed and asks again. A one-time catch-up approval attaches only to that invocation; kiki does not insert it as a remembered grant.

Consent records live in the registered repo's SQLite database. They are inspectable and revocable. Revocation takes effect before the next provider-bound read; it does not recall content already sent. Committed config, environment variables, CLI flags, MCP calls, and an operational HumanApproval cannot create a remembered grant.

Privacy and retention

Kiki performs no secret scrubbing. Three promises stay separate: rows are stored locally, rows are never used for publication, and provider egress requires explicit consent. Redaction that is "usually right" is not enough for text that may contain prompts, quoted source, command output, or tool failures.

The initial transcript release has no retention cap. SQLite is expected to handle the volume because token deltas and tool payloads are not captured.

kk close preserves transcript rows. kk thread destroy deletes them by default; --keep-log is the explicit opt-out that keeps them.

docs/reference · built with VitePress