Title
Claude Code session provider
Summary
Implement the Claude Code source: discover session JSONL files under the
projects directory with an mtime prefilter, assemble per-session facts
(span, project, counts, title) with the tolerant scanner (13), and emit
agent-session Events per DESIGN.md §7.3 and the research doc.
Context
~/.claude/projects/<encoded-path>/<uuid>.jsonl is an unofficial,
version-drifting format. The research doc
(docs/research/claude-code-session-format.md) is the normative field
contract: cwd (not the lossy directory name) attributes the project;
summary lines beat first-prompt titles; sidechain lines are counted
separately; prompts are opt-in (include_prompts, ADR-004).
Scope
internal/source/claudecode/provider.go, session.go, tests, synthetic
fixtures under testdata/.
Detailed Requirements
New(cfg config.SourcesClaudeCode) *Provider; ID() =
model.SourceClaudeCode.
- Discovery:
<projects_dir>/*/*.jsonl via os.ReadDir two levels (no
deeper recursion); skip files with mtime < rng.Start − 48h
(files still being written have fresh mtimes; a session ending after
rng.End still matters for spanning, so no upper-bound skip). Missing
projects dir → claudecode_dir_missing warning, zero events, nil error.
Symlinked project dirs are not followed.
- Per file, one streaming
jsonlutil.ForEach pass building:
first, last — min/max timestamp over lines with a parseable
timestamp that fall inside rng (per research doc: do not trust
line order); also track any-line-in-range boolean.
- counts (computed over in-range lines only — a midnight-spanning
session reports per-day counts, per the research doc): user lines
with isSidechain != true → UserMsgs; assistant non-sidechain →
AgentMsgs; any user/assistant with isSidechain == true →
SidechainMsgs.
- modal
cwd and modal gitBranch over in-range user/assistant lines.
- title candidates: latest
summary-type line's summary string
(regardless of range — a summary describes the whole session); first
in-range user line's text when include_prompts (content as a plain
string, OR the concatenation of all text-type blocks when content is
a block array — per the research doc; then take the first line and cap
at 120 runes).
version (last seen), sessionId (any line; fallback filename stem).
- Early stop: none (need min/max over whole file) — but skip files fast
when the first 50 parseable timestamps are all > rng.End AND mtime
indicates no rewrite risk? NO — keep it simple and correct: full scan
per file that passes the mtime prefilter (documented decision).
- Emit one Event per session with ≥ 1 in-range user/assistant line:
Kind KindAgentSession, Start=first, End=last, Project = modal cwd
(raw path; normalization happens in 18), Ref=sessionId,
Title = summary → prompt (opt-in) → claude-code session <first 8 of id>,
Meta: agent="claude-code", user_messages, assistant_messages,
sidechain_messages, git_branch, cli_version (omit empty values).
- Per-file malformed handling:
MalformedRatio > 0.20 && Lines ≥ 10 →
claudecode_malformed_lines warning naming the file (basename only —
full paths of other projects would leak into shared reports via the
warnings footer; DESIGN §13 messages must use basenames). Unreadable
file (permissions) → claudecode_file_skipped warning, continue.
ctx honored between files and via ForEach.
Acceptance Criteria
Validation
go test -race -cover ./internal/source/claudecode/ in PR.
Dependencies
04, 08, 13.
Non-goals
~/.claude/history.jsonl (v2 candidate), automation-noise filtering beyond
sidechain separation (known unknown U2-adjacent), project-name
normalization (18).
Design References
docs/research/claude-code-session-format.md (normative)
docs/DESIGN.md §7.3, §13
docs/decisions/ADR-004-redaction-default-on.md (prompts opt-in)
Source of truth: docs/issues/14-claude-code-provider.md (PR #1, branch docs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies: docs/ISSUE_PLAN.md (this is issue 14 of 33).
Title
Claude Code session provider
Summary
Implement the Claude Code source: discover session JSONL files under the
projects directory with an mtime prefilter, assemble per-session facts
(span, project, counts, title) with the tolerant scanner (13), and emit
agent-sessionEvents per DESIGN.md §7.3 and the research doc.Context
~/.claude/projects/<encoded-path>/<uuid>.jsonlis an unofficial,version-drifting format. The research doc
(
docs/research/claude-code-session-format.md) is the normative fieldcontract:
cwd(not the lossy directory name) attributes the project;summarylines beat first-prompt titles; sidechain lines are countedseparately; prompts are opt-in (
include_prompts, ADR-004).Scope
internal/source/claudecode/provider.go,session.go, tests, syntheticfixtures under
testdata/.Detailed Requirements
New(cfg config.SourcesClaudeCode) *Provider;ID()=model.SourceClaudeCode.<projects_dir>/*/*.jsonlviaos.ReadDirtwo levels (nodeeper recursion); skip files with mtime <
rng.Start − 48h(files still being written have fresh mtimes; a session ending after
rng.Endstill matters for spanning, so no upper-bound skip). Missingprojects dir →
claudecode_dir_missingwarning, zero events, nil error.Symlinked project dirs are not followed.
jsonlutil.ForEachpass building:first,last— min/maxtimestampover lines with a parseabletimestamp that fall inside
rng(per research doc: do not trustline order); also track any-line-in-range boolean.
session reports per-day counts, per the research doc):
userlineswith
isSidechain != true→UserMsgs;assistantnon-sidechain →AgentMsgs; any user/assistant withisSidechain == true→SidechainMsgs.cwdand modalgitBranchover in-range user/assistant lines.summary-type line'ssummarystring(regardless of range — a summary describes the whole session); first
in-range user line's text when
include_prompts(content as a plainstring, OR the concatenation of all
text-type blocks when content isa block array — per the research doc; then take the first line and cap
at 120 runes).
version(last seen), sessionId (any line; fallback filename stem).when the first 50 parseable timestamps are all >
rng.EndAND mtimeindicates no rewrite risk? NO — keep it simple and correct: full scan
per file that passes the mtime prefilter (documented decision).
Kind
KindAgentSession, Start=first, End=last, Project = modal cwd(raw path; normalization happens in 18), Ref=sessionId,
Title = summary → prompt (opt-in) →
claude-code session <first 8 of id>,Meta:
agent="claude-code",user_messages,assistant_messages,sidechain_messages,git_branch,cli_version(omit empty values).MalformedRatio > 0.20 && Lines ≥ 10→claudecode_malformed_lineswarning naming the file (basename only —full paths of other projects would leak into shared reports via the
warnings footer; DESIGN §13 messages must use basenames). Unreadable
file (permissions) →
claudecode_file_skippedwarning, continue.ctxhonored between files and via ForEach.Acceptance Criteria
one Event with exact counts, summary-based Title, modal cwd Project.
include_prompts=false(default): no prompt text anywhere in Event(Title falls back when no summary);
=true: first-prompt Title,first line only, ≤ 120 runes.
message.contentas array-of-blocks fixture parses (text blocksconcatenated).
Start/End clamp to in-range min/max per day AND the per-day message
counts differ according to which lines fall in each day (two Collect
calls asserted, counts hand-computed in the test).
fixture with unreadable permissions that would warn if opened — no
warning appears when mtime is old).
still emitted from good lines.
ghp_…token in title text passesthrough raw here (sanitize/redact are 18's job) — asserted so nobody
"helpfully" sanitizes in the parser and breaks layering.
queue-operation,last-prompt, an inventedtype) silently ignored.Validation
go test -race -cover ./internal/source/claudecode/in PR.Dependencies
04, 08, 13.
Non-goals
~/.claude/history.jsonl(v2 candidate), automation-noise filtering beyondsidechain separation (known unknown U2-adjacent), project-name
normalization (18).
Design References
docs/research/claude-code-session-format.md(normative)docs/DESIGN.md§7.3, §13docs/decisions/ADR-004-redaction-default-on.md(prompts opt-in)Source of truth:
docs/issues/14-claude-code-provider.md(PR #1, branchdocs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies:docs/ISSUE_PLAN.md(this is issue 14 of 33).