Hands the work to a fresh session before a long context degrades the model, and carries on there.

A Claude Code mod that hands your work to a fresh session before a long context starts degrading the model, then keeps going there on its own.
Models get worse deep into a long context: they forget early instructions, mix up files and invent details. Auto-compact steps in late and summarizes in place. context-relay acts earlier and starts clean.
turn ends at 62% ─┐
├─ 1. the model writes a handoff note from its own transcript ($.model.fork, prompt-cached)
├─ 2. the note is saved to ~/.claude/relay/<old-session-id>.md
├─ 3. /clear → new session id, empty context
└─ 4. the note is submitted as the first prompt → work continues
The handoff note always has the same sections: Goal · Done so far · Current state · Decisions and findings · Next steps · Key files. It also starts with a status line:
STATUS: CONTINUE: work is unfinished, so the new session picks up from Next steps without waiting for you.STATUS: WAIT: the task is done or needs your answer. The new session reads the note, says where things stand in one line, and waits.The old session is never lost. The handoff includes claude --resume <old-id>.
| Trigger | Default | Setting |
|---|---|---|
| A turn ends with the context at least this full | 60 % of the window | threshold |
| …or holding at least this many tokens, whatever the window size (useful on 1M-token models) | 300 000 (0 = off) | tokenCeiling |
| Mid-turn: after a tool call finishes with the context at least this full, the running turn is stopped and relayed | 80 % (0 = off) | hardThreshold |
| By hand | /relay |
Subagent turns are ignored. Only the main conversation is measured.
This is a mod, a plugin of function hooks. That's an early-access Claude Code feature (built and tested on 2.1.286).
git clone https://github.com/retrocodes12/context-relay ~/.claude/mods/context-relay
# one session:
claude --plugin-dir ~/.claude/mods/context-relay
# every session: add to ~/.claude/settings.json
# "env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/mods/context-relay" }
Change the thresholds in /config (each userConfig field shows up as a row there), or in settings.json:
"pluginConfigs": { "context-relay": { "options": { "threshold": 50, "tokenCeiling": 200000, "hardThreshold": 75 } } }
claude plugin validate . # manifest + what the module hooks and calls
claude plugin test . # tests/relay.test.ts against the engine's own test kit
To type-check, run /plugin-types in a session to write the engine's declarations into .claude/types, then npx -p typescript tsc -p ..
/clear and the prompt submit. They cover the continue path, the wait path, the token ceiling, the mid-turn stop and ignoring subagents.hardThreshold to 0 if you only want relays between turns.MIT
hooks/register.ts 152 lines1import type { EngineInterface, Register } from 'claude-code'
2
3type $ = EngineInterface
4
5type Options = { threshold?: number; tokenCeiling?: number; hardThreshold?: number }
6
7// What the old session's model is asked, over its own transcript, before it is cleared.
8const HANDOFF_PROMPT = `Your context window is filling up and this conversation is about to be cleared.
9A fresh session of you will continue the work, and the note you write now is ALL it will know.
10
11Write the handoff note. Be concrete and complete; no preamble.
12
13The FIRST line must be exactly one of:
14STATUS: CONTINUE (work is unfinished and the next step needs no input from the user)
15STATUS: WAIT (the task is done, or you are waiting on the user's answer or decision)
16
17Then these sections:
18## Goal
19What the user asked for, in their own words where it matters, and every constraint or preference they stated.
20## Done so far
21What has been completed, with file paths, commands, commits, URLs.
22## Current state
23Where things stand right now: what was in progress when you stopped, anything half-applied, what is running.
24## Decisions and findings
25Choices already made and why, dead ends already tried, facts discovered that are expensive to rediscover.
26## Next steps
27The exact next actions in order. If waiting on the user, the question they need to answer.
28## Key files
29Paths worth reading first.`
30
31type Usage = { percent?: number; tokens?: number }
32
33let relaying = false
34
35const describe = (u: Usage) =>
36 `${u.percent ?? '?'}% / ${Math.round((u.tokens ?? 0) / 1000)}k tokens`
37
38async function relay($: $, reason: string) {
39 if (relaying) return
40 relaying = true
41 try {
42 $.ui.status('relay: writing handoff…')
43 const fork = await $.model.fork({ prompt: HANDOFF_PROMPT })
44 if (!fork.isAnswered) {
45 $.ui.toast(`relay: no handoff (${fork.reason}); staying in this session`)
46 return
47 }
48
49 const note = fork.text.trim()
50 const shouldContinue = /^STATUS:\s*CONTINUE/i.test(note)
51 const body = note.replace(/^STATUS:.*\n?/i, '').trim()
52
53 const oldId = await $.session.id()
54 const home = await $.env.get('HOME')
55 const file = `${home ?? '.'}/.claude/relay/${oldId}.md`
56 await $.fs.write(file, `<!-- relayed from session ${oldId}: ${reason} -->\n${note}\n`)
57 const hops = Number((await $.store.get('hops')) ?? 0) + 1
58 await $.store.set('hops', hops)
59 await $.store.set('last', { from: oldId, file, reason, shouldContinue })
60
61 $.ui.status('relay: starting fresh session…')
62 await $.command.run({ command: 'clear', args: '' })
63
64 const handoff =
65 `[context-relay] You are continuing work from a previous session that was cleared ` +
66 `because its context was filling up (${reason}). The full previous transcript is ` +
67 `resumable with \`claude --resume ${oldId}\`; the note below is saved at ${file}.\n\n` +
68 body
69
70 await $.prompt.submit({
71 text: shouldContinue
72 ? `${handoff}\n\nContinue the work from "Next steps". Do not redo what is done.`
73 : `${handoff}\n\nDo not start any work. Reply with one short line saying where things stand, then wait for the user.`,
74 })
75 $.ui.toast(
76 shouldContinue
77 ? `relay #${hops}: fresh session, carrying on`
78 : `relay #${hops}: fresh session, handoff loaded, waiting for you`,
79 )
80 } catch (err) {
81 $.ui.toast(`relay failed: ${err instanceof Error ? err.message : String(err)}`)
82 } finally {
83 $.ui.status(undefined)
84 relaying = false
85 }
86}
87
88export const register: Register = (on, options) => {
89 const opts = (options ?? {}) as Options
90 const threshold = opts.threshold ?? 60
91 const tokenCeiling = opts.tokenCeiling ?? 300_000
92 const hardThreshold = opts.hardThreshold ?? 80
93
94 let turnId: string | undefined
95 let stoppedForRelay = false
96
97 const over = (u: Usage, percent: number) =>
98 (percent > 0 && (u.percent ?? 0) >= percent) ||
99 (tokenCeiling > 0 && (u.tokens ?? 0) >= tokenCeiling)
100
101 on('session.start', async ($, e, next) => {
102 await $.command.register({
103 name: 'relay',
104 description: 'Hand this work to a fresh session now (context-relay)',
105 })
106 return next(e)
107 })
108
109 on('command.run', { command: 'relay' }, async $ => {
110 const u = (await $.session.usage()).context
111 // A command's own dispatch is one the session waits on: relay from a timer instead.
112 $.clock.after(0, () => void relay($, `asked by hand at ${describe(u)}`))
113 return { text: `Relaying at ${describe(u)}…` }
114 })
115
116 on('turn.start', ($, e, next) => {
117 turnId = e.turnId
118 stoppedForRelay = false
119 return next(e)
120 })
121
122 // Mid-turn: a long autonomous turn can blow far past the threshold before it ends.
123 on('tool.call', async ($, e, next) => {
124 const result = await next(e)
125 if (e.agentId !== undefined || relaying || stoppedForRelay || !turnId || hardThreshold <= 0) {
126 return result
127 }
128 const u = (await $.session.usage()).context
129 if ((u.percent ?? 0) >= hardThreshold) {
130 stoppedForRelay = true
131 const id = turnId
132 $.clock.after(0, () => void $.turn.abort({ turnId: id }).catch(() => {}))
133 }
134 return result
135 })
136
137 on('turn.complete', async ($, e, next) => {
138 const result = await next(e)
139 if (e.agentId !== undefined || relaying) return result
140
141 const u = (await $.session.usage()).context
142 const reason = stoppedForRelay
143 ? `stopped mid-turn at ${describe(u)}`
144 : e.reason === 'answer' && over(u, threshold)
145 ? `turn ended at ${describe(u)}`
146 : undefined
147 stoppedForRelay = false
148 if (reason) $.clock.after(0, () => void relay($, reason))
149 return result
150 })
151}
152