Metadata-Version: 2.5
Name: chsum
Version: 2.0.0
Summary: Work logs and reload-ready context from Claude Code conversations. Deterministic: no model, nothing invented.
Author: Joshua
License: MIT
Keywords: claude,claude-code,context,transcripts,work-log
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# chsum

Work logs and reload-ready context from your Claude Code conversations.

**Digests and listings are never generated by a model.** Every line of that output
is either copied verbatim from a transcript or computed from it, so nothing can be
invented. That matters because the output is designed to be pasted back into a
future Claude session, where a plausible-but-wrong sentence would become ground
truth. The one exception is labelled where it appears: `chsum recap` ends in
a timeline written by a model, printed beneath the verbatim record it was written
from.

## The idea

Your own prompts already are a faithful record of what you were trying to do.
Extracted in order they read as the story of the session — most of what a summary
would have said, without the risk:

```markdown
**m1**
> can you open a chrome page to the site, it is running on 3000

**m7**
> sherpa-onnx-tts.worker.js:267 [Sherpa Worker] Initialization failed…

**m137**
> when I speed up the text to speech, it ends up sounding like a chipmunk

**m153**
> The toolbar is no longer working to slow it down or speed it up live
```

Blockquoting is functional, not cosmetic: a quoted reply containing `## Summary`
would otherwise forge a section of the digest. Everything else — dates, duration,
branch, files, commands — is parsed straight out of the transcript.

## Requirements

- [`claude-history`](https://github.com/) on your `PATH` — for `chsum find`'s
  `--semantic` and `--hybrid` search only. Every other command reads the
  transcripts directly and spends no subprocess.
- Python 3.10+. No third-party packages, no model, no network.

## Install

```sh
pipx install chsum            # from a checkout: pipx install .
```

Or as a Claude Code plugin, which brings the skill with it:

```
/plugin marketplace add InDate/indate-tools
/plugin install chsum@indate-tools
```

The plugin carries the skill; the `chsum` command still comes from pipx.

pipx, not `pip install --user`: chsum is an application, so it gets its own venv
and one symlink on `PATH`. `pipx install --editable .` while working on it.

A real command rather than a shell alias, because an alias doesn't exist for
scripts, hooks, or agents.

### Marking while an agent is working

`!` is not available while you are addressing an agent — what you type goes to the
agent as a message, so there is no way to run the command yourself until you are
back in the conversation. Two things do work:

- Mark it afterwards, from the session: `chsum mark --match "<phrase the agent
  said>"` searches the sidecars too, so the agent's own words are addressable.
- Ask the agent to mark it as it goes. That is a Bash call, which prompts, and a
  subagent has nobody watching to approve — so allow the command first, in
  `~/.claude/settings.json` or a project's `.claude/settings.local.json`:

```json
{ "permissions": { "allow": ["Bash(chsum:*)"] } }
```

The whole command, not just `mark`: everything chsum does is read transcripts you
already have, and `digest`'s file lands in `~/.chsum/digests/`. Narrow it
to `Bash(chsum mark:*)` if you'd rather approve the reading commands case by case.

The plugin can't set this for you — permissions come from settings files, and a
plugin that allowlisted its own shell command would be granting itself something
you never agreed to.

## Usage

```sh
chsum                                          # the five most recent in this project
chsum -n 25                                    # more of them; 0 for all
chsum --since 7d                               # only the last week
chsum --all                                    # across every project
chsum recap                                    # this session, since the last recap
chsum recap --full                             # this session, all of it
chsum recap --last                             # the most recent session that isn't this one
chsum context --last                           # that session verbatim, no timeline
chsum context --last -n 2                      # the one before that
chsum find "text to speech playback speed"     # locate a conversation
chsum digest <ch_ref>                          # write a digest file
chsum digest <ch_ref> --stdout                 # print it instead
chsum digest --file path/to/session.jsonl      # address by file
chsum digest <ch_ref> --commands               # every Bash call in order, unfiltered
chsum digest <ch_ref> --command <id>           # one of them whole, with its output
chsum context <ch_ref>                         # reload artifact, for pasting into Claude
chsum context <ch_ref>/<agent-id>              # one subagent's own digest
chsum journal --since 7d                       # work log for this project
chsum journal --since 2w --all                 # across every project
! chsum mark "this is the approach that worked"  # flag the moment as notable
! chsum mark --recent 20                       # list recent messages, with ids
! chsum mark --at 47dca7e9 "where it turned"   # mark an earlier message
chsum find --marks                             # everything you've marked
chsum name "what it actually was"              # rename the session you're in
chsum name <ch_ref> "marks: design + build"    # rename a past one
chsum name --list                              # everything you've renamed
```

Bare `chsum` lists the project's sessions, newest activity first:

```
Thu 06 Aug 2026                        dur    prompts  files  agents  marks
  ch_c120431a267b202aebf0b38f6c3c1b69  5h38m  78       14     -       ⚑2
    ↳ Plan 3D house model from floor plan photographs

Wed 05 Aug 2026
  ch_da4e99d42e5efab11ebdedc22fb65145  3h03m  30       12     2       -
    ↳ Set up cdp-tools server
      a43c4ff4401ca693e  Weed noise from the test suite
      a81d77b6cba4a46b3  Fix standby setpoint tracking
  ch_b99f11b7c257dafc8b93f53480ba3804  6s     1        0      -       -
    ↳ (untitled)
```

Listing is the default because picking is the common case, and "most recent" is
often a session you abandoned after one prompt. Dead ends are listed, not hidden
— that a session went nowhere is the answer to "where did that work go". The
header counts them: *no activity* is no file, no notable command, no agent, and
one prompt.

Subagents are named, not just counted, because "3 agents" says nothing about a
session that delegated its work — and the id is the one `chsum context
<ref>/<id>` takes. Five deep, then a count. Five sessions too, by default: the
listing is usually read into a context window, and `-n 0 --all` is the whole
corpus.

`chsum context --last` is `chsum context` on the most recent session with activity, ordered
by last activity so one you resumed yesterday beats one you started last week.
Run from inside Claude Code, the session doing the running is excluded.

`chsum recap` with no arguments is the opposite case: the session that's excluded above,
caught mid-run. Run it from a second terminal while Claude works. It anchors on
the last thing you typed, then shows everything since — files changed, commands
run, agents at work, and what went wrong — each verbatim or computed, same as
the rest of chsum. Failures get their own section, quoted from the actual error
text, because that is usually what you came back to find out. It
ends in the one section chsum prints that isn't: a timeline written by `claude -p
--model haiku` from a verbatim extract of those events, printed beneath the
verbatim record and labelled model-written, so a wrong sentence can be checked
against what sits above it. Like the verbatim sections, it's a snapshot — the
transcript trails the live screen, so it can lag what's actually on screen.

Run from that second terminal, there's no env var to say which session is
running, so it lists the project's recent sessions and asks — newest at the
bottom, next to the prompt, and Enter takes that one. Inside the session itself,
or piped, there's no prompt to show: it just picks.

The timeline is written by several small, independent `claude -p` calls, not
one big one — the events are split into chunks (at your own turn boundaries
for `recap`, by size alone here) and each chunk gets its own call, run in
parallel, none of them seeing another's material or output. `--dry-run` makes
no model call: you get the verbatim record as usual, then a breakdown of what
each chunk would cost, by where its tokens come from (edits, command output,
what Claude said) and the largest single events. Character counts are
measured off the exact text that would be piped in; the token figures are
chars/4, hence the `~`. It also states the fixed cost of the call itself —
`claude -p` sends its own system prompt and tool definitions unless told not
to, so chsum strips them (`--tools ""`, `--setting-sources ""`) and supplies
its own short instructions via `--system-prompt`, which brings that fixed
cost down to ~158 tokens *per call*, counted once for every chunk that
actually runs.

```sh
chsum recap --dry-run                          # what would this cost, and why
chsum recap --invalidate                       # summarise this window again, replacing what's stored
```

After a real run, the terminal gets one line of what it actually cost, summed
across every chunk call — total input, cache reads and cache writes
separately, output, elapsed, and how the estimate compared. Measured from the
calls' own accounting, not guessed. It goes to stderr like the rest of the
progress chatter, so a piped document is untouched:

```
haiku: 5,974 in (5,042 cache read) · 4,866 out · 56.0s · extract estimated ~3,211, harness ~2,763
```

Everything scopes to the current project; `--all` widens. Digests land in
`~/.chsum/digests/<uuid>.md` (`--out` to change).

### Recap

`chsum recap` is the same document over a range you choose. At a terminal it's
an arrow-key picker: choose a session, press enter, then move to where you want
to start and press enter — everything from there to the cursor highlights as you
move down. A second enter sets the end and shows what the call will cost, and a
third runs it. Escape steps back; escape at the session list exits. Your turns
are shown in full, never truncated, with what happened after each one on its own
line:

```
 33  19:25  can we add all the activity between each of my replies - 3 tools,
            15 edits, 3 agents. and can we make it interactive, so I can push
            arrows up and down to select a session
            ↳ 3 edits · 6 cmds · 2 tools
 34  19:28  Hmm, finsih what you were doing first
            ↳ 5 edits · 8 cmds · 1 failure
```

Piping still works — the wizard draws on the terminal while the document goes to
stdout. `--no-tui` uses typed prompts instead:

```sh
chsum recap                                    # pick session, then start and end
chsum recap --from 3 --to 10                   # same window, no prompts
chsum recap ch_3654a13c --from 3 --to 10       # a specific session
chsum recap --from 3 --to 10 --dry-run         # what it would cost, no model call
chsum recap --from 3 --to 10 --no-cache        # call for every chunk, store nothing
```

Each turn's bullets are kept, so recapping a window twice costs nothing the
second time. The breakdown lands in `~/.chsum/turns/<project>/<turn-uuid>.json`
once that turn's gap has closed — a later turn bounds it, or its last reply
carries a `stop_reason` that ends the exchange. Measured on one 3-turn window:
30.1s and two model calls, then 0.4s and none, the same document both times.

Existing is not enough to reuse. A stored breakdown is read back only when it
names the same instructions, the same model, and the same events it was written
from; change any of the three and the turn is summarised again, with the old
breakdown kept beside the new one. `--dry-run` prices only what is missing, so
`0 calls would be made` means the window is already on disk.

A turn is something you typed *or* an answer you gave the question tool — those
are marked `?` in the list, because a decision made by menu choice is still a
turn that steered the session.

The per-turn files-touched bullets come from one of two sources, and the recap
says which, counted:

```
## Files touched — 4 of 6 turns from a checkpoint

*Files for the other 2 turns come from the transcript scan, which sees
`Edit`/`Write`/`MultiEdit` only and carries each edit's line range as recorded
at the time.*
```

A git checkpoint sees every change however it was made and its line ranges are
current; the transcript scan sees neither. The counts are measured — `0 of 6`
says no checkpoint covered any turn, and does not say why, because a hook that
never ran and a reflog entry that aged out look the same from here.

A recap spans several of your turns, so its timeline is sliced onto them: each
turn quoted verbatim, and beneath it the bullets covering what happened before
you spoke again.

```
### You said (21:43)

> ok

- **21:45** Created a new `is_typed_prompt()` helper that filters out
  `<bash-…>` records, and updated five call sites to use it.
- **21:47** Tested the fix against the live airtouch session; chsum now
  reports `prompts: 0` for the dead 2-second session and skips it.

### You said (21:50)

> commit the work
```

Placement isn't guessed here: each turn's events are their own chunk (or
chunks, if the gap was large), summarised by a call that sees only that
turn's own material, so its bullets slot straight under the turn they follow
— there's nothing to place after the fact, and nothing to get wrong by
copying a time out of the extract. The verbatim sections above still settle
any disagreement between a bullet and what actually happened.

### Names

Sessions are titled by Claude Code, from the first thing you said — so a session
that started as one question and became a day's work is filed under the question.
`chsum name` fixes that:

```sh
chsum name "marks: design + build"                 # the session you're in
chsum name ch_3654a13c "marks: design + build"     # one from last week
```

Your name wins everywhere chsum shows a title — listing, digest, journal — and is
flagged `✎` in the listing, because whose reading of the session it is matters.

It also lands in `/resume`. Claude Code's title is an `ai-title` record it appends
to the transcript as the conversation grows, dozens per session, last one wins;
`chsum name` appends one more of exactly that shape. Never a rewrite of a line
already written — the one thing chsum adds to a transcript, and it is added the
way Claude Code adds it.

That is why the name is also kept in `~/.chsum/names.json`: rename a
session that is still running and Claude Code will title it again ten minutes
later. chsum keeps yours; `/resume` may drift back.

`chsum name --list` shows what you've renamed, `--clear` undoes one — putting
Claude Code's own title back as another appended record, so `/resume` reverts too.
`--no-resume` renames in chsum only and leaves the transcript alone.

### Subagents

A subagent's edits and commands fold into its parent's totals — otherwise a
session that delegated everything reads as no activity. Files no parent turn
touched are marked `(agent)`. Each agent gets a line in **Delegated**, and its
task shows in the listing and in `journal` too — five deep, then a count, since
"3 agents" says nothing about a session that delegated its work. Each has an
address:

```sh
chsum context ch_da4e99d42e5efab11ebdedc22fb65145/a728cd49179f1a356
```

Its task, files, commands, and last message. Everything past the one-line summary
is fetched on demand, so a heavily-delegated session doesn't produce a digest
nobody wants to read.

`<parent-ref>/<agent-id>` resolves to `<uuid>/subagents/agent-<id>.jsonl`. chsum's
own scheme, not claude-history's — see *Notes on correctness*.

### Marks

`chsum mark` flags a moment while you're in it, so the digest says which part
mattered — extraction can tell you what changed, not which of it was the point.

```sh
! chsum mark "the shrinkwrap approach, after two dead ends"
```

The `!` prefix is the mechanism, not decoration. chsum writes nothing: it prints
a marker line, and Claude Code's own recording of the `!` run puts it in the
transcript, at the point in the conversation where you typed it. So there is no
second store to keep in sync, nothing injected into a file Claude Code is
appending to, and the mark inherits an `mN` and a durable `ma_` anchor for free.
Run outside a session it warns instead — there is nothing there to record it.

To mark something further back, list recent messages and name one:

```sh
! chsum mark --recent 20
47dca7e9  06:27  you     can we make the digest quote the anchor instead
be74e21f  06:40  claude  That collides — two messages with identical text share one anchor
a27a1c9c  06:41  claude  Edit: chsum.py
0b2f4db2  06:42  claude  Bash: python3 -m pytest -x
! chsum mark --at be74e21f "the anchor collision, explained properly"
```

Everything that happened, in order: both sides' messages *and* every tool call,
so you can mark the edit or the command rather than the sentence near it. Tool
results are left out — a mark resolves to the message containing the action
either way.

Ids come from the transcript itself. `--at <line>` takes a bare row number too.

Or name the message by something it said:

```sh
! chsum mark --match "worth knowing exactly where it dies" "the subagent gap"
```

Matching folds case, punctuation, and markdown away — `currently no` finds
`Currently **no** —`, because nobody retypes the asterisks. Marks still quote the
original bytes. If more than one message matches, chsum lists the candidates and
marks nothing: asking to mark a phrase puts that phrase in your own prompt too,
so "newest wins" would keep marking the request instead of its subject. `chsum
mark`'s own calls and output are excluded from matching — its tool call is
recorded before the command runs, so otherwise every search would find itself.

Marks show up as **Notable** at the top of the digest, verbatim, with the message
they point at; as a `⚑` count in the listing; inline in `journal`; and
`chsum find --marks [query]` searches them across sessions.

Marked something you'd rather not keep:

```sh
! chsum mark --list
the subagent gap, stated plainly    dde43c3c
  ↳ Currently **no** — and worth knowing exactly where it dies.

Testing                             32e8b253
  ↳ Left in place — it records the state that prompted the change.

! chsum mark --list --full          # whole reason, whole marked message
! chsum mark --show dde43c3c        # where it landed, with what surrounds it
! chsum mark --revoke 32e8b253      # takes several ids at once
```

Each mark shows its reason, its id, and the message it marks. A bare `chsum mark`
points at the message it followed — its own output record says nothing about what
you were marking.

`--show` takes the same id and answers where: the file, the row in it, the time,
the agent when the message is a subagent's, the row it sits on where the
caught up, then the marked message whole and `--context N` records either side
(3 by default). The location is stamped into the mark as it is made, so this is a
lookup rather than a fresh search of the conversation and every sidecar. A stamp
that disagrees with the record it names is discarded and the search runs anyway,
which is what keeps an edited or rebuilt transcript from pointing somewhere wrong.

A revocation is another line of output, same as a mark — nothing was written, so
there is nothing to delete. Both records stay in the transcript; the mark simply
stops counting everywhere marks are read.

A subagent can mark too. Its marks land in its own sidecar and fold into the
parent, like its edits and commands, tagged `agent <id>` instead of an `mN` —
sidecars have no ordinals or anchors to cite. Revocations cross that boundary in
both directions: the parent can drop a mark its agent made, and vice versa.

### Search modes

`--hybrid` (default) and `--semantic` are best for conceptual recall but are slow:
tens of seconds warm, and **several minutes on the very first run** while the
embedding index builds. Use `--lexical` (sub-second) for identifiers, filenames,
and error strings, or `--exact` for exact tokens.

### Reporting something that looks wrong

`--debug` goes on the end of any command. It prints what that run read, ran and
resolved, beneath the normal output:

```sh
chsum digest ch_8b0a671d… --stdout --debug
```

```
--- chsum debug ---
invocation: chsum digest ch_8b0a671d… --stdout --debug
cwd: ~/Documents/Code/chsum
projects: ~/.claude/projects/  (…/ below)
chsum 1.2.0 (9162a1d dirty) · python 3.10.11 · darwin · exit 0
files (1)
  ch_8b0a671d…  meta,marks  947.1K  671 recs  …/-Users-…-chsum/9a9e9ac5-….jsonl
procs (3)
  0   0.03s  claude-history agent outline ch_8b0a671d… --no-budget  (1005 chars out)
  0   0.01s  claude-history agent read ch_8b0a671d…:m1..m27 --no-budget  (11084 chars out)
steps (4)
  resolve_ref    via=argv ref=ch_8b0a671d…
  _parent_path   ref=ch_8b0a671d… uuid=9a9e9ac5-…
  scan_marks     file=9a9e9ac5-….jsonl marks=0 sentinel=absent
  read_messages  ref=ch_8b0a671d… asked=m1..m27 messages=7 got=m1..m27
reproduce
  chsum digest ch_8b0a671d… --stdout
  claude-history agent read ch_8b0a671d… --no-budget
--- end chsum debug ---
```

No transcript text is copied — only paths, refs, record ids and counts — so the
block names the record behind a line that looks wrong rather than carrying it.
It assumes the reader is on the same machine: paste it into a session in the
chsum checkout and the files it names are still there to open.

`steps` is where a wrong line usually comes from. Each one is a resolution with
its inputs and its result, including the fallbacks that print nothing during a
normal run: `_verify_stamp … match=no fallback=walk` (a mark's stamp disagreed
with the record it named), `_turn_checkpoints … checkpoint=4 transcript=2` (two
turns had no checkpoint covering them), `_run_chunk … failed=1` (one summariser call
died and its gap says so). Repeated steps collapse in the middle, and `files`
and `procs` cap at 24 rows with the remainder counted.

Without the flag nothing is recorded and nothing is printed — verified
byte-identical against the previous release across thirteen invocations.

## What a digest contains

| Section | Source |
|---|---|
| Frontmatter — ref, title, project, branch, start, duration, counts | computed |
| **Notable** — what you flagged with `chsum mark`, verbatim | copied |
| **What I asked for** — your prompts, verbatim, in order | copied |
| **Files changed** / **Commands run** | parsed from tool calls |
| **Delegated** — one line per subagent, with its address | parsed from sidecars |
| **Where I left off** — last prompt and last reply, verbatim | copied |
| — *found by two separate backward scans, so they may be far apart and are not a Q&A pair* | |
| **Drill down** — transcript and sidecar paths, and the `sed` that opens a row | computed |

An agent digest has the same shape minus the intent trail — an agent gets one
instruction, so **Task** is a single block.

Every reference in a digest is a row: `1f271ca8:441` is line 441 of session
`1f271ca8…`'s transcript, and `1f271ca8/a190d601:87` is line 87 of that session's
`a190d601…` sidecar. **Drill down** expands both to full paths. Nothing needs a
second tool to resolve.

Output is budgeted, because it lands in a future context window: quotes clip,
lists cap. Every truncation is marked (`[+N chars, read the anchor]`, `…and N
more`) so you always know when you're seeing a fragment.

**Commands run** shows ten, and it shows them after a filter that drops
look-only commands (`ls`, `cat`, `grep`, and 27 more) and after a dedupe. Its
overflow line therefore carries two numbers and the invocation that opens the
rest:

```
- …and 25 more of these — `chsum digest ch_c77196cc… --commands` lists all 60 in order
```

`--messages`, `--tools` and `--commands` each print one line per row — the id
where there is one, a `<session>:<line>` locator, local time, the role or tool
name, and the first line of the text — in timestamp order across the transcript
and its sidecars, with nothing filtered, deduplicated or collapsed. An agent ref
(`<ref>/<agent-id>`) narrows any of them to that sidecar. `--call <id>` prints one
tool call and its captured output whole.

A `## Sources` block at the top expands every locator to a full path and gives a
worked `sed` line, so a row reaches its raw record without chsum:

```
## Sources

- `f1b9bbc6` — `~/.claude/projects/…/f1b9bbc6-….jsonl`
- `f1b9bbc6/a190d601` — `~/.claude/projects/…/f1b9bbc6-…/subagents/agent-a190d601….jsonl`

Open a record: `sed -n '18p' ~/.claude/projects/…/f1b9bbc6-….jsonl`

- `01CPKjYaSD`  `f1b9bbc6:26`  11:30:28  Bash  `ls && wc -l chsum.py`
```
`--command <id>` prints one of them whole with its captured output, which is
where the text the row clipped actually lives. Both print rather than writing a
file: they are lookups reached from a hint, not artifacts to keep.

## Notes on correctness

Several things here are non-obvious and were established by measuring, not assuming:

- **Duration excludes idle time.** Sessions get resumed hours or days later, so
  first-record-to-last-record wildly overstates effort — one session in the corpus
  reads as 92 hours. Gaps over 30 minutes are treated as "walked away".
- **Anchors are content-addressed, so they can collide.** Two messages with
  byte-identical text (`[Request interrupted by user]`, say) share one anchor, and
  `read --anchor` then fails with `ambiguous-ref`. Ambiguous anchors are detected
  and never published — every anchor a digest prints resolves to exactly one message.
- **Most "user" records aren't from you.** They're tool results, interrupts, and
  harness scaffolding. Those are filtered out; `prompts:` counts what you typed.
- **`outline` has two output shapes** — segment ranges for long conversations,
  per-message lines for short ones. Both are handled.
- **Subagent transcripts** aren't conversations in their own right and never appear
  in the listing, matching `claude-history`'s discovery rules.
- **`claude-history` has no per-agent ref.** `--subagents` inlines agent messages
  into the parent read untagged, so they can't be sliced apart. Sidecars are
  parsed directly. Every digest reads the JSONL this way now, parent included.
- **An agent's last message isn't necessarily its conclusion**, so the section is
  *Last thing it said*. An interrupted agent ends mid-thought.
- **Agent counts take the larger of two sources** — `Agent`/`Task` calls in the
  parent, and sidecars on disk. Sidecars go missing; an agent that spawns its own
  outnumbers the visible calls.
- **Scratch paths** (`/tmp`, scratchpads, plan files) are excluded from "files
  changed" so the work log shows real project changes.

## Prose, and where it's allowed

The `Summariser` seam at the bottom of `chsum.py` has its first backend:
`HaikuSummariser`, used only by `recap`, via `claude -p --model haiku` — no
SDK, no key handling, your existing Claude Code auth signs the call. A TL;DR is
the one thing extraction can't produce, and a running session is where you're
most likely to want one before the transcript catches up.

The rule for any backend: it gets the already-extracted material, and its output is
**additive** — layered on top of the verbatim record so a wrong sentence can always
be checked against the quotes beneath it.

If you do go local, note that the model in `mlx-community/DeepSeek-R1-Distill-Qwen-14B-MLX`
is **139 GB** of unquantised weights. The 4-bit build is `…-14B-4bit` at 8.32 GB. On a
16 GB machine the binding constraint is KV cache, not context length: this architecture
costs 192 KB/token at fp16 (96 KB with `kv_bits=8`), so after 8.32 GB of weights you get
roughly 18k–36k tokens of usable input, not the 131k the config advertises.
