SCOUT Advisory Memo | A068 | 2026-04-26
---
Claude Code's context window is 200K tokens. Auto-compaction fires at approximately 83.5% fill (~167K tokens used), reserving a 33K-token buffer for the summarization LLM to do its work. Once compaction triggers, the full conversation history is replaced by a lossy prose summary. What you had as structured, queryable conversation turns becomes a fuzzy paragraph. The session continues, but the model now operates on a degraded signal.
For TITAN specifically, this means:
The compaction is not a crash. CLAUDE.md survives (re-read from disk on every turn). Skills re-inject their most recent invocation. But the conversation layer — the actual live state — is gone.
---
As of Claude Code v2.1.x (confirmed April 2026), PreCompact is a first-class hook event.
| Trigger | When |
|---------|------|
| auto | Context usage hits ~83.5% of the window; Claude Code triggers compaction automatically |
| manual | User runs /compact explicitly |
The hook fires before summarization begins. This is the last moment the full transcript is on disk and readable.
{
"session_id": "abc123...",
"transcript_path": "/Users/.../.claude/projects/.../session-id.jsonl",
"cwd": "C:/Users/Harnoor/Desktop/Trillionair...",
"hook_event_name": "PreCompact",
"trigger": "auto"
}
Fields confirmed present in production (v2.1.x):
session_id — current session UUIDtranscript_path — absolute path to the session JSONL on disk (critical — this is the raw, uncompacted log)cwd — working directory when the hook fireshook_event_name — always "PreCompact"trigger — "manual" or "auto"Note: custom_instructions is included only for manual compaction (whatever the user passed to /compact). For auto triggers it is absent or empty.
Standard TITAN environment variables are inherited from the parent Claude Code process:
TITAN_ROOT (if set in shell profile)CLAUDE_PROJECT_DIR — project rootCLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA — plugin paths (if plugin hook)No special PreCompact-specific env vars beyond the stdin payload.
| Exit Code | Effect |
|-----------|--------|
| 0 | Hook succeeded; compaction proceeds normally |
| 2 | Blocks compaction. stderr message is fed back to Claude. Use to abort. |
| Any other non-zero | Non-blocking error; compaction still proceeds |
Block via exit code 2:
echo "Compaction blocked: state not yet written" >&2
exit 2
Or via JSON output:
{"decision": "block", "reason": "PreCompact dump in progress"}
Important caveat: If auto-compaction triggered because the API already returned a context-limit error (i.e., you were already over), blocking via exit 2 surfaces that underlying API error and the current request fails. Block only works cleanly when compaction is proactive (triggered before the hard limit hit).
PreCompact can be defined in:
~/.claude/settings.json — user-wide (TITAN's preferred location).claude/settings.json — project-wide.claude/settings.local.json — project, git-untrackedTITAN already has an entry in ~/.claude/settings.json. See Section 5 for the updated config.
Default: 600 seconds (10 minutes). Set "timeout": N in the hook object. For a fast Python dump script, 30 seconds is generous.
Setting "async": true means Claude Code does not wait for the hook to complete before proceeding with compaction. Do not set async: true on PreCompact. The dump must complete before compaction summarizes the transcript. Async is appropriate for fire-and-forget logging hooks (PostToolUse, Stop), not for state-preservation hooks that must complete before context is destroyed.
---
The transcript_path points to a JSONL file (one JSON object per line) in:
~/.claude/projects/<project-slug>/<session-id>.jsonl
Each line is one event. Confirmed schema from production transcripts:
uuid, parentUuid, isSidechain, sessionId, version, gitBranch,
type, timestamp, cwd, userType, entrypoint
type: "user" events
{
"type": "user",
"message": {
"role": "user",
"content": "the user's message text or tool_result array"
},
"promptId": "...",
"uuid": "...",
"timestamp": "..."
}
type: "assistant" events
{
"type": "assistant",
"message": {
"role": "assistant",
"model": "claude-sonnet-4-6",
"content": [
{"type": "text", "text": "..."},
{"type": "tool_use", "name": "Write", "input": {...}},
{"type": "thinking", "thinking": "..."}
],
"usage": {
"input_tokens": 45000,
"cache_creation_input_tokens": 23710,
"cache_read_input_tokens": 0,
"output_tokens": 761
}
}
}
type: "summary" eventsThese appear when a prior compaction has occurred. Content is the prose summary text.
isSidechain: true)Agent subagent turns. These have their own separate JSONL in a subagents/ directory. They do NOT count against the main session's context window.
| What to extract | Where it lives in JSONL |
|-----------------|------------------------|
| Last N user messages | type="user", message.role="user", tail of file |
| TodoWrite calls | type="assistant", content[].type="tool_use", name="TodoWrite" |
| Recent Bash/Write/Edit tool calls | content[].type="tool_use", name in {Bash, Write, Edit} |
| Token usage (to compute fill %) | message.usage.input_tokens + cache_creation_input_tokens + cache_read_input_tokens |
| Ask IDs mentioned | regex A\d{3} across all text content |
| R-numbers mentioned | regex R\d{4} across all text content |
---
The most comprehensive community implementation. Reads the full JSONL, inserts every message into a local SQLite database on every compaction event. PostCompact hook then queries the database and re-injects structured context (project summary, last session notes, recent decisions, relevant historical matches via keyword/semantic search).
Scale achieved: 1,300 sessions, 69,000 messages, ~1 GB database.
Why it matters for TITAN: The SQLite approach gives TITAN a queryable history. But it's heavyweight — a full db insert on every compaction adds latency and a new dependency. TITAN's current architecture writes flat files. The lighter-weight version (Adolan's "simple alternative") is the SESSION_STATE.md approach — a flat markdown file that documents current task, key decisions, modified files, encountered errors, next steps.
A three-file architecture:
backup-core.mjs — transcript parser, markdown formatterstatusline-monitor.mjs — runs on every turn, monitors token usage, fires backups at 50K tokens then every 10K, and at 30%/15%/5% remaining via StatusLineconv-backup.mjs — the PreCompact hook proper, emergency backupCritical insight: StatusLine is the only hook that receives live token counts. The PreCompact hook itself does NOT know how full the context was when it fired — you infer it from the transcript's last usage block. The ClaudeFa.st pattern uses StatusLine as a continuous early-warning system and PreCompact as the final failsafe.
Token math: freeUntilCompact = max(0, pctRemainTotal - 16.5%) because 33K tokens are reserved for the autocompact buffer.
TITAN implication: A StatusLine monitor that starts dumping state at 60-70% fill is more reliable than waiting for PreCompact. But PreCompact is the backstop.
Originates from Zara Zhang's manual /handover command concept; automated by Paul Oportella into a PreCompact hook. The hook spawns a fresh Claude instance (claude -p) with a prompt to read the transcript and write a "shift-change report" — what was accomplished, solutions attempted, decisions made, next steps. Output: HANDOVER-YYYY-MM-DD.md.
Why it matters: This is the LLM-assisted summary approach. Instead of raw dump, you get an opinionated summary written by Claude itself. The downside: adds a new API call (cost, latency) and introduces another point of failure. For TITAN's PreCompact hook, a deterministic Python parser is more reliable — no LLM call required.
Three persistent files per project:
plan.md — approved implementation strategy (frozen)context.md — living state: completed tasks, active files with line numbers, current blockers, next stepstasks.md — granular checklist with completion statusUpdated by the user running /update-dev-docs before compaction. After compaction, typing "continue" causes Claude to read the files automatically. Results: context rebuild time dropped from 20-30 minutes per session to 0-2 minutes.
TITAN adaptation: Automate the /update-dev-docs step via PreCompact hook. The hook writes these files from transcript analysis rather than requiring manual user action.
Uses StatusLine + UserPromptSubmit hook chain:
1. StatusLine calculates total = input + cache_creation + cache_read, computes pct = total / window_size
2. Saves to a per-session JSON file
3. UserPromptSubmit reads the JSON and injects urgency instructions into Claude's context
Urgency levels:
/compactThe key insight this pattern reveals: Claude Code cannot introspect its own context fill percentage. The only way to know it proactively is via StatusLine data saved externally. A PreCompact hook firing at 83.5% fill means you've already missed the "do something gracefully" window. The StatusLine + warning pattern gives TITAN advance notice.
---
Understanding the lossy compression is critical for knowing what to dump.
It reads the full conversation history and produces a prose summary that tries to capture:
| Category | Example of what was there | What survives after compaction |
|----------|--------------------------|-------------------------------|
| Exact error messages | TypeError: Cannot read property 'map' of undefined at line 847 | "there was a TypeError" |
| Specific file paths | F:/TITAN/scripts/titan-precompact.py line 44 | "a script in the TITAN directory" |
| Debugging chains | 4 failed approaches + why each failed | "some debugging was attempted" |
| Ask ledger state | A004b re_ask_count=5, status=open, nuclear priority | "some tasks were in progress" |
| R-number sequence | R0209 ships tomorrow, R0210 is the color fix, R0211 is STT | "work on releases was ongoing" |
| Harnoor's emotional signals | "i'm pissed, this is nuclear priority" | (usually dropped entirely) |
| TodoWrite items | {content: "implement X", status: "in_progress", id: "abc"} | "there were some pending todos" |
| SCOUT memo progress | "A068 memo is 60% written, need to add section 4" | "research was being done" |
| Architecture decisions | "use flat JSONL not SQLite because existing TITAN tooling is flat-file" | "a storage decision was made" |
Based on the ask ledger and project structure:
1. Ask ledger tail — last 10 entries from F:/TITAN/state/harnoor-asks.jsonl (the most recently discussed ones, including status and re_ask_count)
2. In-flight TodoWrite items — any TodoWrite tool calls in the transcript with status in_progress or pending
3. Current R-number — the highest R-number mentioned in the last 20 assistant turns
4. SCOUT memos in flight — any reference to advisor memo filenames that appeared in the last 50 turns
5. Harnoor mood/urgency signals — any user messages containing priority signals (nuclear, pissed, S-tier, can't believe, asked X times)
6. Current working directory and active files — which files were most recently written/read
7. Session token usage — computed from the last assistant message's usage block
---
The existing script (as of 2026-04-26) captures only: session_id, cwd, timestamp, trigger, transcript_path. It writes a nearly-empty stub. It does not read the transcript. This is essentially a no-op from a state-preservation standpoint.
PreCompact fires
|
precompact_dump.py reads stdin JSON (gets transcript_path)
|
Parse transcript JSONL:
- Extract last 20 user messages (verbatim)
- Extract all TodoWrite tool calls
- Extract A-number and R-number mentions from last 50 turns
- Extract last assistant message's token usage block
- Extract any file paths written in last 30 turns
|
Read F:/TITAN/state/harnoor-asks.jsonl (last 15 entries)
|
Write F:/TITAN/state/precompact-snapshots/<timestamp>.md
(structured markdown, human-readable, Claude-ingestible)
|
Also write F:/TITAN/state/precompact-latest.md
(always the most recent snapshot — PostCompact hook can read this)
|
Print hookSpecificOutput.additionalContext to stdout
(injected into Claude's context immediately after compaction)
|
Exit 0 (allow compaction to proceed)
---
session_id: abc123
timestamp: 2026-04-26T14:30:00
trigger: auto
cwd: C:/Users/Harnoor/Desktop/...
token_usage: 167000 / 200000 (83.5%)
---
# TITAN PreCompact Snapshot
## Ask Ledger (last 15 entries)
[table: ask_id, status, re_ask_count, paraphrase, notes]
## In-Flight Todos
[TodoWrite calls with in_progress/pending status]
## Current R-Number
R0211 (most recent R-number in last 50 turns)
## Recent SCOUT Memos
[any F:/TITAN/plans/advisors/*.md filenames mentioned]
## Urgency Signals from Harnoor
[verbatim user messages containing nuclear/pissed/S-tier/asked X times]
## Files Active This Session
[file paths from Write/Edit/Read tool calls in last 30 turns]
## Last 20 User Messages (verbatim)
[tail of user turns]
The Handover pattern (spawning claude -p) is seductive but has three failure modes:
1. The spawned Claude instance hits its own cost/rate limit
2. The summarization LLM makes different editorial choices than you want
3. Adds 5-30 seconds of latency before compaction can proceed
Deterministic Python parsing is faster, cheaper, always available, and produces exactly the fields TITAN needs in a predictable structure. The additionalContext injection ensures Claude sees the snapshot immediately post-compaction.
---
See F:/TITAN/scripts/precompact_dump.py for the full implementation. Key sections:
import json, sys, re
from pathlib import Path
from datetime import datetime
def parse_transcript(transcript_path: str) -> dict:
"""
Returns extracted state from the JSONL transcript.
Reads from tail — most recent events first.
"""
lines = Path(transcript_path).read_text(encoding="utf-8").strip().splitlines()
events = []
for line in lines:
try: events.append(json.loads(line))
except: pass
user_msgs, todo_calls, tool_writes, a_numbers, r_numbers, urgency_signals = [], [], [], set(), set(), []
last_usage = {}
for ev in reversed(events[-200:]): # only process last 200 events
msg = ev.get("message", {})
role = msg.get("role")
content = msg.get("content", [])
if role == "user":
text = content if isinstance(content, str) else _text_from_content(content)
if text:
user_msgs.append(text[:2000])
# urgency signals
if any(w in text.lower() for w in ["nuclear","pissed","s-tier","asked","re-ask","can't believe","5 times","still not"]):
urgency_signals.append(text[:500])
# A/R numbers
a_numbers.update(re.findall(r'\bA\d{3,4}\b', text))
r_numbers.update(re.findall(r'\bR\d{4}\b', text))
if role == "assistant":
if not last_usage and isinstance(msg.get("usage"), dict):
last_usage = msg["usage"]
if isinstance(content, list):
for block in content:
if block.get("type") == "tool_use":
if block["name"] == "TodoWrite":
todo_calls.append(block.get("input", {}))
if block["name"] in ("Write", "Edit"):
fp = block.get("input", {}).get("file_path", "")
if fp: tool_writes.append(fp)
text_in = json.dumps(block.get("input", {}))
a_numbers.update(re.findall(r'\bA\d{3,4}\b', text_in))
r_numbers.update(re.findall(r'\bR\d{4}\b', text_in))
return {
"user_msgs": list(reversed(user_msgs))[-20:], # last 20
"todo_calls": todo_calls,
"tool_writes": list(dict.fromkeys(tool_writes))[-20:], # dedup, last 20
"a_numbers": sorted(a_numbers),
"r_numbers": sorted(r_numbers),
"urgency_signals": urgency_signals[-5:],
"last_usage": last_usage,
}
---
The existing TITAN settings.json already has a PreCompact block pointing to titan-precompact.py. That stub is being augmented with a second hook entry for precompact_dump.py — explicit 30-second timeout, no async flag, no matcher (fires for all sessions).
Updated PreCompact block in ~/.claude/settings.json:
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "python F:/TITAN/scripts/titan-precompact.py 2>/dev/null || true",
"timeout": 10
}
]
},
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python F:/TITAN/scripts/precompact_dump.py",
"timeout": 30
}
]
}
]
The first hook (existing titan-precompact.py) is kept as-is. The second hook (precompact_dump.py) is the new addition from A068.
---
When you notice Claude Code has compacted (the conversation history feels truncated, or you see "context was summarized" in the UI):
1. Read the pointer. cat F:/TITAN/state/precompact-last.txt gives you the snapshot path.
2. Read the snapshot. Open the .md file at that path. The ledger tail shows recent asks; the queue tail shows pending work; the memo index shows active research threads.
3. Re-inject critical context. Paste the relevant queue entries or plan steps back into the conversation. Do not rely on Claude Code's compaction summary alone for task queues.
4. Resume from the ledger. The last few ask-ledger entries will tell you exactly what TITAN was working on. Use them as the prompt for the resumption message.
5. Check the memo index. If a SCOUT memo is listed in the snapshot, it is still on disk. Read it directly with the Read tool — it contains the full research output that would have been summarised away.
Automation option. A future skill could read precompact-last.txt on session start and auto-inject the snapshot content into the system prompt, making resume fully automatic. This is tracked as a future enhancement under the /evolve skill.
---
Q: Can the hook block compaction if the dump fails?
Yes, via exit code 2. But only if the context limit hasn't already been hit by an API call. The safe approach: always try to dump, exit 0 regardless. A failed dump is better than a failed user request.
Q: Does the additionalContext from hookSpecificOutput actually survive post-compact?
Yes — the hook output's additionalContext field is injected into Claude's context after the hook completes, before the compaction summary is inserted. This means Claude will see the snapshot in the same turn that compaction fires.
Q: Should TITAN also add a StatusLine hook for pre-emptive warnings at 60% fill?
Yes, but that is a separate ticket. StatusLine runs on every turn and adds latency; it needs its own implementation. The PreCompact hook is the fallback floor. Ideal: StatusLine at 60% warns Harnoor; PreCompact at 83.5% does the deterministic dump.
Q: What about subagent transcripts?
Subagent sessions have their own JSONL in subagents/ with a different path. The PreCompact hook fires on the main session only. Subagent context is already isolated (doesn't count toward main window), so this is a non-issue in practice.
---
1. Claude Code Hooks Reference — Official Docs — April 2026
2. Feature Request #43733: PreCompact hook to write session state — April 5, 2026
3. Claude Code Compaction Kept Destroying My Work — Mike Adolan, DEV.to — 2026
4. Claude Code Context Backups: Beat Auto-Compaction — ClaudeFa.st — Jan 2026
5. Claude Code Hooks: Complete Guide to All 12 Lifecycle Events — ClaudeFa.st — 2026
6. Auto Memory & PreCompact Hooks Explained — Yuanchang's Blog — 2026
7. Context Warning System with StatusLine — Zenn.dev / trust_delta — 2025
8. Claude Code Context Buffer: 33K-45K Token Problem — ClaudeFa.st — 2026
9. Claude Kept Losing Context After Compaction — Chudi Nnorukam, DEV.to — 2026
10. Analyzing Claude Code Interaction Logs with DuckDB — Liam ERD — 2025
11. claude-code-hooks-mastery — disler/GitHub — 2026
12. Feature Request #15923: pre-compaction hook — Dec 2025 (closed)
13. Feature Request #25689: Context usage threshold hook — 2025