jackin'
RoadmapAgent Orchestrator ResearchFleet phase 2 — Live operator surface

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 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

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 stdioexec.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 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

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).

#VectorWhere it tapsLive / post-hocPassive?Vendor-blessed?
ANative session fileThe agent's own on-disk JSONL/SQLite transcriptPost-hoc (or tail)Yes — file read, zero cooperationFormat usually internal/undocumented
BLifecycle hooksAgent runs a command on events (prompt submit, tool use, stop)LiveNo — agent must support hooks; jackin' installs them at launchYes
CBuilt-in OpenTelemetryAgent's own instrumentation emits OTLP logs/metrics/tracesLiveSemi — env-configured once, then passiveYes
DWire interceptionThe agent↔model HTTP boundary (base-URL redirect, fetch patch, MITM)LiveYes — operator controls env/proxy, agent unawareConfig blessed; capture-via-proxy operational
EHeadless stream-jsonAgent stdout in --print / --output-format stream-json modeLiveNo — you launch the run headlessYes
FMultiplexer screen scrapejackin's own DamageGrid screen + scrollback for the paneLive / post-hocYes — jackin owns the PTYn/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.

VectorUPATTCTRTHTOKDIFFTIME
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

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)

SurfaceYieldsPassive?Stability
Native JSONL ~/.claude/projects/<encoded-cwd>/<id>.jsonl (subagents in …/<id>/subagents/agent-*.jsonl)Full thread: user/assistant/tool_use/tool_result/thinking, uuidparentUuid tree, model, token usage, cwd, gitYesInternal 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 JSONLNo (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 historySemi (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 usageYesOperational; 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)

SurfaceYieldsPassive?Stability
Native rollout JSONL ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonlFull thread auto-saved: SessionMeta, ResponseItem, EventMsg(UserMessage/AgentMessage), TurnContext, Compacted; what codex resume readsYesInternal format (vendor warns it is not a stable interface)
notify callbackJSON on agent-turn-complete: input-messages (user) + last-assistant-message + cwdNoVendor-blessed but coarse (last turn only)
Hooks (requirements.toml, allow_managed_hooks_only)Exists; event list / payload content unverifiedNoVendor-blessed (details unconfirmed)
codex exec --jsonNDJSON: thread.*, turn.*, item.* (agent_message, reasoning, command exec, file changes, MCP tool calls)NoVendor-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_eventSemiVendor-blessed; exec has no metrics, mcp-server emits none
OPENAI_BASE_URL / [model_providers] base_urlFull wire traffic via a proxy (wire_api = responses; Chat-only proxies need translation)YesConfig 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.

SurfaceYieldsPassive?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.comNoVendor-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-jsonNoVendor-blessed (core @sourcegraph/amp is now a CLI alias, not a library)
OTEL / model base-URL proxyNone — Amp brokers models server-side; no model-base-URL override surfacedAbsent

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.

SurfaceYieldsPassive?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)YesInternal format
Hooks ([[hooks]] in config.toml; 13 events)UserPromptSubmit.prompt, PreToolUse/PostToolUse, SubagentStop.response (assistant text), SessionStart/EndNoVendor-blessed (Beta); no transcript_path exposed — cannot pivot to the file from a hook
--print --output-format stream-jsonOpenAI-chat-shaped role stream (user/assistant+tool_calls/tool); no init/result envelopeNoVendor-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)NoVendor-blessed (wire experimental)
Provider base_url (config) proxyFull wire traffic via a proxy (per-provider base_url, no env override)YesConfig blessed; capture operational
OTELNone (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)

SurfaceYieldsPassive?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-*)YesInternal 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 SDKCooperative (run the server) then passive pollingVendor-blessed
SSE GET /eventLive bus: message.updated, message.part.updated (text deltas), session.* — 80+ event typesCooperative (run server)Vendor-blessed
opencode run --format jsonPer-run JSON resultNoVendor-blessed
SDK @opencode-ai/sdk / plugins (tool.execute.before/after)Typed client over HTTP+SSE; plugins observe tool I/ONoVendor-blessed
OTELNone 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 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

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 readersvibe-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-outclaude-code-hooks-multi-agent-observability POSTs hook events to a local server in real time. Same vector B.
  • Built-in OTEL collectorsclaude-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 tracersclaude-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

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

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 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 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 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.

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

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 (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).

Architecture

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

LayerOwnsDoes not own
Per-agent providersLocating, reading, and parsing each agent's chosen extraction vector (native file / stream-json / API / hooks / screen) into the normalized recordThe archive format, the index, or the browse UI
Archive + indexThe canonical protobuf store, the SQLite index, capture timing, render-on-demand to Markdown/JSONHow any single agent encodes its own sessions
Surfaces (TUI / CLI / MCP)Operator and agent access: browse, search, show, exportRe-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-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

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 @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); it does not schedule or drive agents.

Open questions

QuestionCurrent 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.

On this page