Déjà Vu — Cross-Agent Conversation Capture
Status: Open — research and design proposal (Phase 2, Agent Orchestrator Research Program)
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 prunetear 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.
Amphas the best-in-class version of this (server-side threads,amp threads continue, shareableampcode.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
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.
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)
SpecStory 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, 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...)withcmd.Stdin/Stdout/Stderr = os.Stdin/Stdout/Stderr(nocreack/pty, no PTY dependency ingo.mod) — and concurrently runs anfsnotifyfile-watcher on the agent's session directory, converting new/changed session files to Markdown as they appear.specstory watch [<agent>]runs only thefsnotifywatcher 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
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). 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)
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
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:
- 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).
- 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
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)
| 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; 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; interactive claude ignores ANTHROPIC_BASE_URL — reliable only with -p (#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)
| 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 is the outlier: 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)
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) / 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)
| 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
- 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 Claude Code's hooks hand back
transcript_path; the rest carry inlineprompt/responsetext 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
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-templatesobservability 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-observabilityPOSTs hook events to a local server in real time. Same vector B. - Built-in OTEL collectors —
claude-code-otel,claude_telemetry/"claudia" (a drop-inclaudewrapper that sets the OTEL env vars), andLangfuseingesting Claude Code's OTLP directly. Same vector C. - Wire-level tracers —
claude-trace(fetch-patch vianode --require, captures system prompt + tool defs + thinking + raw req/resp pairs) andclaude-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
Capture is a layered fallback, selected per agent from the vectors above, in priority order:
- 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.
- 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'sserveAPI for live capture. - 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.
- Wire interception (D) — when the exact model-facing payload or thinking is needed. Optional, passive for most agents; headless-only for interactive Claude Code.
- 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
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 crates/jackin-capsule/src/session.rs):
- Capsule keeps one
DamageGridper session; itsGridSnapshot/GridPatchoutput 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
DamageGridplus a separate inline-scrollback buffer for Codex-style top-anchored scroll regions. The pane renderer already reads cells fromGridSnapshot/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 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.
This fallback shares its substrate and its discipline with the agent runtime status authority (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 source, same "structured first, screen last" ranking, same per-agent visible-chrome knowledge. Where the status authority asks "is the agent working or blocked?", Déjà Vu's fallback asks "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
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
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 — 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). 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
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:
~/.jackin/data/deja-vu/
index.db # SQLite browse/search index
<workspace>/
<instance-or-date>/
<agent>/
<conversation-id>.pb # canonical protobuf blobKeying 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.
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 prunereclaims 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 syncre-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-jsoncapture is the only complete option.
Browse surfaces
Three surfaces over the same archive, in priority order for V1:
- TUI (
jackin deja-vu, console-style). The headline deliverable. Ajackin 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 (W3C Tabs navigation, scrollable blocks viajackin_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.showrenders a conversation to Markdown/JSON;exportwrites it out. - MCP server. Exposes
list/read/searchas 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).
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
Per the 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-forwardedAMP_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
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
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
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
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
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
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. Both opt-in and redaction-aware.
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 memory direction.
- Cross-agent
@threadinjection (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); it does not schedule or drive agents.
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 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
- 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 — the index/DB home Déjà Vu should reuse, and the cross-agent memory direction it complements.
- Terminal observation and automation — owns the PTY/
DamageGridread APIs and opt-in recording the screen-scrape fallback would build on. - Agent runtime status authority — 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 — the five-runtime model whose per-agent extraction surfaces the providers normalize.
- Host bridge — the host-mediated MCP surface the agent-facing read tools plug into.
- jackin' daemon — the future long-running host process that could own periodic/live capture.
- Agent workflow orchestration — future consumer of the captured-conversation read surface.