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.
What you can rely on
Section titled “What you can rely on”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.
Reliability tiers
Section titled “Reliability tiers”| Layer | Always the same | Opt-in (off by default) | Could drift |
|---|---|---|---|
| Read path | Read-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. |
| Normalization | OpenAI 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 & selection | Dedup 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). |
| Output | Same 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 & state | Schema-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. |
What paste does NOT preserve
Section titled “What paste does NOT preserve”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_resultalready carries the consolidated output. - Internal Claude metadata records —
file-history-snapshot,progress,queue-operationare 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.
Predictability checklist
Section titled “Predictability checklist”If you’re scripting against portaconv, these are safe assumptions:
- Same JSONL bytes + same flags → same output bytes (within the same pconv version).
--format jsonis a stable contract at the field level; extensions may gain fields but won’t rename or drop core ones.- Exit codes:
0success, non-zero any error,stderrcarries the message. --grepnever matches message content — only title and cwd. “Full-content search” stays a separate verb if/when it lands.--tail Non a session with fewer than N messages is a no-op (no spurious truncation marker).
How portaconv sees a conversation
Section titled “How portaconv sees a conversation”Three layers between the JSONL on your disk and the markdown in your clipboard:
~/.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.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.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.Edge cases you’ll hit
Section titled “Edge cases you’ll hit”portaconv is explicit about the weird corners of Claude Code’s storage, so you know what to expect when output looks unexpected.
Subagent sessions are filtered
Section titled “Subagent sessions are filtered”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:
<project-dir>/
agent-<hash>.jsonl
<project-dir>/
<parent-uuid>/
subagents/
agent-*.jsonl
A single JSONL can hold multiple sessions
Section titled “A single JSONL can hold multiple sessions”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.
Same session, multiple files
Section titled “Same session, multiple files”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 Conversation shape
Section titled “The Conversation shape”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)
}How the adapter classifies records
Section titled “How the adapter classifies records”Each line of a Claude Code JSONL is one record. Types we’ve seen in practice fall into three buckets:
messages.userassistantextensions, not rendered.system → system_events
permission-mode
attachment
custom-title
agent-name
(unknowns)
file-history-snapshotprogressqueue-operationlast-promptFull per-record contract in the adapter notes.