Skip to content

Concepts

If you’re new, start with the 60-second quickstart — you’ll be running pconv before you read any of this. This page covers the “wait, what’s actually happening” questions you might have after the tool starts returning data.

Before anything else: portaconv never writes to any tool’s storage. Not a byte. There is no daemon, no file watcher, no auto-sync. Every pconv invocation reads the current state of disk and emits output to stdout. Close the process and nothing’s running.

If you ever wonder “is this going to corrupt my Claude state?”, the answer is no, by construction. The worst portaconv can do is crash or print garbage to your terminal.

That’s the foundational invariant. The rest of the system is layered on top with explicit reliability tiers — worth knowing if you’re automating against the tool.

LayerAlways the sameOpt-in (off by default)Could drift
Read pathRead-only; stdio-only output; cache miss always falls back to full scan.—no-cache bypasses cache entirely.Claude Code on-disk format — new record types get captured in extensions.unknown_records, never silently dropped.
NormalizationOpenAI outer + Anthropic inner content-blocks. Subagent filter rules fixed (path + filename match). cwd metadata never rewritten; only content is.—rewrite transforms (wsl-to-win, win-to-wsl, strip).New Claude record types land in unknowns bag until a future adapter bump promotes them.
Dedup & selectionDedup keeps highest message_count (tie: most recent updated_at). —latest picks first after dedup+sort. Byte-reproducible on stable corpus.—show-duplicates skips dedup. —sort / —reverse / —limit.Relative —since 2d is resolved against wall clock at invocation (by design).
OutputSame input → same bytes. Markdown structure is stable. Any truncation is self-documented in the header + extensions.truncated.—tail, —include-thinking, —full-results, —include-system-events. All off by default.
Cache & stateSchema-versioned — bump invalidates the old cache gracefully. Corrupt/missing cache = full walk. Never the source of truth.PORTACONV_CACHE_ROOT env overrides location.Cache is machine-local, not portable. Delete anytime; no data loss.

Lossy by design — don’t expect any of these to survive:

  • Tool-call runtime state. The new agent has no running tools and no live file state. Tool args and results survive; the actual live process does not.
  • Subagent internal reasoning. Filtered out at the adapter layer. The parent session’s tool_use + tool_result already carries the consolidated output.
  • Internal Claude metadata recordsfile-history-snapshot, progress, queue-operation are dropped (they’re not part of the conversation’s substance).
  • Conversation identity across paste. You’re starting a new session in the new tool; the sessionId changes. portaconv preserves the content, not the identity.

If you’re scripting against portaconv, these are safe assumptions:

  • Same JSONL bytes + same flags → same output bytes (within the same pconv version).
  • --format json is a stable contract at the field level; extensions may gain fields but won’t rename or drop core ones.
  • Exit codes: 0 success, non-zero any error, stderr carries the message.
  • --grep never matches message content — only title and cwd. “Full-content search” stays a separate verb if/when it lands.
  • --tail N on a session with fewer than N messages is a no-op (no spurious truncation marker).

Three layers between the JSONL on your disk and the markdown in your clipboard:

Adapter
tool-specific
”We read what your tool saved.”
v0.1 has one adapter — Claude Code. It reads ~/.claude/projects//.jsonl, handles Claude’s two on-disk encoding shapes (WSL + Windows), skips subagent files, and splits out sessions that share a file after /compact.
Shared model
tool-agnostic
”We normalize it to one shape.”
One Conversation type that every future adapter (opencode, Cursor, Aider…) will target. OpenAI Chat Completions on the outside, Anthropic content-blocks inside, so tool calls + thinking survive without flattening.
Renderer
output shape
”We format it for you.”
markdown for humans and agents, json for machines. Path-rewrite transforms (wsl-to-win, win-to-wsl, strip) optionally run between model and renderer. They touch content only — never metadata like cwd.

portaconv is explicit about the weird corners of Claude Code’s storage, so you know what to expect when output looks unexpected.

Subagents — the reasoning loops triggered by a parent session’s tool_use — don’t show up in pconv list by default. They’re transient, and the parent session’s tool_use + tool_result already captures their consolidated output. Two on-disk shapes exist, both filtered:

old shape Claude ≤ 2.0.x

<project-dir>/
  agent-<hash>.jsonl

new shape Claude ≥ 2.1.x

<project-dir>/
  <parent-uuid>/
    subagents/
      agent-*.jsonl

When Claude’s /compact writes a continuation under a new session UUID, it keeps appending to the same on-disk file. portaconv surfaces each distinct sessionId as its own entry in list — matching Claude’s own /resume mental model where the sessionId (not the file) is the identity.

If you’ve launched Claude Code on a project from both WSL and Windows, both encoded directories carry a copy of the same session. By default list dedups them (keeping the entry with the most messages). Pass --show-duplicates if you want to eyeball both copies for manual reconciliation. For dump, portaconv picks the canonical “home” file (the one named <uuid>.jsonl), tie-breaking by size (larger = fuller history).

When the automatic pick isn’t the one you want — say you’re recovering the older Windows-side state, not the newer WSL tail — dump --file <path> forces a specific backing JSONL. Discover paths via list --show-duplicates --format json and the source_path field; see commands reference for the full flow.

Tool-call state doesn’t survive the paste

Section titled “Tool-call state doesn’t survive the paste”

The renderer preserves the intent of a tool call (name + args) and the result body, but when you paste the output into a fresh agent, that agent has no live tools and no running filesystem state. Tools are re-runnable against the current directory — that’s fine for most recovery. If the exact mid-turn state of a tool mattered, paste-recovery is the wrong shape for the job.

The normalized model, for reference:

Conversation {
id,                // session UUID
title?,            // derived from first user message
cwd?,              // launch-time working dir
started_at?,
messages: [
  Message {
    role,          // user | assistant | system | tool
    content: [
      Text { text } |
      ToolUse { id, name, input } |
      ToolResult { tool_use_id, output, is_error } |
      Thinking { text } |
      Unknown { raw }        ← resilience hook
    ],
    timestamp?,
    extensions,    // adapter-specific per-message bits
  }
],
extensions,        // adapter-specific per-conversation bits
                   //   - system_events (dropped from messages)
                   //   - unknown_records (safety net)
}

Each line of a Claude Code JSONL is one record. Types we’ve seen in practice fall into three buckets:

Schema
Normalized into messages.
user
assistant
Extensions
Preserved in extensions, not rendered.

systemsystem_events
permission-mode
attachment
custom-title
agent-name
(unknowns)

Skipped
Dropped — not load-bearing for paste workflows.
file-history-snapshot
progress
queue-operation
last-prompt

Full per-record contract in the adapter notes.