lazy documentationv0.22

Importing Claude Code conversations

Lazy stores conversations — the reasoning, exploration, and decisions from interactive Claude Code sessions — as persisted development context alongside task data. Sessions for a lazy project are captured live (see below). This page covers importing other Claude Code history into the store: sessions that predate lazy, or that recovery left behind.

Adopting lazy on a repo that already has Claude Code history should mean inheriting that history as builder memory — not starting from scratch.

Live capture: two paths, both automatic

Nothing needs importing for sessions that happen while lazy is running — they are captured as they go:

Capture never shortens a conversation

The same session exists in several projects dirs, and a stale copy can be touched — mounted, refreshed — without gaining a byte. Capture used to re-parse whatever it found and blind-write it, so a stale copy could replace a stored conversation with an earlier snapshot of itself; the user then could not read their newest turns via lazy view at all. A capture whose messages are a strict prefix of what is already stored (same message uuids, position for position, fewer of them) is now refused, and the refusal is logged. This is deliberately narrow: a conversation that genuinely diverged is still stored, because there the on-disk copy is the truth.

What is deliberately NOT captured

Lazy runs its own machine-generated claude -p one-shots — the PR/commit fidelity summaries on every accept, lazy report, and LLM memory compaction. These are housekeeping, not conversations, and they were ~83% of the store, drowning real builder conversations in lazy builder list and search. They are now never captured, by either path.

They are identified by a marker that runClaudeOneshot prepends to the prompt, so the session JSONL carries it durably (see src/import/machine-oneshot.ts). Detection is structural — marked at the source, not sniffed from prompt wording — so a real conversation that merely discusses the marker is still captured.

The end-of-pairing session summary (lazy pair) is marked the same way, even though it goes through runClaude rather than runClaudeOneshot — its output already lands on the task as a turn, so capturing the session too is noise.

Deliberately-skipped one-shots never count as uncaptured, so the capture-rot check below stays honest instead of going red after every accept.

One-shots are also excluded from session ownership

Skipping capture is only half of it. runClaudeOneshot inherits the daemon's cwd, so a one-shot's JSONL lands in the very projects dir that every "which session was this launch running?" rule scans — and, being brand new, it is by construction the newest file created since launch. That is the whole of pickLaunchSessionId and rule 1 of pickActiveSessionFile.

The consequence, if the ownership rules do not filter: a fidelity summary fired by an accept made during a builder or lazy pair session wins "newest session this launch owns". Its id is stamped as the resume target and printed as lazy builder --resume <id>, so the next builder opens inside the housekeeping conversation — the human's terminal shows the fidelity prompt as a user turn and answers it. That is a real incident, not a hypothetical.

So every ownership and capture path filters with the same head-anchored predicate that discovery uses (excludeMachineOneshots in src/import/machine-oneshot.ts):

Discovery itself stays honest — it reports what is on disk; the ownership paths filter. Unreadable or unmarked files are kept (treated as real sessions), which is the safe direction: a redundant capture costs nothing, handing a human's resume target to housekeeping costs the session.

Purging housekeeping conversations captured before the marker

Conversations stored before the marker shipped carry no marker, so nothing removes them automatically. lazy doctor --purge-housekeeping-conversations is the one-time cleanup for them:

lazy doctor --purge-housekeeping-conversations          # lists, deletes nothing
lazy doctor --purge-housekeeping-conversations --yes    # deletes

Without --yes it prints every conversation it classified — short session ID, start time, which kind of one-shot it is, and why it was classified — and deletes nothing. On a TTY it then asks for confirmation, defaulting to no; a non-TTY is told to re-run with --yes. Deletion is not recoverable from lazy (Claude Code prunes the raw JSONL on disk over time), so read the list.

This is the only place in lazy that classifies a conversation by sniffing prompt wording (src/import/housekeeping-conversation.ts). That brittleness is acceptable here precisely because the command is explicit, human-reviewed, and one-time, where at capture time — running forever, on every sweep tick — it would not be. Every rule is anchored at the very start of the conversation's single user message, so a real conversation that quotes a lazy prompt is never matched.

A caveat worth knowing: because these conversations have no marker on disk, lazy doctor --reimport-conversations will re-import them if their raw JSONL is still present. Purge is about cleaning up the store, not about rewriting what is on disk.

If the daemon is not running, the second path is not running either — the sessions stay on disk until a daemon is up or you import them manually. lazy doctor fails (not warns) when it finds sessions from the last 24 hours that never reached the store, because that means live capture is broken now:

✗ Conversation capture is live
  3 conversation(s) written in the last 24h (most recent 41m ago) are on disk
  but never reached the store — live capture is not running.

Sessions written in the last few minutes are treated as in flight, not as failures — the sweep may simply not have ticked yet.

One surface: lazy import-conversation

lazy import-conversation is the single surface for bringing Claude Code history into the store, per-session or in bulk.

lazy import-conversation              # Preview + import all new sessions
lazy import-conversation --yes        # Import all new sessions, no prompt
lazy import-conversation --list       # List available sessions
lazy import-conversation bc77e1b1     # Import one session (id can be shortened)
lazy import-conversation --all        # Re-import everything (incl. already imported)
lazy import-conversation --show-imported     # Show already-imported conversations
lazy import-conversation --show bc77e1b1     # Show a full conversation transcript

