Problem
ExternalSessionAdapter is source-agnostic and Codex uses it today, but there is no adapter for Claude Code. A user with history in ~/.claude/projects/<mangled-cwd>/<uuid>.jsonl has no way to bring it into Maka.
Part of #2499. An earlier attempt is at #2954 (closed); this issue records the problem and what that attempt established, so the next one does not rediscover it.
What the transcript format requires
- One model response is split across several records sharing
message.id, and the records of one response can be separated by a tool_result. Folding only consecutive records imports one answer as several rows — 383 occurrences across 33 of 1236 local transcripts, 378 of them continuations carrying only tool_use.
- Tool results arrive as
type: 'user' records. They are the harness replying, not the human, and must not import as user Turns.
- Sub-agent transcripts are whole
isSidechain files, not interleaved records (verified: 0 mixed across 1231 files), so exclusion is per-file.
- The directory name mangles path separators, so only a record's own
cwd can answer a project-scoped query.
isMeta records are context Claude Code injected on the user's behalf and were never typed by a human.
The load-bearing constraint
This is the part worth knowing before starting.
isTrustworthyRecoveredTerminal (runtime-ledger-repair.ts) discards a reconstructed terminal RuntimeEvent unless a recorded turn_state corroborates it. Every imported Run is then repaired to failed/missing_terminal_event, and the whole conversation renders as errors.
A transcript does not record that a Turn ended — only that another one began. So an importer that wants its history to render has to assert a recorded terminal it never observed. That assertion is wrong in a way that matters: 50 of 376 real root transcripts end with a prompt that has no answer after it (a killed process, a crash, or a Session still running), and each would import as a Turn that finished normally.
The honest shape is two changes that only work together:
- Adapters emit
turn_state only for facts the transcript carries — the interrupt notice and isApiErrorMessage — and let deriveTurnRecords label everything else inferred.
- The Ledger accepts an inferred terminal on the transcript-materialization path, where
transcriptRunHeader derives the header from the same messages, so there is no independent record for the transcript to contradict.
Note that (2) needs both isTrustworthyRecoveredTerminal and terminalStatus in runtime-event-backfill.ts to open — the latter also refuses to emit a terminal event without a turn_state, so relaxing only the first leaves the Run at failed.
Since this touches Runtime terminal-state authority, it may be worth landing as its own change before the adapter, rather than inside it.
Suggested scope
- The adapter, registered alongside Codex.
- Fixtures for both the 2.1 and 2.0 transcript shapes — 2.0 predates
ai-title/last-prompt, never emits thinking blocks, and puts text and tool_use in one record where 2.1 splits them.
- Reuse
claudeUserMessageText and the interrupt literal from @maka/core/foreign-session rather than re-implementing them; the scanner and the importer deciding "what did the user say" differently would be a real bug.
Problem
ExternalSessionAdapteris source-agnostic and Codex uses it today, but there is no adapter for Claude Code. A user with history in~/.claude/projects/<mangled-cwd>/<uuid>.jsonlhas no way to bring it into Maka.Part of #2499. An earlier attempt is at #2954 (closed); this issue records the problem and what that attempt established, so the next one does not rediscover it.
What the transcript format requires
message.id, and the records of one response can be separated by atool_result. Folding only consecutive records imports one answer as several rows — 383 occurrences across 33 of 1236 local transcripts, 378 of them continuations carrying onlytool_use.type: 'user'records. They are the harness replying, not the human, and must not import as user Turns.isSidechainfiles, not interleaved records (verified: 0 mixed across 1231 files), so exclusion is per-file.cwdcan answer a project-scoped query.isMetarecords are context Claude Code injected on the user's behalf and were never typed by a human.The load-bearing constraint
This is the part worth knowing before starting.
isTrustworthyRecoveredTerminal(runtime-ledger-repair.ts) discards a reconstructed terminal RuntimeEvent unless a recordedturn_statecorroborates it. Every imported Run is then repaired tofailed/missing_terminal_event, and the whole conversation renders as errors.A transcript does not record that a Turn ended — only that another one began. So an importer that wants its history to render has to assert a
recordedterminal it never observed. That assertion is wrong in a way that matters: 50 of 376 real root transcripts end with a prompt that has no answer after it (a killed process, a crash, or a Session still running), and each would import as a Turn that finished normally.The honest shape is two changes that only work together:
turn_stateonly for facts the transcript carries — the interrupt notice andisApiErrorMessage— and letderiveTurnRecordslabel everything elseinferred.transcriptRunHeaderderives the header from the same messages, so there is no independent record for the transcript to contradict.Note that (2) needs both
isTrustworthyRecoveredTerminalandterminalStatusinruntime-event-backfill.tsto open — the latter also refuses to emit a terminal event without aturn_state, so relaxing only the first leaves the Run atfailed.Since this touches Runtime terminal-state authority, it may be worth landing as its own change before the adapter, rather than inside it.
Suggested scope
ai-title/last-prompt, never emits thinking blocks, and puts text andtool_usein one record where 2.1 splits them.claudeUserMessageTextand the interrupt literal from@maka/core/foreign-sessionrather than re-implementing them; the scanner and the importer deciding "what did the user say" differently would be a real bug.