# Déjà Vu — Cross-Agent Conversation Capture (https://jackin.tailrocks.com/reference/roadmap/deja-vu/)



**Status**: Open — research and design proposal (Phase 2, [Agent Orchestrator Research Program](/reference/roadmap/agent-orchestrator-research/))

## Problem [#problem]

jackin' runs five agent runtimes — `Claude Code`, `Codex`, `Amp`, `Kimi`, and `OpenCode` — inside isolated, per-instance containers. Each one keeps its own conversation history in its own private layout, and each one can resume *its own* threads with *its own* resume command. There is no single place an operator can stand and see **every conversation they have had, across every agent, across every workspace and instance**. The history is real, but it is scattered across five incompatible stores and trapped behind five different resume CLIs.

This hurts in two concrete ways the operator hits today:

* **Eject erases it.** `jackin eject` / `jackin prune` tear down `~/.jackin/data/<container>/`, and the agent's conversation history goes with it. That cleanup is the intended default — but it means a conversation worth keeping is gone the moment the instance is reclaimed, with no way to go back and re-read what was discussed, what the agent proposed, and what was decided.
* **No cross-agent recall.** Even while instances live, there is no "show me that conversation again" surface that works regardless of which agent produced it. `Amp` has the best-in-class version of this (server-side threads, `amp threads continue`, shareable `ampcode.com/threads/T-<id>` URLs, and `@T-<id>` cross-thread references). jackin's job is to bring that *experience* to every agent — including agents and models that some operators can only run in certain regions — without picking a winner among them.

**Déjà Vu** is the capture-store-browse layer that closes this gap. It captures each agent's conversation — every prompt the operator sent and every answer the operator saw — normalizes it into one shared shape, archives it on the **host** in a location that survives instance teardown, and exposes it through a `jackin console`-style TUI, a CLI, and an MCP server so both operators and agents can find, read, and re-live past work.

The name is literal: Déjà Vu is the feeling of seeing something again. The first deliverable is exactly that — see your past agent conversations again.

## Scope of the first cut [#scope-of-the-first-cut]

Capture, store, and browse. Nothing more, deliberately:

* **Must capture** every operator input (the full prompt thread) and every agent answer the operator visually saw (the rendered assistant turns). This is the load-bearing requirement: a Déjà Vu that loses what the operator typed or what the agent replied is not worth shipping.
* **Should capture, when the native source makes it cheap** tool calls, tool results, thinking/reasoning blocks, diffs, model id, and token usage. These come for free from most agents' session files; capture them when present, never block on them.
* **Browse** select a workspace → narrow by instance or date → pick a conversation → read the thread, top to bottom.