Where it looks (multi-root discovery)

Discovery spans every Claude projects dir for this repo:

When the same session appears in several dirs (seeding copies a session into each isolation dir), the most complete copy is used (largest size, newest mtime). This means a session that only ever lived in an isolation dir is still importable — both in bulk and by session-id.

Not to be confused with lazy builder --resume <id> --import, which is a different operation entirely: it decides which projects dir a builder launch mounts (adopting a session that has never run under builder isolation), and writes nothing to lazy's store. See "One /resume list per project" in lazy-agent design. Importing a conversation here never affects which sessions /resume lists, and adopting a session there never imports it.

Bulk import never writes silently

Running lazy import-conversation with no session-id previews what would be imported and asks for confirmation before writing:

Found 5 session(s) on disk; 3 missing from the store, 2 already imported.
Import 3 conversation(s) into the store?

Pass --yes to skip the prompt (for non-interactive callers). On a non-TTY without --yes, it prints what it would do and writes nothing. Naming an explicit session-id, or passing --all, imports directly — the argument/flag is itself explicit intent.

Import is idempotent: sessions already in the store are skipped, and empty or unparseable JSONL shells are skipped rather than stored as content-free stubs. Lazy's machine-generated one-shots are excluded from discovery entirely, so no import path can resurrect them; when any are present the count is reported (ignored 2 machine-generated lazy one-shot(s)) rather than passed over in silence, and naming one explicitly says so instead of "session not found".

Recovery is the same flow

lazy doctor --reimport-conversations is an alias for the bulk path of lazy import-conversation — same multi-root discovery, dedupe, preview, and confirmation. It exists as a recovery entry point for stranded builder conversations (the fallout of a capture bug that left conversations on disk but out of the store). The lazy doctor health sweep also detects conversations on disk but not in the store — a warning for old ones, a failure for recent ones (see "Live capture" above) — and points at this command; detection is report-only and never writes.

lazy doctor --reimport-conversations          # Preview, then confirm
lazy doctor --reimport-conversations --yes    # Recover without prompting

Using what is stored: lazy ask <conversation-id>

Claude Code's own retention ages old sessions out of /resume; lazy's store keeps them. Reading one back has always worked (lazy show <session-id>, lazy builder list, lazy search --conversations) — lazy ask is the verb for asking one a question instead of reading the whole thing:

lazy builder list                                   # find the session id
lazy ask 4f8c2a1b -m "what did we decide about retention?"
lazy ask 4f8c2a1b                                   # no -m: opens $EDITOR
lazy ask 4f8c2a1b -m "..." --json                   # structured answer

lazy ask takes either kind of id: a task id asks that task's live agent (see lazy ask --help), a conversation session id — full, or any unique prefix — asks the stored transcript. An ambiguous prefix is an error, never a silent pick.

Nothing is written back. A conversation is immutable history and an ask is a read of it: no turn, no comment, and the ask's own claude -p session is marked as a machine one-shot so it is never captured as a new conversation. The answer goes to stdout and that is all. The agent is locked down to match — it gets no Bash, Write or Edit, so it can only read the transcript it was handed.

Agents and the builder get the same thing as lazy_conversation_ask (session_id + question). Prefer it over lazy_conversation_read when you want one fact rather than the whole transcript — a long conversation read in full can overflow the caller's own context.

Transcripts too large for one pass

The prompt is passed as a single claude -p argument, and one argv element is capped at 128 KiB on Linux — that, not the context window, is the binding limit. A transcript over the 96 KiB budget is therefore split at message boundaries into consecutive excerpts: each is read for what bears on the question, and a final pass writes one answer from those findings. --json reports chunks and relevantChunks so you can see it happened.

Every degradation is reported rather than absorbed. An excerpt that fails to read, a single message too large to pass whole, findings that did not fit in the final pass — each comes back as a warning (on stderr, or in warnings under --json). If every excerpt fails, the ask fails; you never get a confident answer built from silently-dropped input.

Onboarding: lazy init offers to inherit history

When you run lazy init on a repo that already has Claude Code history, init detects it and offers to inherit it. This is deliberately one step, not two disjoint prompt blocks — adopting lazy on an existing repo should feel like a single "inherit your history?" question, covering both kinds of history the harness leaves behind:

This repo already has Claude Code history from before lazy.
  Found 4 existing Claude Code session(s) for this repo.
  Found 3 Claude Code harness memory record(s) with no lazy counterpart.
  Lazy can import both, so you inherit your project's history instead of
  starting from scratch.
  Import 4 conversation(s) as builder memory? [Y/n]
  Import 3 memory record(s) into lazy shared memory? [Y/n]

Each half of the offer appears only when there is something to import, so a repo with sessions but no harness memory sees exactly one prompt.

Detection stays cheap for both: the per-builder isolation dirs don't exist yet at init, so it's a single scan of ~/.claude/projects for dirs matching this repo, plus a readdir of each match's memory/ subdir. The memory count is filtered against the store, so re-running init never re-offers records lazy already holds.

Both offers are prompts — skipped under --non-interactive, never a silent write. Detection and import are best-effort: a hiccup in either prints a (Skipped Claude Code history import: …) note and never fails init. If you decline, you can import later with lazy import-conversation and lazy doctor --import-memory respectively.