A Claude Code plugin that persists structured agent state across compaction events.
When Claude Code's context window fills up and compaction wipes the conversation, relay keeps track of what the agent was doing — its objective, working files, hypothesis, next actions, and open questions. On resume, that context is injected back automatically so work can continue without the agent losing its place.
Relay uses Claude Code's hook system to run Python scripts at key lifecycle points. No agent cooperation is required for basic tracking — hooks fire automatically on file operations and turn boundaries.
Write/Edit/Read (PostToolUse) Stop/SubagentStop (pack.py)
│ │
▼ ▼
Register artifact Rebuild INDEX.md, EXEC_PACKET.md
Track reads Append to TASKLOG.md
Update working set Parse markers, infer objective
Spawn async extraction
Check staleness, warn on compaction
PreCompact (pre_compact.py) SessionStart (reload.py, nudge.py)
│ │
▼ ▼
Snapshot state Post-compaction: inject RELOAD.md
Write RELOAD.md Non-compact: nudge if stale or
Record compaction history missing objective, surface
deferred notifications
Automatically (no agent action needed):
- File artifacts — every Write, Edit, and Read is registered with provenance
- Working set — the 10 most recent artifacts, used for RELOAD.md
- Turn count, compaction count, compaction history
- Git context — branch, status, recent commits
- Read tracking — last 5 unique file reads
Semi-automatically (via markers in assistant output):
- Objective, hypothesis, next actions, open questions — parsed from
<!-- relay:field value -->HTML comments at the end of responses - Done signal —
<!-- relay:done -->clears next_actions and sets status to idle
Background (async LLM extraction):
- Every 5 marker-free turns, a background
claude --print --model haikucall infers state changes from the assistant's message - Results auto-apply on the next Stop hook (prefixed with
[auto]for traceability)
All state lives in .agent-workspace/ at the project root:
.agent-workspace/
├── STATE.json # Source of truth — all structured state
├── EXEC_PACKET.md # Narrative summary for the agent
├── INDEX.md # Full artifact registry table
├── TASKLOG.md # Append-only turn history
├── artifacts/ # Reserved for artifact metadata
├── summaries/ # Pre-compaction snapshots
├── derived/
│ ├── RELOAD.md # Post-compaction injection payload
│ ├── file_snapshots.md # First lines of working set files
│ ├── extraction_context.json # Input for background LLM extraction
│ ├── pending_suggestions.json # Output from background extraction
│ └── notifications.txt # Deferred notifications for next SessionStart
└── errors.log # Hook error log (capped at 50 entries)
Deploy to the Claude Code plugin cache:
bash scripts/deploy.shThis rsyncs the source to ~/.claude/plugins/cache/ryo-marketplace/relay/1.0.0/, excluding .git, .agent-workspace, __pycache__, and docs.
All commands are accessed via /relay:
| Command | Description |
|---|---|
/relay or /relay status |
Show current workspace state |
/relay sync |
Update all semantic fields (objective, status, hypothesis, next_actions, open_questions) |
/relay pack |
Manually trigger a pack cycle |
/relay objective <text> |
Set or show the workspace objective |
/relay forget <path-or-id> |
Remove an artifact from the registry and working set |
/relay reset |
Delete .agent-workspace/ and start fresh |
Defined in hooks/hooks.json:
| Event | Script | What it does |
|---|---|---|
PostToolUse (Write/Edit/Read) |
post_tool_use.py |
Registers artifacts, tracks reads, manages working set |
Stop |
pack.py |
Rebuilds derived files, parses markers, spawns extraction, checks staleness |
SubagentStop |
pack.py |
Same as Stop but skips marker parsing, extraction, and blocking |
PreCompact |
pre_compact.py |
Snapshots state, writes RELOAD.md, records compaction history |
SessionStart (compact) |
reload.py |
Injects RELOAD.md as additional context after compaction |
SessionStart (*) |
nudge.py |
Nudges on missing objective or stale state, surfaces deferred notifications |
Two layers keep workspace state fresh without the agent running /relay sync every few turns.
The agent drops HTML comments anywhere in its response:
<!-- relay:objective fix the OAuth redirect loop -->
<!-- relay:next add PKCE support -->
<!-- relay:next write integration tests -->
<!-- relay:question should we support refresh token rotation? -->
<!-- relay:hypothesis the 401s are caused by clock skew -->
<!-- relay:done -->
The Stop hook parses these from last_assistant_message and applies them to workspace state. Values are tagged [auto] to distinguish from manual updates.
Limitation: markers must be at the end of the response, after all tool calls. last_assistant_message only contains the final text block — anything before a tool call gets cut off.
When 5+ turns pass without markers, the Stop hook spawns extract_state.py in the background. It calls claude --print --model haiku with isolation flags (--setting-sources local, --strict-mcp-config, --dangerously-skip-permissions) to infer state changes from the assistant's message.
Results are written to derived/pending_suggestions.json and auto-applied on the next Stop hook. All auto-applied values are prefixed with [auto] for traceability. Suggestions expire after 3 turns if not applied. When markers are present in the same turn, they take priority and suggestions are skipped.
Priority: markers > manual /relay sync > LLM suggestions.
The core value of relay. When compaction hits:
- PreCompact fires — snapshots current state, writes
derived/RELOAD.mdwith objective, hypothesis, working set, recent history, git context, file snapshots, and a recovery checklist - Context is wiped — the conversation loses everything
- SessionStart (compact) fires —
reload.pyreadsRELOAD.mdand injects it asadditionalContext, so the agent immediately has its bearings
The recovery checklist in RELOAD.md tells the agent to:
- Read
TASKLOG.mdfor recent history - Read
file_snapshots.mdfor code context - Run
/relay syncto verify and update state - Read
STATE.jsononly if full artifact details are needed
Relay tracks how many turns have passed since the last semantic update:
- Fresh (0-5 turns): no warnings
- Mild (6-15 turns): gentle nudge
- Loud (16+ turns): strong warning
- Periodic reminder: every 8 turns of staleness
The SessionStart hook also nudges if the workspace has artifacts but no objective, or if state is stale from a previous session.
146 pytest tests covering all hook scripts:
python3 -m pytest hooks/scripts/test_hooks.py -qCovers: artifact registration, working set management, index/exec_packet rebuilding, TASKLOG trimming, staleness detection, compaction prediction, marker parsing/application, suggestion TTL, extraction spawning, nudge logic, reload injection, git context capture, error logging, and more.
Detailed design rationale for each feature set:
docs/plans/2026-02-27-feat-baton-plugin-plan.md— original plugin designdocs/plans/2026-02-28-guardrails-backlog-design.md— sync guardrails and richer reloaddocs/plans/2026-03-01-relay-v2-design.md— read tracking, git awareness, auto objective, compaction predictiondocs/plans/2026-03-01-seamless-state-tracking.md— markers and async LLM extraction
- Markers must be at end of response.
last_assistant_messageonly contains the final text block. Markers before tool calls are lost. - SubagentStop carries subagent's message. All marker/extraction logic is gated to main Stop events only.
- Stop hooks can't inject additionalContext. Only
decision/reason/systemMessageare supported. Non-critical notifications are deferred to file and surfaced on next SessionStart. - Async extraction requires
claudeCLI. Ifclaudeisn't installed or can't run, extraction silently skips. The 30s timeout handles hangs.