throughline
v0.3.22
Published
Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)
Downloads
3,194
Maintainers
Readme
Throughline
Cut ~90% of Claude Code's context usage while keeping nearly all the memory.
In a typical Claude Code session, 80% of the context window is tool I/O — file reads, Bash output, grep results. This data is consumed the moment Claude acts on it, but it stays in the context forever, pushing you toward the window limit.
Throughline fixes this by separating conversation content by type, not time:
Without Throughline (50 turns, no /clear):
Context = user text + assistant text + tool I/O + system messages
≈ 125,000 tokens (80% is tool I/O you'll never re-read)
With Throughline (50 turns → /clear → resume):
Context = recent 20 turns of conversation text (L2)
+ older 30 turns as one-line summaries (L1)
+ zero tool I/O (L3 — retired to SQLite, on-demand)
≈ 13,000 tokens — same decisions, same context, 90% lighterUnlike MemGPT or LangChain's SummaryBufferMemory which compress by recency (old = summarized), Throughline separates by content type: human-readable conversation stays, machine-generated tool output retires. This is purpose-built for coding assistants where tool I/O is heavy but transient.
The retired L3 data isn't lost — Claude can pull it back on demand via
throughline detail <time> when a past turn's tool output becomes relevant
again.
Throughline also ships a multi-session token monitor that reads real
Anthropic API usage from the transcript JSONL (no length / 4 heuristics).
Quick Start
npm install -g throughline
throughline installThat's it. install registers Throughline's hooks in ~/.claude/settings.json
(user scope), so every Claude Code project on your machine picks it up
automatically. No per-project wiring required.
Start any Claude Code session and your turns will begin flowing into
~/.throughline/throughline.db in the background.
Three-layer memory model (schema v7)
| Layer | Name | Where it lives | Content | Cost per turn | | ----- | ---------- | --------------------- | --------------------------------------------------------------------- | ------------- | | L1 | Skeleton | injected when old | one-line Haiku-generated summary of the turn | ~10 tok | | L2 | Body | injected when recent | user text + assistant reply, verbatim | full natural | | L3 | Detail | SQLite only | tool I/O, system messages, images, extended thinking (on-demand) | heavy, retired |
The layers are complementary and disjoint — nothing is duplicated across
them. Extended thinking blocks are stored at L3 (kind='thinking') so the
next session can see what the previous Claude was thinking at the moment it
was interrupted, not just what it said aloud. On SessionStart the thinking
of the final turn is injected inline above the L2 history; older thinking
remains retrievable via throughline detail <time>.
On SessionStart, Throughline rebuilds the context from SQLite and
injects it as plain text:
- The most recent 20 turns are injected as full L2 (
bodies) text - Older turns are injected as L1 (
skeletons) one-liners - L3 stays in SQLite and is retrieved on demand via
/sc-detail <time>
L1 summaries are generated by Claude Haiku 4.5 via a subprocess
(claude -p --model claude-haiku-4-5-*), reusing your Claude Max login — no API
key required. Summarization is lazy: for sessions that stay under 20 turns,
Haiku is never invoked, so short tasks cost zero summarization time.
All three layers (L1/L2/L3) have working write paths as of schema v5.
/sc-detail HH:MM:SS returns user/assistant text (L2) plus a kind-grouped view
of tool inputs, tool outputs, and hook output captured at L3 for that turn.
Explicit handoff via /tl (with in-flight memo)
Inheritance is opt-in, not automatic. When you want the next session to
pick up where this one left off, type /tl in the current session before you
/clear or open a new chat. Without /tl, new sessions start fresh — no
memory is carried over.
The /tl slash command does two things:
- Writes a handoff baton (the current
session_id) into thehandoff_batonstable via theUserPromptSubmithook. - Asks the current Claude to write an in-flight memo.
/tlinstructs Claude to summarize what it was about to do next, its current hypothesis, open questions, and in-progress TODOs, then pipe that Markdown intothroughline save-inflight, which attaches it to the baton'smemo_textcolumn. This captures the "currently thinking" state that plain transcript replay cannot preserve.
On the next SessionStart, the hook reads the baton, and if it is less than
1 hour old, merges that session's memory into the new session using a
deterministic UPDATE session_id = ? inside a BEGIN IMMEDIATE transaction.
The baton is consumed (deleted) atomically with the merge, so it cannot fire
twice. The injected resume context is reframed as "resuming an interrupted
task" rather than "reading past logs", and the in-flight memo plus the
final turn's extended thinking appear at the top so the new Claude picks up
mid-thought.
Session A (type /tl) -----------> baton written
|
/clear |
| ▼
Session B ---- reads baton, merges A into B, deletes baton ---->
|
(type /tl again to hand off further)Why explicit baton instead of auto-inherit:
- Zero false positives. A parallel window, a VSCode restart, or a genuine
new task in the same repo won't accidentally inherit the previous session's
memory. Only an explicit
/tltriggers inheritance. - VSCode extension compatibility. The
SessionStarthook'ssourcefield is rewritten to"startup"by the Claude Code VSCode extension even after/clear(see issue #49937), so source-based detection is unreliable. A user-driven baton sidesteps this. - Deterministic. No time-window heuristic, no PID guessing, no ancestor walking. The user declares intent; the hook carries it out.
Each merged row keeps its origin_session_id, so repeated /tl handoffs
accumulate memory through chains:
S1 (4 turns) --/tl,/clear--> S2 (merges S1, adds 3 turns) --/tl,/clear--> S3 (merges S2, adds 5 turns)
origin=S1×4 origin=S1×4, S2×3, S3×5Multi-session token monitor
Run:
throughline monitor # all active sessions in the current project
throughline monitor --all # every project, every session
throughline monitor --session <id-prefix>Example output (real values from a running 1M-context Opus session):
[Throughline] 1 セッション
▶ Throughline 2ed5039c ████░░░░░░░░░░░░░░░░ 205.1k / 21% 残 794.9k claude-opus-4-6- Token counts are accurate. Read straight from the latest
message.usagefield in the session transcript JSONL, which is what Anthropic's API actually reported (input_tokens + cache_creation_input_tokens + cache_read_input_tokens). Nolength / 4approximation. - 1M-context detection is automatic. It checks the
[1m]suffix in the transcript, falls back to string matching on1M context, and finally promotes to 1M if observed usage exceeds 200k. - Multi-session view. Each Claude Code session writes its own state file
(
~/.throughline/state/<session_id>.json). The monitor scans the directory every second and displays one row per live session, sorted by last activity. The most recent one is highlighted with▶. - Stale hiding. Sessions that haven't been touched in 15 minutes drop out of the default view; files older than 24 hours are deleted entirely. This is the only time threshold in the system and is used solely for display hygiene — no memory decisions are made from it.
- Line-wrap safe. Each line is truncated to
process.stdout.columns - 1before drawing, preserving ANSI color codes. The redraw cursor math cannot desync on narrow terminals. - Resize resilient via OSC 18t. Windows ConPTY + VS Code task terminals
freeze
process.stdout.columnsat the PTY's initial size and never propagate panel resizes into Node, so polling orresizeevents can't catch them. Throughline queries the terminal itself with the CSI18 tescape (\x1b[18t) every tick, parses the\x1b[8;rows;cols treply off stdin in raw mode, and uses the real current width for truncation. On terminals that don't answer the query, the renderer falls back toprocess.stdout.columns → env.COLUMNS → 80. When the width changes the viewport is cleared in full (\x1b[2J\x1b[3J\x1b[H) before the next frame so the previous, wrongly-sized frame can't stack beneath it. - Per-row "last updated" stamp. Each session row carries an 8-cell
just now/24m agostamp right after the session id, placed before the bar so narrow terminals don't truncate it. It resets tojust nowon every Stop hook, so a growing stamp means the session is truly idle — not the monitor stuck. When you need more detail,throughline doctor --session <id-prefix>compares the state file against the actual transcript JSONL and flags drift, idle time, and/clear-induced transcript path staleness. - State-backed usage snapshot. When the Stop hook finishes a turn it
persists the latest
tokens / model / contextWindowSizeback into the state file. The monitor prefers this snapshot over re-reading the JSONL, which removes a source of flicker when the transcript path in state drifts from the one Claude Code is currently appending to.
VS Code auto-start (automatic)
After throughline install, any VS Code / Cursor / VSCodium project you work in
gets .vscode/tasks.json provisioned automatically on the first session event.
The file configures runOn: folderOpen so the monitor appears in a dedicated
terminal panel the next time you open that folder.
How it works. ensureMonitorTaskFile is called from all three hooks
(SessionStart, UserPromptSubmit, Stop) as of v0.3.18. Whichever one fires
first in your environment creates the file; the rest are idempotent no-ops.
Once per project it inspects .vscode/tasks.json:
- No file yet → creates one with a single
Throughline Monitortask, and emits a one-time<system-reminder>to stdout so Claude tells you a Developer: Reload Window is needed to activate thefolderOpentask once (v0.3.19+). - Plain JSON with other tasks → appends the monitor task, preserves your
existing entries,
version, and indentation (same notice fires once). - JSONC (comments or trailing commas) → does not touch the file. Prints a one-time notice to stderr asking you to paste the snippet below.
- Already contains a Throughline Monitor task → does nothing (idempotent; this is the common path on every subsequent turn; notice is silent).
The generated task uses type: 'shell' with the absolute path to Node and
bin/throughline.mjs. VS Code wraps shell tasks in a PTY (xterm.js) so the
monitor sees isTTY=true, real columns, and resize events. Windows .cmd
shims and missing PATH entries cannot break it because the command is already
an absolute Node binary path.
Opt out: set THROUGHLINE_NO_VSCODE=1 in the environment used by Claude
Code. Delete .vscode/tasks.json (or just the monitor entry) if you want to
stop auto-start for a project that already has one.
Manual snippet for JSONC tasks.json files. If Throughline refused to edit
your tasks.json because it contains comments or trailing commas, add this
entry to the tasks array yourself:
{
"label": "Throughline Monitor",
"type": "shell",
"command": "throughline monitor",
"isBackground": true,
"presentation": {
"reveal": "always",
"panel": "dedicated",
"group": "throughline",
"close": false,
"echo": false,
"focus": false,
"showReuseMessage": false,
"clear": true
},
"runOptions": { "runOn": "folderOpen" },
"problemMatcher": []
}Commands
| Command | What it does |
| ---------------------------------------------- | ------------------------------------------------------------ |
| throughline install | Register hooks in ~/.claude/settings.json (user scope) |
| throughline install --project | Register hooks in .claude/settings.json for this repo only |
| throughline uninstall | Remove Throughline hooks from the settings file |
| throughline monitor [--all] [--session <id>] | Run the multi-session token monitor |
| throughline monitor --diag | Dump TTY/columns/env diagnostics (for debugging monitor render bugs) |
| throughline detail <time> | Retrieve L2 body text and L3 tool I/O for a turn (see below) |
| throughline save-inflight | Called by /tl to attach an in-flight memo (stdin) to the current baton |
| throughline doctor | Check Node version, hook registration, DB writability, PATH |
| throughline doctor --session <id-prefix> | Diagnose a specific session — detect state/transcript drift, idle vs. stuck |
| throughline status | Print DB statistics (sessions, skeletons, bodies, details) |
| throughline --version | Print the installed version |
Slash commands (invoked by the user in Claude Code):
| Command | What it does |
| ------------- | ----------------------------------------------------------------- |
| /tl | Write a handoff baton + ask Claude to save an in-flight memo for the next session |
| /sc-detail <time> | Retrieve L2 body text and L3 tool I/O for a past turn |
When
/tltriggers, Claude will callthroughline save-inflightvia its Bash tool. Claude Code will prompt for permission the first time; addBash(throughline save-inflight:*)to your allowlist to skip the prompt on subsequent/tlinvocations.
Hook subcommands (invoked by Claude Code, not by humans):
session-start (SessionStart), process-turn (Stop),
prompt-submit (UserPromptSubmit — detects /tl and writes baton).
throughline detail — for AI, not humans
throughline detail is the escape hatch Claude itself uses to pull archived
detail back into the context when an L1 summary isn't enough. The injection
footer explicitly instructs Claude to run this via its Bash tool when a past
turn's tool I/O becomes relevant.
throughline detail 14:23:05 # single timestamp
throughline detail 14:23-14:30 # timestamp rangeOutput groups records by kind: L2 conversation bodies, then L3 tool input/
output, then system messages (hook output), then images. Records are scoped to
the current project's merge chain so Claude only sees turns from its own
project history.
Requirements
- Node.js >= 22.5 (for the built-in
node:sqlitemodule — no native build required, nonpm installof SQLite bindings) - Claude Code with hooks support (
SessionStart,Stop) - Claude Max subscription (for Haiku-based L1 summarization via
claude -p) - Works on Windows, macOS, Linux
Throughline has zero runtime dependencies. The published tarball is just
plain .mjs files.
Data layout
~/.throughline/
├── throughline.db SQLite database (WAL mode)
├── haiku-workdir/ Isolated cwd for Haiku subprocess (recursion guard)
└── state/
└── <session_id>.json Per-session activity state for the monitorSchema v7:
sessions— one row persession_id, withproject_pathandmerged_intoskeletons— L1 one-liners, keyed by(session_id, origin_session_id, turn, role)bodies— L2 verbatim text (user + assistant), same key shapedetails— L3 records withkindcolumn (tool_input/tool_output/system/image/thinking) andsource_idfor idempotent re-processinghandoff_batons— one row perproject_path, withsession_id,created_at, andmemo_text(the in-flight memo written bysave-inflightafter/tl). Consumed and deleted by the nextSessionStartif within the 1-hour TTL.injection_log— audit trail of injection events
All memory tables carry an origin_session_id so rebonded rows keep their
lineage across a chain of /tl handoffs.
Design principle: no fallback code
Throughline deliberately refuses to swallow unexpected errors.
Silent try { … } catch { /* ignore */ } blocks hide bugs; instead, hooks throw
and exit with a non-zero status so Claude Code surfaces the failure in stderr.
Specifically:
- JSON parse failures →
throw, notcontinue - Missing required fields →
throw new Error(...), notexit(0) - DB transactions → explicit
BEGIN IMMEDIATE/ROLLBACK/ re-throw - Hook entry points wrap
main()with a single.catchthat writesstderrand exits with code 1
The only tolerated silent paths are:
- JSONL per-line parse tolerance (tail partial writes are part of the format spec)
- State-file corruption recovery (files are idempotently regenerated next turn)
See docs/PUBLIC_RELEASE_PLAN.md §0 for the full
rule.
Haiku recursion defense
L1 summarization spawns claude -p --model claude-haiku-4-5-* as a subprocess.
Without precautions this would recursively fire the same Stop hook on the
subprocess and infinite-loop. Two defenses stack:
- Isolated cwd. The subprocess runs in
~/.throughline/haiku-workdir/, a directory that contains no.claude/settings.json, so project-local hooks are never picked up by the child. - Env var guard. The parent sets
THROUGHLINE_IN_HAIKU_SUBPROCESS=1in the child env. The Stop hook (turn-processor.mjs) exits immediately on line 1 if it sees this variable.
See src/haiku-summarizer.mjs for the implementation.
Troubleshooting
Monitor says 待機中 — アクティブなセッションがありません
No session has touched its state file in the last 15 minutes. Send a message in
Claude Code and the monitor should pick it up within 1 second. If it still does
not, run throughline doctor.
Monitor seems stuck on the same value
Each session row ends with a (Nm ago) stamp. If it keeps growing, the session
is idle — no assistant turn has finished. For a deeper look, run
throughline doctor --session <id-prefix> to compare the state file against
the actual transcript JSONL and flag drift, idle time, or /clear-induced
transcript path staleness.
throughline install wrote to the wrong settings file
By default, Throughline installs to ~/.claude/settings.json (user scope, applies
to all projects). Use --project to scope it to the current directory's
.claude/settings.json instead.
Hooks never fire
Run throughline doctor — it checks Node version, hook registration, DB
writability, and PATH resolution. If the binary is not on PATH, reinstall with
npm install -g throughline.
node:sqlite warning on startup
Node.js prints ExperimentalWarning: SQLite is an experimental feature on stderr.
This is cosmetic — the module is stable enough for production and is used
unchanged here.
Database got corrupted / want a clean slate
Delete ~/.throughline/throughline.db (and the -shm / -wal companion files)
and ~/.throughline/state/*.json. A fresh database with schema v7 is created on
the next hook fire.
New session didn't inherit memory from the previous one
This is the designed behavior — inheritance requires an explicit /tl in the
previous session. If you forgot to type it before /clear, the memory is still
in SQLite but won't auto-inject. You can still retrieve specific turns with
/sc-detail <time>.
Development
git clone https://github.com/kitepon-rgb/Throughline.git
cd Throughline
npm link # Put `throughline` on PATH (dev only)
throughline install --project # Register hooks for this repo only
node --test src/turn-processor.test.mjs src/session-merger.test.mjsRun the monitor directly without a global install:
node src/token-monitor.mjsThe .vscode/tasks.json in this repo auto-launches the monitor when you open
the folder in VS Code.
Design docs
docs/L1_L2_L3_REDESIGN.md— core design spec for the L1/L2/L3 differential layer model (schema v4 base + v5 L3 classification extension). Authoritative for the memory layering rules.docs/INHERITANCE_ON_CLEAR_ONLY.md— design record for the/tlbaton handoff system (schema v6–v7). Explains why the current inheritance is opt-in rather than heuristic.docs/PUBLIC_RELEASE_PLAN.md— public release plan, implementation status by version, § 0 fallback rule, and remaining tasks.docs/archive/— superseded design documents kept for historical reference (original CONCEPT, session-linking experiments, pre-publish action list).
License
MIT — see LICENSE.
