Skip to content

Claude Code: discover transcripts under ~/.claude/transcripts/ and skip subagent journal.jsonl orchestration files #208

Description

@willwashburn

Status against main @ 338ce5f (2026-09-21): still open. ~/.claude/transcripts/ is not scanned anywhere (git grep transcripts crates/ai-hist/src only hits Cursor's agent-transcripts), and journal.jsonl has no exclusion. The nested <session>/subagents/**/agent-*.jsonl layout is already covered: claude_subagents (crates/ai-hist/src/ingest/hydrate.rs:2499) collects agent-*.jsonl recursively from the project directory, so that part of the original issue is dropped. Roots now come from ProviderRoots.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"); :1338 fingerprints *.json sidecars under the same root; :1326 registers the watch root roots.claude.join("projects").
  • crates/ai-hist/src/ingest.rs:1319 — full sync calls sync_claude_session_metadata (:5180) on roots.claude.join("projects").
  • crates/ai-hist/src/ingest/hydrate.rs:1873 — hydration path validation accepts only provider_roots.claude.join("projects"), so a session under transcripts/ would be rejected even if discovered.

Proposed change

  1. Make the Claude root a list on ProviderRoots (or a claude_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.
  2. 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/gitBranch are present (project identity from [G1] Canonical project identity (git-remote project_key) on every session and event, for all sources #175 depends on cwd), and whether sessionId values can collide with a projects/ file for the same session. If they can, the raw_path upsert in upsert_session must 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.
  3. 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.jsonl alongside agent-*.jsonl transcripts to record subagent workflow orchestration (spawn/verdict/result events). It shares the .jsonl extension and lives under the recursively-scanned project dir, so the dir-walk discovers it — but it is metadata, NOT a message transcript ... Its lines carry type: "started"/"result" (not user/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 the agent- prefix.
  • Full sync is not: sync_claude_session_metadata walks collect_matching_files(root, "", "jsonl") (ingest.rs:8605, recursive read_dir), so journal.jsonl is stamped in claude_sessions_v3, read on every change by scan_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 no sessionId, 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's session_markers: any future "store unknown record types as unknown markers" path would ingest started/result rows 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 requiring sessionId.

Proposed change

  • Add the is_workflow_journal predicate and apply it in collect_matching_files callers for Claude (discovery enumerate, 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/result name 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, and transcripts/t1.jsonl: sync --local produces sessions s1 and t1, one delegation row for agent-a, no session, event, marker or continuity row derived from journal.jsonl, and claude_sessions_v3 has no entry for it.
  • sessions hydrate claude t1 passes root validation.
  • CLAUDE_CONFIG_DIR=/x relocates 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions