Tool Journaling for Compaction Safety
May 11, 2026 · View on GitHub
Overview
Tool journaling is an external log observer pattern that preserves agent execution context across Claude Code's context window compaction events. When long-running wave agents approach their context limits, Claude Code automatically compacts the conversation history, removing older tool use/result blocks. This can leave agents disoriented — they lose visibility into what they've already done, which files they've modified, and what test results they've seen.
The journal system solves this by:
- Tailing Claude Code session logs (
.claude/sessions/*.jsonl) in real-time - Extracting tool execution history into a persistent, indexed format
- Generating context summaries that are injected into agent prompts before launch
- Providing recovery checkpoints for debugging failed agents
This is not a backend modification — it's an external observer that requires zero changes to Claude Code's internal architecture.
Why It Exists
Problem: Context compaction in long-running waves erases agent memory. An agent that has spent 45 minutes implementing a feature, running tests, and iterating on failures can suddenly lose all that history when Claude Code compacts the conversation. The next tool call after compaction operates with amnesia — no knowledge of prior work, no awareness of files already modified, no memory of test failures already encountered.
Solution: The journal observer runs alongside the wave execution loop. Before launching each agent, the orchestrator:
- Starts the journal observer for that agent (
journal.NewObserver(projectRoot, agentID)) - Generates a context summary from the journal (
observer.GenerateContext()) - Prepends the summary to the agent's prompt as a
## Prior Worksection
When Claude Code compacts the agent's conversation, the journal-generated context remains in the prompt — the agent retains memory of its own execution history.
When recovery happens: Automatic, before every agent launch. The orchestrator calls observer.Sync() to update the journal index, then observer.GenerateContext() to produce the markdown summary. No manual intervention required.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Claude Code Session │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Wave Agent (wave1-agent-A) │ │
│ │ • Read, Write, Edit, Bash tool calls │ │
│ │ • Compaction events erase history after ~30-45 min │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ (logs to) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ .claude/sessions/1a2b3c4d.jsonl │ │
│ │ {"type":"tool_use","name":"Read",...} │ │
│ │ {"type":"tool_result","tool_use_id":"...",...} │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ (tailed by)
▼
┌─────────────────────────────────────────────────────────┐
│ JournalObserver (external process) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ .polywave-state/journals/wave1/agent-A/ │ │
│ │ ├── cursor.json (read position tracker) │ │
│ │ ├── index.jsonl (full tool history) │ │
│ │ ├── context.md (generated summary) │ │
│ │ ├── recent.jsonl (last 50 entries, fast scan) │ │
│ │ └── results/ (full tool output files) │ │
│ │ ├── tool_001.txt │ │
│ │ └── tool_002.txt │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ (generates)
▼
┌─────────────────────────────────────────────────────────┐
│ Agent Prompt (before launch) │
│ ## Prior Work │
│ You have already modified: │
│ - pkg/journal/observer.go (created) │
│ - pkg/journal/observer_test.go (created) │
│ │
│ Test results: │
│ - TestSync: PASS │
│ - TestGenerateContext: FAIL (line 42: nil pointer) │
│ │
│ Git commits: │
│ - abc1234 "implement journal sync" │
└─────────────────────────────────────────────────────────┘
External observer pattern: The journal system never modifies Claude Code's session logs. It reads them as an external observer, extracting tool execution events into its own index. This design keeps the journal system decoupled — it can be disabled, upgraded, or debugged without affecting Claude Code's operation.
Journal Structure
Directory Layout
All journal state lives in .polywave-state/journals/<wave>/<agent>/:
.polywave-state/
└── journals/
└── wave1/
├── agent-A/
│ ├── cursor.json # Read position in session log
│ ├── index.jsonl # Full tool execution history
│ ├── context.md # Generated summary for prompt injection
│ ├── recent.jsonl # Last 50 entries (fast scan for debugging)
│ ├── results/ # Full tool outputs (>800 chars)
│ │ ├── tool_001.txt # Read tool output
│ │ └── tool_002.txt # Bash tool output
│ └── checkpoints/ # Named snapshots
│ ├── pre-test-run.tar.gz
│ └── post-merge.tar.gz
└── agent-B/
└── ... (same structure)
File Formats
cursor.json — Tracks read position in Claude Code's session log:
{
"session_file": "1a2b3c4d.jsonl",
"offset": 2048
}
index.jsonl — One line per tool use/result pair:
{"ts":"2026-03-10T19:30:15Z","kind":"tool_use","tool_name":"Read","tool_use_id":"toolu_01ABC","input":{"file_path":"/path/to/file.go"}}
{"ts":"2026-03-10T19:30:16Z","kind":"tool_result","tool_use_id":"toolu_01ABC","content_file":"results/tool_001.txt","preview":"package journal\n\nimport...","truncated":true}
context.md — Generated summary for agent prompt injection:
## Prior Work
You are resuming work on Wave 1, Agent A (branch: wave1-agent-A).
### Files Modified
- pkg/journal/observer.go (created, 234 lines)
- pkg/journal/observer_test.go (created, 89 lines)
### Test Results
- TestSync: PASS
- TestGenerateContext: FAIL
Error: line 42: nil pointer dereference in extractFilesModified
### Git Commits
- abc1234 (2026-03-10 19:25): implement journal sync
- def5678 (2026-03-10 19:40): add context generation
### Recent Activity (last 10 tool calls)
1. Read pkg/journal/types.go
2. Write pkg/journal/observer.go
3. Bash: go test ./pkg/journal/...
4. Read pkg/journal/observer_test.go
5. Edit pkg/journal/observer.go (fix nil check)
...
recent.jsonl — Last 50 entries from index.jsonl (for fast debugging without scanning full history).
results/ — Full tool outputs for entries where truncated: true in index.jsonl (preview is first 800 chars; full output lives here).
Context Recovery
How Context is Generated
The GenerateContext() function analyzes the journal index and produces a markdown summary with:
- Files modified — Extracted from Write/Edit tool calls and
git diffoutputs - Test results — Parsed from Bash tool calls that ran test commands (exit codes + failure messages)
- Git commits — Extracted from
git logandgit commitoutputs - Recent activity — Last 10 tool calls (tool name + input summary)
Filtering logic:
- Only includes tool calls from the current wave/agent (filters by directory/branch context)
- Prioritizes failures over successes (failed tests appear before passed tests)
- Deduplicates files (if a file was modified 5 times, shows only the latest state)
When Context is Injected
Before agent launch: The orchestrator runs:
observer := journal.NewObserver(projectRoot, agentID)
observer.Sync() // Update index from session log
contextMd, _ := observer.GenerateContext()
prompt := contextMd + "\n\n" + agentPrompt // Prepend to agent brief
During execution: The journal observer syncs periodically (every 30 seconds) to keep the index up-to-date. If the agent is re-launched mid-wave (e.g., after a transient failure), the context regenerates with the latest state.
No re-injection during compaction: The context is injected once, at agent launch. Claude Code's compaction events don't trigger re-injection — the context summary stays in the prompt as static text. This is intentional: re-injecting after every compaction would be expensive and unnecessary (the summary already captures the full history up to launch).
Debugging Failed Agents
CLI Command: polywave-tools debug-journal
When an agent fails mid-wave, inspect its journal to understand what it did before failure:
# Full summary (same as context.md)
polywave-tools debug-journal wave1/agent-A
# Show only failed tool calls
polywave-tools debug-journal wave1/agent-A --failures-only
# Show last N entries
polywave-tools debug-journal wave1/agent-A --last 20
# Export as HTML timeline (opens in browser)
polywave-tools debug-journal wave1/agent-A --export timeline.html
Output format:
=== Journal Summary: wave1/agent-A ===
Branch: wave1-agent-A
Total entries: 142
Duration: 45m 32s
Files Modified:
pkg/journal/observer.go (created, 234 lines)
pkg/journal/observer_test.go (created, 89 lines)
Test Results:
PASS: TestSync (3 runs)
FAIL: TestGenerateContext (line 42: nil pointer)
Recent Activity (last 10):
19:55:12 | Read | pkg/journal/types.go
19:55:18 | Write | pkg/journal/observer.go
19:56:02 | Bash | go test ./pkg/journal/...
...
Failures-only mode (--failures-only):
- Filters to Bash tool calls with non-zero exit codes
- Filters to Read tool calls that returned errors
- Shows full error output (no truncation)
HTML timeline export (--export timeline.html):
- Generates an interactive timeline with collapsible tool outputs
- Color-codes by tool type (Read = blue, Write/Edit = green, Bash = orange)
- Highlights failures in red
- Embeds full tool outputs inline (expands on click)
Manual Journal Inspection
For deep debugging, inspect journal files directly:
# View full index (JSONL format)
cat .polywave-state/journals/wave1/agent-A/index.jsonl
# View last 50 entries (fast scan)
cat .polywave-state/journals/wave1/agent-A/recent.jsonl
# View specific tool output
cat .polywave-state/journals/wave1/agent-A/results/tool_042.txt
# Check cursor position
cat .polywave-state/journals/wave1/agent-A/cursor.json
Checkpoints
Checkpoints are named snapshots of journal state at key milestones. Use them to restore an agent's context to a known-good state.
Creating Checkpoints
Automatic checkpoints (created by orchestrator):
pre-wave— Before agent launchpost-commit— After agent commits to worktree branchpre-merge— Before merge to main
Manual checkpoints (created by user):
polywave-tools checkpoint wave1/agent-A pre-test-run
What's captured:
- Full
index.jsonlat checkpoint time context.mdsnapshot- Cursor position
- Checkpoint metadata (name, timestamp, entry count)
Checkpoint files are stored as .tar.gz archives in .polywave-state/journals/<wave>/<agent>/checkpoints/.
Listing Checkpoints
polywave-tools list-checkpoints wave1/agent-A
Output:
=== Checkpoints: wave1/agent-A ===
1. pre-wave (2026-03-10 19:30:15, 0 entries)
2. post-first-commit (2026-03-10 19:45:22, 67 entries)
3. pre-test-run (2026-03-10 19:50:10, 89 entries)
4. pre-merge (2026-03-10 20:15:30, 142 entries)
Restoring Checkpoints
Use case: Agent diverged into a bad path after a test failure. Restore to the checkpoint before the test run, revise the approach, and re-launch.
polywave-tools restore-checkpoint wave1/agent-A pre-test-run
What happens:
- Current journal state is archived (as
backup-<timestamp>.tar.gz) - Checkpoint archive is unpacked into the journal directory
context.mdis regenerated from the restored index- Agent can be re-launched with the restored context
Warning: Restoring a checkpoint does not revert code changes or git commits — it only restores the journal state. You must manually revert the worktree to the desired commit before re-launching the agent.
Archive Policy
After a wave merges successfully, journals are archived to save disk space.
Automatic Archiving
The orchestrator calls observer.Archive() after merge verification passes:
polywave-tools cleanup "<manifest-path>" --wave <N> --repo-dir "<repo-path>"
This command:
- Compresses each agent's journal directory into
.polywave-state/archives/wave<N>-agent-<ID>.tar.gz - Removes the original journal directory
- Logs archive location to stdout
Archive location: .polywave-state/archives/ in project root.
Retention Policy
Archives are retained based on polywave.config.json settings:
{
"journal": {
"retention_days": 30,
"auto_archive": true
}
}
retention_days— Archives older than this are deleted (default: 30)auto_archive— Enable/disable automatic archiving on merge (default: true)
Manual cleanup:
polywave-tools clean-archives --older-than 30
This scans .polywave-state/archives/ and deletes archives older than 30 days.
Restoring from Archive
If you need to inspect a past wave's journal after it's been archived:
polywave-tools restore-archive wave1-agent-A
This unpacks the archive back into .polywave-state/journals/wave1/agent-A/ for inspection. The original archive is preserved.
Configuration
All journal settings live in polywave.config.json (project-local) or ~/.claude/polywave.config.json (global default):
{
"journal": {
"enabled": true,
"retention_days": 30,
"auto_archive": true,
"sync_interval_seconds": 30,
"max_context_entries": 50,
"max_preview_chars": 800
}
}
Settings:
enabled— Enable/disable journal observer (default: true)retention_days— Archive retention in days (default: 30)auto_archive— Archive journals after successful merge (default: true)sync_interval_seconds— How often to sync journal from session log (default: 30)max_context_entries— Max entries to include in context.md "Recent Activity" section (default: 50)max_preview_chars— Max chars to include in index.jsonl preview field (default: 800)
Disabling journaling:
{
"journal": {
"enabled": false
}
}
When disabled, agents run without context recovery. This is not recommended for long-running waves but may be useful for debugging orchestrator issues.
See Also
- Architecture — Full system architecture with journal subsystem diagram
- Orchestrator Skill — How orchestrator loads journals before agent launch
- Protocol Execution Rules — E19 agent failure handling