Everything else — forking a conversation, change/diff attribution per turn, semantic search, cross-agent `@thread` injection, cloud sharing, orchestration — is explicitly out of this cut and tracked under [Non-goals for V1](#non-goals-for-v1).

The rest of this document is primarily about **extraction**: the technical question of *how* you get a complete, faithful conversation out of each agent. Storage and browse are the easy half; extraction is where the real engineering is, because each agent exposes a different set of surfaces with different fidelity, stability, and cooperation requirements. The research below catalogs every public extraction vector, validates each against agent source or docs, maps which conversation data each yields, and recommends the primary vector per agent.

## Prior art: how SpecStory extracts (the key lesson) [#prior-art-how-specstory-extracts-the-key-lesson]

[SpecStory](https://specstory.com/) is the closest existing product, and the question that matters for us is not how it stores conversations but **how it gets them out of the agents**. The answer is the single most important design input here, and it is decisively simple.

SpecStory's CLI ([`specstoryai/getspecstory`](https://github.com/specstoryai/getspecstory), Go, Apache-2.0) does **not** wrap the agent's PTY, scrape the terminal, intercept the network, or use hooks. It reads each agent's **own on-disk session files** and converts them. Verified from source:

* **`specstory run <agent>`** launches the agent with **plain passthrough stdio** — `exec.Command(agentCmd, args...)` with `cmd.Stdin/Stdout/Stderr = os.Stdin/Stdout/Stderr` (no `creack/pty`, no PTY dependency in `go.mod`) — and concurrently runs an [`fsnotify`](https://github.com/fsnotify/fsnotify) file-watcher on the agent's session directory, converting new/changed session files to Markdown as they appear.
* **`specstory watch [<agent>]`** runs only the `fsnotify` watcher against the session directory, auto-saving an agent the operator launched separately.
* **`specstory sync [<agent>]`** is a retroactive batch conversion of already-stored sessions (`-s <session-uuid>` for one).

Each agent has a provider package (`pkg/providers/<agent>/`) whose `path_utils.go` knows the on-disk location, whose `jsonl_parser.go` / `sqlite_reader.go` parses the native format, and the results land in one shared `SessionData` struct that a single Markdown renderer emits. The supported sources are all native session stores: Claude Code `~/.claude/projects/<mangled-cwd>/*.jsonl` (JSONL), Codex `~/.codex/sessions/*.jsonl` (JSONL), Cursor CLI `~/.cursor/chats/<md5>/<uuid>/store.db` (**SQLite**, read with the pure-Go `modernc.org/sqlite`), Gemini CLI `~/.gemini/tmp/<hash>/` (JSON), Droid `~/.factory/sessions/` (JSONL), DeepSeek `~/.deepseek/sessions/*.json` (JSON). The closed-source IDE extensions (Cursor, VS Code + Copilot) do the editor equivalent — they read the editor's chat store, which Cursor/VS Code keep in a SQLite `state.vscdb` under `workspaceStorage`/`globalStorage`.

**The lesson for Déjà Vu:** the highest-fidelity, lowest-cooperation, fully-passive way to capture a conversation is to read the agent's own session file. SpecStory proves a per-provider-parser → one-normalized-record → render-on-demand architecture works across many agents. jackin' should do the same as its primary vector — and, because jackin' *owns the container and the multiplexer*, it has additional vectors SpecStory does not (built-in telemetry, wire interception, and screen scraping), available as fallbacks where a native file is missing or insufficient.

## Leverage: jackin' already puts the history on the host [#leverage-jackin-already-puts-the-history-on-the-host]

The critical enabler is already shipped. jackin' bind-mounts each agent's home into the container and restores it on the host under the per-instance data dir — `~/.jackin/data/<container>/.claude/`, `.codex/`, `.local/share/amp/`, `.kimi-code/`, and `.local/share/opencode/` all carry the agent's conversation history (see [Session keep and resume](/reference/roadmap/session-keep-and-resume/)). That means **for four of the five agents the raw conversation is already a host-side file jackin' can read directly** — no terminal scraping, no new container mount, no host mutation. For those agents Déjà Vu's capture step is a host-side read of paths jackin' already owns, plus a normalize-and-copy into a purge-surviving archive. Only `Amp` (cloud-only) needs a different vector.

## Extraction vectors (research) [#extraction-vectors-research]

There are six mechanically distinct ways to get a coding agent's conversation out. They differ along three axes that decide which one Déjà Vu uses for a given agent: **passive vs cooperative** (does the agent author have to do anything — a hook script, a launch flag, an env var — or can jackin' do it transparently), **live vs post-hoc**, and **fidelity** (which fields it yields).

| #     | Vector                        | Where it taps                                                        | Live / post-hoc    | Passive?                                                       | Vendor-blessed?                               |
| ----- | ----------------------------- | -------------------------------------------------------------------- | ------------------ | -------------------------------------------------------------- | --------------------------------------------- |
| **A** | **Native session file**       | The agent's own on-disk JSONL/SQLite transcript                      | Post-hoc (or tail) | Yes — file read, zero cooperation                              | Format usually internal/undocumented          |
| **B** | **Lifecycle hooks**           | Agent runs a command on events (prompt submit, tool use, stop)       | Live               | No — agent must support hooks; jackin' installs them at launch | Yes                                           |
| **C** | **Built-in OpenTelemetry**    | Agent's own instrumentation emits OTLP logs/metrics/traces           | Live               | Semi — env-configured once, then passive                       | Yes                                           |
| **D** | **Wire interception**         | The agent↔model HTTP boundary (base-URL redirect, fetch patch, MITM) | Live               | Yes — operator controls env/proxy, agent unaware               | Config blessed; capture-via-proxy operational |
| **E** | **Headless stream-json**      | Agent stdout in `--print` / `--output-format stream-json` mode       | Live               | No — you launch the run headless                               | Yes                                           |
| **F** | **Multiplexer screen scrape** | jackin's own `DamageGrid` screen + scrollback for the pane           | Live / post-hoc    | Yes — jackin owns the PTY                                      | n/a (jackin-internal)                         |

A seventh item is not a tap but the shared *schema* these converge on: the **OpenTelemetry GenAI semantic conventions** (`gen_ai.*` — `gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, token-usage and finish-reason attributes; the older opt-in `gen_ai.content.prompt`/`gen_ai.content.completion` events were deprecated in semconv v1.38.0). Where Déjà Vu needs a normalized on-the-wire vocabulary, this is the standard to align to.

### What each vector yields [#what-each-vector-yields]

Columns: UP = user prompt text · AT = assistant text · TC = tool calls (name + input) · TR = tool results/output · TH = thinking/reasoning · TOK = token usage · DIFF = code diffs · TIME = timing. ✅ available · ⚠️ conditional · ❌ not available.

| Vector                          | UP              | AT                                                                           | TC                     | TR                     | TH                      | TOK               | DIFF               | TIME |
| ------------------------------- | --------------- | ---------------------------------------------------------------------------- | ---------------------- | ---------------------- | ----------------------- | ----------------- | ------------------ | ---- |
| **A. Native session file**      | ✅               | ✅                                                                            | ✅                      | ✅                      | ✅                       | ✅                 | ✅                  | ✅    |
| **B. Hooks**                    | ✅               | ⚠️ via `transcript_path` re-read (Claude); ⚠️ `SubagentStop.response` (Kimi) | ✅                      | ✅                      | ⚠️ via transcript       | ⚠️ via transcript | ⚠️ via transcript  | ✅    |
| **C. OTEL (defaults)**          | ❌ length only   | ❌                                                                            | ⚠️ name/decision only  | ❌ size only            | ❌                       | ✅                 | ✅                  | ✅    |
| **C. OTEL (all content flags)** | ✅               | ✅ (Claude `OTEL_LOG_RAW_API_BODIES`)                                         | ✅                      | ✅                      | ❌ **always redacted**   | ✅                 | ✅                  | ✅    |
| **D. Wire interception**        | ✅               | ✅                                                                            | ✅                      | ✅                      | ✅                       | ✅                 | ⚠️ inside tool I/O | ✅    |
| **E. stream-json**              | ✅               | ✅                                                                            | ✅                      | ✅                      | ⚠️ if thinking surfaced | ✅                 | ⚠️ inside tool I/O | ✅    |
| **F. Screen scrape**            | ✅ (post-render) | ✅ (post-render)                                                              | ⚠️ only what's printed | ⚠️ only what's printed | ⚠️ only if printed      | ❌                 | ⚠️ if printed      | ✅    |

Two asymmetries decide the design:

1. **The native session file (A) is the only single vector that captures everything losslessly with zero cooperation** — which is why it is the primary vector wherever it exists (Claude Code, Codex, Kimi, OpenCode).
2. **Thinking/reasoning is the discriminator.** The agent's own OTEL never exports thinking content (Claude Code redacts extended-thinking from raw API bodies regardless of flags). Wire interception (D) is the only vector that captures the model-facing system prompt and tool *definitions* exactly as sent, plus thinking blocks — at the cost of being headless-only for some agents. The native file captures thinking too, for the agents that persist it.

## Per-agent extraction surfaces [#per-agent-extraction-surfaces]

Each agent's *resume* command is the proof its format is replayable — if the agent can reconstruct a thread from disk, so can Déjà Vu. Below, per agent, every public extraction surface, what it yields, whether it is passive, and its stability, followed by the recommended primary vector. Validated against each agent's source or docs.

### Claude Code (`@anthropic-ai/claude-code`) [#claude-code-anthropic-aiclaude-code]

| Surface                                                                                                                 | Yields                                                                                                                                                                                                                                                                                 | Passive?                       | Stability                                                                                                                                                                                                                           |
| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Native JSONL** `~/.claude/projects/<encoded-cwd>/<id>.jsonl` (subagents in `…/<id>/subagents/agent-*.jsonl`)          | Full thread: user/assistant/`tool_use`/`tool_result`/`thinking`, `uuid`↔`parentUuid` tree, model, token usage, cwd, git                                                                                                                                                                | Yes                            | Internal format (undocumented; stable in practice)                                                                                                                                                                                  |
| **Hooks** (`UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, `SessionStart/End`, …)              | stdin JSON; `UserPromptSubmit.prompt` (literal text), `PostToolUse.tool_response`; &#x2A;*every payload carries `transcript_path`** → pivot to the JSONL                                                                                                                               | No (jackin installs at launch) | Vendor-blessed                                                                                                                                                                                                                      |
| **Built-in OTEL** (`CLAUDE_CODE_ENABLE_TELEMETRY=1`)                                                                    | `claude_code.user_prompt` (redacted unless `OTEL_LOG_USER_PROMPTS=1`), `tool_result`, `tool_decision`, `api_request`; cost/token metrics. `OTEL_LOG_RAW_API_BODIES=1` (or `=file:<dir>`) emits full request+response bodies incl. **assistant text and the full conversation history** | Semi (env set once)            | Vendor-blessed; **extended-thinking always redacted**                                                                                                                                                                               |
| **`--print --output-format stream-json`** (`--include-partial-messages`)                                                | Live NDJSON: `system`(init), `assistant`(text+`tool_use`), `user`(tool\_result), `stream_event` deltas, `result`(usage)                                                                                                                                                                | No (you launch headless)       | Vendor-blessed                                                                                                                                                                                                                      |
| **`ANTHROPIC_BASE_URL` redirect / fetch-patch** (`claude-trace` fetch-patch, and cross-agent proxies like `claude-tap`) | Full wire request+response: system prompt, tool defs, all messages, **thinking blocks**, cache/token usage                                                                                                                                                                             | Yes                            | Operational; &#x2A;*interactive `claude` ignores `ANTHROPIC_BASE_URL` — reliable only with `-p`** ([#36998](https://github.com/anthropics/claude-code/issues/36998)); fetch-patch (claude-trace via `node --require`) works in both |
| **Agent SDK** `@anthropic-ai/claude-agent-sdk` (`query()`)                                                              | Typed streamed messages (text + `tool_use`, `tool_result`, `result`)                                                                                                                                                                                                                   | No (you author the run)        | Vendor-blessed                                                                                                                                                                                                                      |

**Primary for Déjà Vu:** native JSONL (A), already on the host. Hooks (B) for liveness. Wire interception (D) only if we ever need the exact system prompt / thinking beyond what the JSONL holds.

### Codex CLI (`openai/codex`, Rust) [#codex-cli-openaicodex-rust]

| Surface                                                                           | Yields                                                                                                                                                 | Passive? | Stability                                                      |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -------------------------------------------------------------- |
| **Native rollout JSONL** `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl` | Full thread auto-saved: `SessionMeta`, `ResponseItem`, `EventMsg`(`UserMessage`/`AgentMessage`), `TurnContext`, `Compacted`; what `codex resume` reads | Yes      | Internal format (vendor warns it is not a stable interface)    |
| **`notify` callback**                                                             | JSON on `agent-turn-complete`: `input-messages` (user) + `last-assistant-message` + `cwd`                                                              | No       | Vendor-blessed but coarse (last turn only)                     |
| **Hooks** (`requirements.toml`, `allow_managed_hooks_only`)                       | Exists; event list / payload content **unverified**                                                                                                    | No       | Vendor-blessed (details unconfirmed)                           |
| **`codex exec --json`**                                                           | NDJSON: `thread.*`, `turn.*`, `item.*` (`agent_message`, `reasoning`, command exec, file changes, MCP tool calls)                                      | No       | Vendor-blessed (`--json` vs older `--experimental-json` churn) |
| **Built-in OTEL** (`[otel]` in config)                                            | `codex.user_prompt` (text only if `log_user_prompt=true`), `codex.tool_decision`, `codex.tool_result`, `codex.api_request`, `codex.sse_event`          | Semi     | Vendor-blessed; `exec` has no metrics, `mcp-server` emits none |
| **`OPENAI_BASE_URL` / `[model_providers]` base\_url**                             | Full wire traffic via a proxy (`wire_api = responses`; Chat-only proxies need translation)                                                             | Yes      | Config blessed; capture operational                            |

**Primary:** native rollout JSONL (A). Because the vendor flags the format as unstable, pair it with hooks/`notify` (B) or `--json` (E) as a stability hedge; OTEL (C) for tool decisions and cost.

### Amp (Sourcegraph) [#amp-sourcegraph]

Amp is the outlier: &#x2A;*threads are server-side (cloud Postgres); there is no durable local transcript on disk.** Capture must come from a thread surface, not a file.

| Surface                                                                           | Yields                                                                                                                                                                                                | Passive?                  | Stability                                                                                                                                                    |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`amp --execute --stream-json`** (and `amp threads continue <id> --stream-json`) | Live NDJSON: `system`(init), `user`, `assistant`(+`tool_use`), `tool_result`, final `result`; subagents via `parent_tool_use_id`. `--stream-json-thinking` adds thinking (not Claude-Code-compatible) | No (you launch / re-emit) | Vendor-blessed                                                                                                                                               |
| **`amp threads` subcommands** (`list`, `continue`, `fork`, `share`, `compact`)    | Thread enumeration + resume; threads sync to ampcode.com                                                                                                                                              | No                        | Vendor-blessed; **`export`/`markdown` and a public REST `/api/threads` are unconfirmed for the core CLI** (markdown exists only in a third-party Elixir SDK) |
| **SDK** `@sourcegraph/amp-sdk` (TS)                                               | Wraps `--execute --stream-json`                                                                                                                                                                       | No                        | Vendor-blessed (core `@sourcegraph/amp` is now a CLI alias, not a library)                                                                                   |
| **OTEL / model base-URL proxy**                                                   | None — Amp brokers models server-side; no model-base-URL override surfaced                                                                                                                            | —                         | Absent                                                                                                                                                       |

**Primary:** `--stream-json` on launch (E), captured live, since there is no file to read and the server REST is not a documented public surface. This is the one agent where Déjà Vu must capture *during* the session rather than reading after the fact — and the one where the multiplexer fallback (F) matters most if a session was started outside Déjà Vu's stream capture.

### Kimi (`kimi-code`, the TypeScript distribution jackin' ships) [#kimi-kimi-code-the-typescript-distribution-jackin-ships]

jackin' installs the `kimi-code` binary from `cdn.kimi.com/kimi-code` with home `~/.kimi-code/` (not the legacy Python `kimi-cli`, which used `~/.kimi/…/context.jsonl`). Surfaces below are for `kimi-code`.

| Surface                                                                                                                | Yields                                                                                                                       | Passive? | Stability                                                                                      |
| ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| **Native JSONL** `~/.kimi-code/sessions/<wd_<slug>_<sha256[:12]>>/<sessionId>/agents/<id>/wire.jsonl` (+ `state.json`) | Per-agent append-only journal: `message`/`event`/`request`/`response` (roles user/assistant/tool, tool-use blocks, `turnId`) | Yes      | Internal format                                                                                |
| **Hooks** (`[[hooks]]` in `config.toml`; 13 events)                                                                    | `UserPromptSubmit.prompt`, `PreToolUse`/`PostToolUse`, `SubagentStop.response` (assistant text), `SessionStart/End`          | No       | Vendor-blessed (Beta); **no `transcript_path` exposed** — cannot pivot to the file from a hook |
| **`--print --output-format stream-json`**                                                                              | OpenAI-chat-shaped role stream (`user`/`assistant`+`tool_calls`/`tool`); no `init`/`result` envelope                         | No       | Vendor-blessed                                                                                 |
| **Wire mode (`--wire`)** / &#x2A;*ACP (`kimi acp`)**                                                                   | JSON-RPC 2.0 over stdio: assistant chunks, tool calls, hook events; ACP makes Kimi tappable by any ACP client (e.g. Zed)     | No       | Vendor-blessed (wire experimental)                                                             |
| **Provider `base_url` (config) proxy**                                                                                 | Full wire traffic via a proxy (per-provider `base_url`, no env override)                                                     | Yes      | Config blessed; capture operational                                                            |
| **OTEL**                                                                                                               | None (only an anonymous on/off telemetry toggle)                                                                             | —        | Absent                                                                                         |

**Primary:** native `wire.jsonl` (A) from the restored home. Hooks (B) for liveness, with the caveat that Kimi hooks lack `transcript_path`, so the file must be located by recomputing the `wd_<slug>_<sha256[:12]>` key.

### OpenCode (`sst/opencode`) [#opencode-sstopencode]

| Surface                                                                                            | Yields                                                                                                         | Passive?                                          | Stability                                   |
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------- |
| **Native store** `~/.local/share/opencode/storage/{session,message,part}/…json` (or `opencode.db`) | Full thread: `Session`/`Message`(role-discriminated)/`Part`(`text`/`reasoning`/`tool`/`file`/`patch`/`step-*`) | Yes                                               | Internal format (mid JSON→SQLite migration) |
| **`opencode serve` HTTP API** (`GET /session`, `GET /session/:id/message`)                         | `{ info, parts[] }[]` — complete history; OpenAPI 3.1 spec drives the SDK                                      | Cooperative (run the server) then passive polling | Vendor-blessed                              |
| **SSE `GET /event`**                                                                               | Live bus: `message.updated`, `message.part.updated` (text deltas), `session.*` — 80+ event types               | Cooperative (run server)                          | Vendor-blessed                              |
| **`opencode run --format json`**                                                                   | Per-run JSON result                                                                                            | No                                                | Vendor-blessed                              |
| **SDK** `@opencode-ai/sdk` / **plugins** (`tool.execute.before/after`)                             | Typed client over HTTP+SSE; plugins observe tool I/O                                                           | No                                                | Vendor-blessed                              |
| **OTEL**                                                                                           | None native (third-party `opencode-plugin-otel` only)                                                          | —                                                 | Third-party                                 |

**Primary:** native store (A) from the restored home. The `serve` HTTP+SSE API (E-ish) is the cleanest live-tap if Déjà Vu ever wants real-time capture without parsing files.

### Cross-agent summary [#cross-agent-summary]

* **Native session file (A)** is available and passive for **four of five** (all but Amp) and is the recommended primary everywhere it exists. jackin' already restores these onto the host.
* **Hooks (B)** exist for Claude Code, Codex, Kimi, OpenCode (plugins). Only &#x2A;*Claude Code's hooks hand back `transcript_path`**; the rest carry inline `prompt`/`response` text but no pointer to the full record. Amp has no lifecycle conversation hook.
* **Headless stream-json (E)** is near-universal (all five) and is the **only** good vector for **Amp**.
* **Built-in OTEL with prompt content (C)** is Claude Code and Codex only, and both deliberately omit assistant response text from first-class events (Claude can include it via raw-body logging; neither exposes thinking).
* **Wire interception (D)** is the only vector that yields the system prompt + tool definitions as-sent and thinking blocks; passive via base-URL/proxy for Claude Code (headless), Codex, Kimi; not available for Amp (server-brokered).

## What the observability ecosystem already solved [#what-the-observability-ecosystem-already-solved]

Agent observability tooling has converged on exactly these vectors, which validates the taxonomy and tells us which off-the-shelf ideas to borrow:

* **Native-file readers** — `vibe-log` (Claude Code + Codex JSONL → reports, sanitized before optional sync), `claude-code-templates` observability dashboard, `claude-code-log`, `claude-JSONL-browser`. Same vector A, same per-provider-parser shape Déjà Vu uses.
* **Hook fan-out** — `claude-code-hooks-multi-agent-observability` POSTs hook events to a local server in real time. Same vector B.
* **Built-in OTEL collectors** — `claude-code-otel`, `claude_telemetry`/"claudia" (a drop-in `claude` wrapper that sets the OTEL env vars), and `Langfuse` ingesting Claude Code's OTLP directly. Same vector C.
* **Wire-level tracers** — `claude-trace` (fetch-patch via `node --require`, captures system prompt + tool defs + thinking + raw req/resp pairs) and `claude-tap` (a cross-agent reverse/forward proxy covering Claude Code, Codex, Gemini, Cursor, OpenCode, Kimi). Same vector D — and proof that one proxy can capture *all* the agents jackin' runs.
* **SDK/standard schema** — OpenLLMetry/Traceloop and Arize Phoenix/OpenInference instrument app code and emit the `gen_ai.*` / OpenInference conventions; relevant to Déjà Vu only as the normalized vocabulary to align the canonical record to.

The takeaway: nobody has built the *cross-agent, host-side, browse-first archive* Déjà Vu targets, but every extraction primitive it needs is proven in the wild. Déjà Vu's novelty is the unification (one record, one browse surface, keyed by workspace/instance) and the integration with jackin's container ownership — not the act of extraction itself.

## Decision: layered fallback, chosen per agent [#decision-layered-fallback-chosen-per-agent]

Capture is a layered fallback, selected per agent from the vectors above, in priority order:

1. **Native session file (A) — source of truth.** Claude Code, Codex, Kimi, OpenCode. Read host-side from the restored per-instance home. Lossless, passive, captures historical sessions, no agent cooperation.
2. **Vendor stream / API / export (E) — cloud and unstable-format agents.** Amp (`--stream-json`, captured live) has no file to read. Also the safe path for any agent whose file format the vendor declares unstable (Codex says exactly this) and OpenCode's `serve` API for live capture.
3. **Hooks and built-in OTEL (B, C) — liveness and a vendor-blessed hedge.** Where present (Claude Code, Codex, Kimi, OpenCode plugins), subscribe for low-latency capture and as a signal that does not depend on reverse-engineering a file layout.
4. **Wire interception (D) — when the exact model-facing payload or thinking is needed.** Optional, passive for most agents; headless-only for interactive Claude Code.
5. **Multiplexer screen scrape (F) — last resort.** Only when none of A–D is available for a session (see below). Lossy and gated.

Per-provider parsers normalize whichever source is used into one record shape, so the storage and browse layers never learn five formats.

## Last-resort fallback: extract from the multiplexer [#last-resort-fallback-extract-from-the-multiplexer]

jackin' owns the in-container PTY multiplexer (`jackin-capsule`), which gives it a capture vector SpecStory and the file-readers do not have: the rendered terminal itself. This is genuinely useful as a **last resort** — e.g. an Amp session started outside Déjà Vu's stream capture, or any agent whose session file is missing — but it is deliberately ranked below every structured vector, and the source confirms exactly why.

What the multiplexer can actually see (validated in <RepoFile path="crates/jackin-capsule/src/session.rs">crates/jackin-capsule/src/session.rs</RepoFile>):

* Capsule keeps one `DamageGrid` per session; its `GridSnapshot`/`GridPatch` output is the source of truth for visible terminal state. Raw PTY bytes are read in a blocking reader loop and fed to the grid, then **dropped** — there is no raw-byte or asciicast log retained by default.
* It retains bounded primary-screen scrollback in `DamageGrid` plus a separate inline-scrollback buffer for Codex-style top-anchored scroll regions. The pane renderer already reads cells from `GridSnapshot`/`GridPatch`.

So jackin' can scrape the conversation as **rendered cells across \~10k lines of scrollback**, not just the 24-row viewport. But the caveats are real and disqualify it as a primary vector:

* **Post-render and lossy.** It is the styled text the operator saw, with TUI chrome, spinners, redraw artifacts, and box-drawing baked in — not structured turns. Tool inputs/outputs appear only as far as the agent printed them; token usage is absent.
* **No durable raw log.** Raw bytes are dropped after parsing, so there is no lossless replay source unless [terminal observation and automation](/reference/roadmap/terminal-observation-automation/) adds opt-in recording first.
* **\~10k-line horizon.** Anything older than the retained scrollback is gone.
* **Secrets.** Rendered terminal output can contain tokens, paths, and file contents; screen-scrape capture must be opt-in and redaction-aware, consistent with the recording stance in [terminal observation and automation](/reference/roadmap/terminal-observation-automation/).

This fallback shares its substrate and its discipline with the [agent runtime status authority](/reference/roadmap/agent-runtime-status/) (the herdr-inspired status work), which already reads the current terminal snapshot to detect agent state. That item's hard-won rule applies here verbatim: **read the current screen, never trust arbitrary scrollback as authority**, and prefer structured/reported signals over screen parsing. Déjà Vu's screen-scrape tier is the conversation-capture analogue of that status detector — same `DamageGrid&#x60; source, same "structured first, screen last" ranking, same per-agent visible-chrome knowledge. Where the status authority asks &#x2A;"is the agent working or blocked?"&#x2A;, Déjà Vu's fallback asks &#x2A;"what text was exchanged?"* — and both should reuse one screen-reading layer in Capsule rather than building two.

The framing to keep straight: Déjà Vu's **default** capture does not scrape the terminal (it reads native files); screen scraping is an explicit, gated, last-resort tier for sessions where no structured surface exists.

## Storage shape [#storage-shape]

### The normalized record [#the-normalized-record]

One canonical record per conversation captures: identity (workspace, instance/container, agent, native session id, captured-at), the ordered turns (role, author, the visible content the operator saw, timestamp), and optional enrichment (tool calls/results, model id, token usage) when the source supplies it. Per-agent quirks (Claude's `parentUuid` tree, Kimi's `turnId`, Codex's `RolloutItem` variants, the vector each turn was captured by) are flattened into this shape at parse time and preserved verbatim in an opaque `raw` blob so nothing is thrown away. Aligning the field vocabulary to the OpenTelemetry GenAI conventions (`input.messages`/`output.messages`/`system_instructions`) keeps the record interoperable with the observability ecosystem above.

### Format: compact canonical blob + index + render-on-demand [#format-compact-canonical-blob--index--render-on-demand]

The operator's preference, and the right call, is a **compact binary canonical store** (Protocol Buffers) rather than keeping fat Markdown or JSON as the system of record. Protobuf gives a schema-checked, space-efficient, append-friendly encoding that is cheap to keep for thousands of conversations, and Markdown/JSON are *derived* views generated on demand — agents never have to decode protobuf, because the CLI/MCP render it to Markdown or JSON for them. This separates "how we store it" from "how we show it."

Alongside the blobs, a small **SQLite index** powers browse and search (workspace, instance, date, agent, title, first prompt, turn count). This deliberately mirrors the [persistent storage layer](/reference/roadmap/persistent-storage-layer/) — Déjà Vu's index should live in, or beside, that layer rather than inventing a parallel one, and the canonical conversation blobs sit next to the index as content-addressed or id-named files.

The library choice (`prost` vs `protobuf` for the schema; `sqlx` vs `rusqlite` for the index) should follow the same decision the persistent storage layer makes, and is worth an ADR (see [Architecture Decision Records](/reference/adrs/)). JSONL was considered as a simpler canonical format and remains the fallback if protobuf's build/codegen cost is judged not worth it for V1; the index and render-on-demand design hold either way.

### Host layout [#host-layout]

Conversations must outlive the instance that produced them — that is the whole point. So the archive lives under jackin's host data root, keyed by workspace, **outside** any per-instance `~/.jackin/data/<container>/` tree that `eject`/`purge` reclaims:

```text
~/.jackin/data/deja-vu/
  index.db                                  # SQLite browse/search index
  <workspace>/
    <instance-or-date>/
      <agent>/
        <conversation-id>.pb                # canonical protobuf blob
```

Keying by workspace (then instance/date, then agent) matches the browse flow directly: workspace → instance/date → conversation. Whether the index is Déjà Vu-private or a table inside the persistent storage layer's DB is an [open question](#open-questions).

## Capture timing [#capture-timing]

The host-side capture runs at the moments that matter and never depends on the operator remembering to act:

* **On session end / eject (the safety net).** Before `jackin eject` / `jackin prune` reclaims a per-instance data dir, Déjà Vu normalizes and copies every conversation it finds into the purge-surviving archive. This directly fixes "exiting the instance cleans up the conversation I still needed."
* **Periodically / on demand for live instances.** A `jackin deja-vu sync` re-reads the restored homes and upserts new turns, so long-running instances are captured incrementally without waiting for teardown.
* **Optionally live via hooks / stream-json.** Where hooks exist, capture can be near-real-time; for Amp, live `--stream-json` capture is the only complete option.

## Browse surfaces [#browse-surfaces]

Three surfaces over the same archive, in priority order for V1:

* **TUI (`jackin deja-vu`, console-style).** The headline deliverable. A `jackin console`-shaped browser: pick a workspace, narrow by instance or date, pick a conversation, read the rendered thread top to bottom. Follows the [TUI design decisions](/reference/tui//) (W3C Tabs navigation, scrollable blocks via `jackin_tui::scroll`, the shared color palette).
* **CLI (`jackin deja-vu list | show | search | export`).** Scriptable and, importantly, the surface an *agent* can call from inside a session. `show` renders a conversation to Markdown/JSON; `export` writes it out.
* **MCP server.** Exposes `list` / `read` / `search` as MCP tools so an agent — in any runtime — can pull a past conversation into its current context. This is the seed of the future cross-agent "go back to that conversation" experience and the orchestration use case, gated behind the same host-mediated MCP/host-bridge surface as other agent-facing capabilities (see [Host bridge](/reference/roadmap/host-bridge/)).

## Architecture [#architecture]

Three layers, mirroring how the rest of jackin' separates capsule / protocol / host:

| Layer                      | Owns                                                                                                                                               | Does not own                                       |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| Per-agent providers        | Locating, reading, and parsing each agent's chosen extraction vector (native file / stream-json / API / hooks / screen) into the normalized record | The archive format, the index, or the browse UI    |
| Archive + index            | The canonical protobuf store, the SQLite index, capture timing, render-on-demand to Markdown/JSON                                                  | How any single agent encodes its own sessions      |
| Surfaces (TUI / CLI / MCP) | Operator and agent access: browse, search, show, export                                                                                            | Re-parsing native formats or writing native stores |

The default capture path is host-side and read-only against agent state: it reads the per-instance homes jackin' already restores and runs read-only vendor exports/streams (`amp threads`/`--stream-json`). The screen-scrape fallback consumes Capsule's existing `DamageGrid` screen layer. The only writes are into Déjà Vu's own `~/.jackin/data/deja-vu/` tree.

## Host-side effects [#host-side-effects]

Per the [security model](/guides/security-model/) and the never-mutate-the-host-silently contract, this item is explicit about what it touches on the host:

* **Reads** the per-instance agent homes under `~/.jackin/data/<container>/` (jackin's own data; reading is always allowed) and runs read-only network fetches of the operator's *own* Amp threads/streams using the already-forwarded `AMP_API_KEY`.
* **Writes** only under `~/.jackin/data/deja-vu/` — jackin's own host root, the sanctioned location for jackin-owned state. It never writes into the operator's repositories, `~/.claude/`, `~/.codex/`, `~/.config/gh/`, dotfiles, or any non-jackin path.
* Capture is **opt-in and surfaced** (config flag / CLI flag), and the archive location is reported, consistent with the host-effects contract. The wire-interception (D) and screen-scrape (F) vectors, because they can capture secrets and model-facing payloads, are additionally opt-in and redaction-aware. No container-side path is required for the file-reading path; if a future capsule-side capture point is added it lives under `/jackin/` (e.g. `/jackin/deja-vu/`), never under `/run`, `/var`, or `/tmp`.

## Phases [#phases]

### Phase 0 — Normalized record + one provider [#phase-0--normalized-record--one-provider]

Define the normalized record and the protobuf schema. Implement the Claude Code native-file provider (richest, best-documented format) and a `jackin deja-vu show` that renders one captured conversation to Markdown. No archive yet — prove the parse → normalize → render path end to end.

### Phase 1 — Archive, index, and the eject safety net [#phase-1--archive-index-and-the-eject-safety-net]

Add the `~/.jackin/data/deja-vu/` archive, the SQLite index, and capture-on-eject so the operator's pain point is fixed first. `jackin deja-vu list` and `jackin deja-vu sync`.

### Phase 2 — Remaining native-file providers + Amp stream [#phase-2--remaining-native-file-providers--amp-stream]

Codex, Kimi (`kimi-code`), and OpenCode native-file providers, plus the Amp live `--stream-json` provider. Confirm each against a live in-container layout before trusting it.

### Phase 3 — Browse TUI [#phase-3--browse-tui]

The `jackin deja-vu` console-style browser: workspace → instance/date → conversation → thread. The first surface most operators will actually use.

### Phase 4 — Search, MCP, and liveness [#phase-4--search-mcp-and-liveness]

Index-backed search (`jackin deja-vu search`), the MCP server exposing list/read/search to agents, and live capture via hooks / stream-json / the OpenCode `serve` API for long-running instances.

### Phase 5 — Optional deep-fidelity and fallback vectors [#phase-5--optional-deep-fidelity-and-fallback-vectors]

Wire interception (D) for the exact system prompt / thinking where an operator wants it, and the multiplexer screen-scrape (F) last-resort tier sharing Capsule's `DamageGrid` layer with the [agent runtime status authority](/reference/roadmap/agent-runtime-status/). Both opt-in and redaction-aware.

## Non-goals for V1 [#non-goals-for-v1]

Named so later work has a home and V1 stays small:

* **Forking / branching a conversation** (git-style divergence from a past turn). Future.
* **Change attribution** — linking each turn to the files/diffs it produced. The user wants this eventually ("what changed, what produced it"); V1 captures only what was said and shown.
* **Semantic / vector search.** V1 is index + keyword. Semantic retrieval waits until there are enough captured conversations to evaluate quality, aligned with the [persistent storage layer](/reference/roadmap/persistent-storage-layer/) memory direction.
* **Cross-agent `@thread` injection** (Amp-style referencing another conversation inside a live prompt). The MCP read surface is the precursor; the injection UX is future.
* **Cloud sharing / team sync.** Local-host-first; the record schema should keep stable ids and provenance so sync is possible later, but it is not in scope.
* **Orchestration.** Déjà Vu provides the read surface a future orchestrator consumes (see [Agent workflow orchestration](/reference/roadmap/agent-workflow-orchestration/)); it does not schedule or drive agents.

## Open questions [#open-questions]

| Question                                                                                   | Current stance                                                                                                                                                                                                    |
| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Protobuf vs JSONL as the canonical store?                                                  | Protobuf for compactness + schema, per operator preference; JSONL is the documented fallback if codegen cost outweighs the benefit for V1. Decide in an ADR.                                                      |
| Déjà Vu-private index vs a table in the persistent storage layer?                          | Prefer reusing the [persistent storage layer](/reference/roadmap/persistent-storage-layer/) once it lands; ship a small private SQLite index first if Déjà Vu moves earlier.                                      |
| Workspace-scoped vs instance-scoped archive?                                               | Workspace-scoped under `~/.jackin/data/deja-vu/<workspace>/` so it survives per-instance purge — the explicit requirement. Instance and date are sub-keys, not the top key.                                       |
| Is reading the agent's native session file durable given vendors call the format internal? | Treat it as the primary vector but pair unstable-format agents (Codex explicitly) with a vendor-blessed vector (hooks / stream-json / OTEL) so a format change degrades rather than breaks capture.               |
| How is the Kimi `kimi-code` on-disk layout confirmed?                                      | Verify live inside a running jackin' Kimi container (`ls ~/.kimi-code/sessions/…`); the shipped distribution may differ from both the documented `kimi-code` and the legacy Python `kimi-cli` trees.              |
| Capture on every eject by default, or opt-in?                                              | Lean opt-in-but-default-on per workspace, surfaced in the launch summary, so the safety net is real without being a silent host write.                                                                            |
| Do we capture tool calls/diffs/thinking in V1?                                             | Capture them when the native source provides them for free; never block the must-have (prompts + visible answers) on them. Thinking and the model-facing system prompt need vector D and are deferred to Phase 5. |
| Should the screen-scrape fallback and the status authority share one screen-reading layer? | Yes — both read Capsule's `GridSnapshot`; build one reusable screen-extraction layer rather than two.                                                                                                             |

## Related roadmap items [#related-roadmap-items]

* [Session keep and resume](/reference/roadmap/session-keep-and-resume/) — the shipped mechanism that already restores agent conversation history onto the host; Déjà Vu's primary capture source.
* [Persistent storage and workspace memory layer](/reference/roadmap/persistent-storage-layer/) — the index/DB home Déjà Vu should reuse, and the cross-agent memory direction it complements.
* [Terminal observation and automation](/reference/roadmap/terminal-observation-automation/) — owns the PTY/`DamageGrid` read APIs and opt-in recording the screen-scrape fallback would build on.
* [Agent runtime status authority](/reference/roadmap/agent-runtime-status/) — the herdr-inspired status work; shares Capsule's screen-reading substrate and the "structured signals first, screen last, never trust scrollback as authority" discipline.
* [Multi-runtime support](/reference/roadmap/multi-runtime-support/) — the five-runtime model whose per-agent extraction surfaces the providers normalize.
* [Host bridge](/reference/roadmap/host-bridge/) — the host-mediated MCP surface the agent-facing read tools plug into.
* [jackin' daemon](/reference/roadmap/jackin-daemon/) — the future long-running host process that could own periodic/live capture.
* [Agent workflow orchestration](/reference/roadmap/agent-workflow-orchestration/) — future consumer of the captured-conversation read surface.
