Repository navigation
Claude Code: discover transcripts under ~/.claude/transcripts/ and skip subagent journal.jsonl orchestration files #208
Description
Status against
main@338ce5f(2026-09-21): still open.~/.claude/transcripts/is not scanned anywhere (git grep transcripts crates/ai-hist/srconly hits Cursor'sagent-transcripts), andjournal.jsonlhas no exclusion. The nested<session>/subagents/**/agent-*.jsonllayout is already covered:claude_subagents(crates/ai-hist/src/ingest/hydrate.rs:2499) collectsagent-*.jsonlrecursively from the project directory, so that part of the original issue is dropped. Roots now come fromProviderRoots.claude(crates/ai-hist/src/paths.rs:14-16, #207 / PR #216), so the second root should hang off that.
Summary
relayhistory only scans <claude root>/projects/**/*.jsonl for Claude Code. tokscale (commit d8fd670) scans a second root, <claude root>/transcripts/, and explicitly excludes a metadata file (journal.jsonl) that shares the .jsonl extension and lives inside the recursively walked project tree. Both apply here.
1. <claude root>/transcripts/
tokscale's README lists Claude Code data sources as:
~/.claude/projects/and~/.claude/transcripts/
and crates/tokscale-core/src/scanner.rs:755-781 registers <resolved claude root>/transcripts as a built-in extra scan root for the same Claude JSONL parser, derived from the CLAUDE_CONFIG_DIR-aware root so it moves with it. A comment in sessions/claudecode.rs:452-458 says what lands there:
Bare transcripts under
~/.claude/transcripts/have the same hazard when a third-party client (e.g. OpenCode) also logs the turn.
i.e. Claude Code transcripts written when Claude Code runs embedded in another client (SDK / ACP style) rather than as the interactive CLI. tokscale reuses its projects/ parser unchanged, which implies the same record shape (type, uuid, parentUuid, sessionId, message.{role,content,model,usage}, isSidechain, ...).
Where relayhistory reads Claude today (main)
crates/ai-hist/src/discover.rs:1351—enumerate:collect_matching_files(&env.claude_config_dir.join("projects"), "", "jsonl");:1338fingerprints*.jsonsidecars under the same root;:1326registers the watch rootroots.claude.join("projects").crates/ai-hist/src/ingest.rs:1319— full sync callssync_claude_session_metadata(:5180) onroots.claude.join("projects").crates/ai-hist/src/ingest/hydrate.rs:1873— hydration path validation accepts onlyprovider_roots.claude.join("projects"), so a session undertranscripts/would be rejected even if discovered.
Proposed change
- Make the Claude root a list on
ProviderRoots(or aclaude_transcript_roots()helper):[claude/projects, claude/transcripts]. Use it at the three sites above and in the watch roots (discover.rs:1326) so [G1] Live capture: fs-event watch with debounce + polling fallback, stat-only change fingerprint, and a Claude hook fast path (--hook claude) #174's live capture follows it too. - Characterize before enabling on a machine that has the directory ([G1] Checked-in harness fixture corpus and parser characterization tests (port burn's 40 fixtures) #163 fixture corpus): record shape, whether
cwd/gitBranchare present (project identity from [G1] Canonical project identity (git-remote project_key) on every session and event, for all sources #175 depends oncwd), and whethersessionIdvalues can collide with aprojects/file for the same session. If they can, theraw_pathupsert inupsert_sessionmust pick one deterministically rather than flip-flopping between syncs, and continuity evidence (continuity.rs, keyed by locator) must not record the two files as a fork of each other. docs/session-catalog.md"How each adapter works → claude": add the second root and its capability caveats.
2. subagents/**/journal.jsonl must never be read as a transcript
From sessions/claudecode.rs:130-153:
Claude Code writes a
journal.jsonlalongsideagent-*.jsonltranscripts to record subagent workflow orchestration (spawn/verdict/result events). It shares the.jsonlextension and lives under the recursively-scanned project dir, so the dir-walk discovers it — but it is metadata, NOT a message transcript ... Its lines carrytype: "started"/"result"(notuser/assistant)
fn is_workflow_journal(path: &Path) -> bool {
if path.file_name().and_then(|n| n.to_str()) != Some("journal.jsonl") { return false; }
path.ancestors().any(|a| a.file_name().and_then(|n| n.to_str()) == Some("subagents"))
}relayhistory today
- Hydration is safe:
claude_subagents(hydrate.rs:2499) filters on theagent-prefix. - Full sync is not:
sync_claude_session_metadatawalkscollect_matching_files(root, "", "jsonl")(ingest.rs:8605, recursiveread_dir), sojournal.jsonlis stamped inclaude_sessions_v3, read on every change byscan_claude_session_file_resumed(ingest.rs:6087), and handed to the continuity scan, which "writes a row for every readable file" (continuity.rs:395-400). Today it yields no session because it has nosessionId, so the cost is I/O and a stamp entry. The risk is [G1] Model currently-dropped record types: compaction/summary boundaries, system rows, non-text content blocks, Codex lifecycle events #165'ssession_markers: any future "store unknown record types asunknownmarkers" path would ingeststarted/resultrows unless the file is excluded by name first. - Discovery (
discover.rs:1351) has the same walk and would list a candidate for it if a future shallow read stops requiringsessionId.
Proposed change
- Add the
is_workflow_journalpredicate and apply it incollect_matching_filescallers for Claude (discoveryenumerate, sync, and the continuity capture) so the file is never opened as a transcript. - Optionally read the journal as explicit delegation evidence for
session_relationships(spawn/verdict/resultname the child agent and can carry a spawn time), through a dedicated reader, never through the transcript parser.
Acceptance
- Fixture with
projects/app/s1.jsonl,projects/app/s1/subagents/journal.jsonl,projects/app/s1/subagents/agent-a.jsonl, andtranscripts/t1.jsonl:sync --localproduces sessionss1andt1, one delegation row foragent-a, no session, event, marker or continuity row derived fromjournal.jsonl, andclaude_sessions_v3has no entry for it. sessions hydrate claude t1passes root validation.CLAUDE_CONFIG_DIR=/xrelocates both roots (extends the Honor CLAUDE_CONFIG_DIR and CODEX_HOME when locating Claude Code and Codex session roots #207 end-to-end test).
Related
- Honor CLAUDE_CONFIG_DIR and CODEX_HOME when locating Claude Code and Codex session roots #207 (closed, env roots), [G1] Checked-in harness fixture corpus and parser characterization tests (port burn's 40 fixtures) #163 (fixture corpus), [G1] Model currently-dropped record types: compaction/summary boundaries, system rows, non-text content blocks, Codex lifecycle events #165 (markers), [G1] Live capture: fs-event watch with debounce + polling fallback, stat-only change fingerprint, and a Claude hook fast path (
--hook claude) #174 (watch roots), [G1] One-entry harness registry and out-of-tree local source plugins (absorb burn's collector backlog) #177 (registry: roots become a list on the descriptor).
Activity
fix(claude): never read subagent workflow journals as transcripts
docs(claude): record why ~/.claude/transcripts/ is not a Claude root
test(claude): pin that sync and hydration never read transcripts/