Commit graph

2 commits

Author SHA1 Message Date
Abhay Singh
eed80dd4ba
fix(learn/claude): don't abort the whole scan on a null message line (#2299)
## Description

A single Claude session-log line with an explicit `{"message": null}`
crashes the entire `headroom learn` run.

`ClaudeCodePlugin._scan_session` reads the message object in four
places:

```python
usage = d.get("message", {}).get("usage", {})   # assistant line
...
msg = d.get("message", {})                        # _extract_tool_uses
msg = d.get("message", {})                        # _extract_tool_results
msg = d.get("message", {})                        # _extract_user_events
```

`dict.get("message", {})` only substitutes `{}` for a **missing** key. A
present-but-null `{"type": "assistant", "message": null}` yields `None`,
and `None.get(...)` raises `AttributeError`.

The per-file guard only catches I/O errors:

```python
try:
    with open(jsonl_path, ...) as f:
        for line in f:
            ...
except (OSError, UnicodeDecodeError) as e:
    ...
    return None
```

so the `AttributeError` propagates out of `_scan_session`, past
`scan_project` (which has no try/except around the scan), and aborts the
whole `learn` invocation — every project, not just the one bad line. One
malformed line takes down the entire run.

## Fix

Coalesce the message with `or {}` at all four sites, so a null (or any
falsy) value collapses to `{}`:

```python
usage = (d.get("message") or {}).get("usage", {})
msg = d.get("message") or {}
```

The malformed line is now skipped and scanning continues; valid lines
are parsed exactly as before.

Closes #

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `headroom/learn/plugins/claude.py`: coalesce `d.get("message")` with
`or {}` in `_scan_session` and the three `_extract_*` helpers.
- `tests/test_learn/test_subagent_scanning.py`: new test that a session
containing `{"message": null}` lines scans without crashing and still
parses the valid tool call.
- `CHANGELOG.md`: Bug Fixes entry.

## Testing

- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ uvx ruff@0.15.17 check headroom/learn/plugins/claude.py tests/test_learn/test_subagent_scanning.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/learn/plugins/claude.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the per-line handling with a dependency-free script and left
the full pytest to CI.
- Exact command / steps: ran an assistant line `{"message": null}` (plus
a real assistant line and a missing-message line) through the OLD
`get("message", {})` and NEW `get("message") or {}` logic.
- Observed result: OLD raises `AttributeError` on the null message; NEW
returns `0` for it and still counts `42` input tokens for the real line
and `0` for a missing-message line.
- Not tested: a full `learn` run over a real history containing such a
line; full local `pytest` deferred to CI (OOM).

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. The new test reuses the
existing `ClaudeCodePlugin` scanner harness in
`tests/test_learn/test_subagent_scanning.py`, so it runs under the
normal CI pytest job; behaviour is additionally verified by the
standalone proof above.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-16 14:36:11 -07:00
Tejas Chopra
0ddd4ed9e9
fix(learn): scan subagent and workflow transcripts (#1045)
## Description

`headroom learn` only scanned top-level main Claude Code sessions
(`<project>/<uuid>.jsonl`). Nested subagent and workflow transcripts
under `<project>/<uuid>/subagents/**` were not opened, which hid a large
amount of tool-call failure and token-spend activity from failure mining
and downstream analysis.

This change makes the Claude scanner descend into nested transcripts by
default and tag each `SessionData` with its source. `--main-only`
restores the previous top-level-only scan scope.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- Updated `ClaudeCodePlugin.scan_project` to discover nested subagent
and workflow transcripts by default.
- Added source tagging for `main`, `subagent`, and `workflow` sessions.
- Added `--main-only` and `include_subagents` plumbing so callers can
opt back into top-level-only scanning.
- Added the `include_subagents` scanner parameter to Codex/Gemini as a
documented no-op because those scanners use flat session layouts.
- Added regression tests for nested discovery, source tagging, parallel
scanning, and CLI flag threading.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality

### Test Output

```text
Full learn + CLI suite:
# 186 passed, 2 skipped

GitHub Actions CI for this PR:
# build, build-wheel, lint, tests, e2e, CodeQL, and native wrapper checks passed
```

## Real Behavior Proof

- Environment: local Claude Code corpus with nested subagent/workflow
transcripts.
- Exact command / steps: Scanned the corpus with the previous
top-level-only behavior and then with nested transcript discovery
enabled.
- Observed result: The scanner saw 24 sessions before and 306 sessions
after descending into nested transcripts.
- Not tested: Codex/Gemini nested transcript discovery, because those
providers currently use flat session layouts and treat
`include_subagents` as a no-op.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-16 12:20:37 -07:00