/handover writes a handover note and starts (or proposes) a fresh session that continues from it; optional automatic handover past a context threshold

A small collection of Claude Code mods (plugins that hook into the session). Install only the ones you want.
| Mod | What it does |
|---|---|
| agent-dashboard | One Dashboard pane with usage and context bars, tasks, tests, sessions and subagents |
| auto-handover | /handover writes a handover note and starts (or proposes) a fresh session |
| auto-compact | Compacts past 250k tokens and saves the full transcript before every compaction |
| build-trimmer | Trims long xcodebuild / swift build output so it does not fill your context |
| prompt-upgrade | /improve rewrites a rough prompt into a clear one |
| keep-going | Stops Claude ending a turn on a vague "want me to…?": it keeps working or asks with options |
| memdb | A searchable memory database Claude can query and add to |
| cache-tax | Keeps the prompt cache warm during breaks and stops a cold send once with its price (third-party, MIT) |
| reflect-mod | Turns your corrections into CLAUDE.md rules, saved with one press (third-party, MIT) |
memdb (it also needs SQLite with FTS5, which most Python builds have) and for build-trimmer's xctrim.py.build-trimmer is for Apple development (Xcode / SwiftPM). agent-dashboard and auto-handover work best in the desktop app, where panes and new sessions are available.To get every mod and the settings this repo is used with (auto-compact at 200k, the built-in auto-compact at the same size), clone it and run the setup script:
git clone https://github.com/flyingpurplepizzaeater/claude-mods.git
cd claude-mods && ./setup.sh
It copies each mod to ~/.claude/mods/<mod> and adds three keys to the env of ~/.claude/settings.json: CLAUDE_CODE_PLUGIN_DIRS (the mods, kept alongside any plugin folders already listed), AUTO_COMPACT_THRESHOLD and CLAUDE_CODE_AUTO_COMPACT_WINDOW. Nothing else in the file changes, and the old one is kept as settings.json.bak. Name mods to install only those (./setup.sh memdb cache-tax), set AUTO_COMPACT_THRESHOLD=300k before it for another size, and run it again after git pull to update. New sessions load the mods. It needs rsync and python3, and follows CLAUDE_CONFIG_DIR if you set it.
Use either this or the marketplace install below for a mod, not both, or its hooks run twice.
At the prompt of a terminal session (swap memdb for any mod name above):
/plugin install memdb --marketplace flyingpurplepizzaeater/claude-mods
Claude asks Add marketplace?: answer y, then pick a scope (user scope is the first one). If the mod has options, a screen lets you set them. You then see Installed memdb. Plugin is now active., and its hooks run from then on, with no reload needed.
From a shell:
claude plugin install memdb --marketplace flyingpurplepizzaeater/claude-mods
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with flyingpurplepizzaeater/claude-mods, then Add plugin.
If you have already added the marketplace, claude plugin install <mod>@claude-mods is enough.
Opens one Dashboard pane at session start and stacks everything in it:
/dashboard brings the pane to the front. The pane refreshes every 5 seconds.
Options (all can be changed later in /config):
| Option | Default | Meaning |
|---|---|---|
planNote | on | Tell Claude to write long or multi-part requests into its task list first |
queueMessages | on | A message sent while Claude is working goes to the end of its task list instead of derailing it. Start the message with !now to act on it immediately |
buildReminder | off | After a long build or test run in the foreground, remind Claude to use the background next time |
/handover asks Claude to write a short handover note (goal, state, done, next steps, key files, gotchas) to ~/.claude/handovers/ and then starts a fresh session that continues from it. In the desktop app it starts the session directly (or proposes it as a card); elsewhere it leaves the "continue" prompt in the prompt box for you to send after /clear.
/handover write the note and hand over now
/handover status show whether auto mode is on, the threshold and this session's size
/handover on | off automatic handover past a context threshold (off by default)
/handover threshold 200k set the threshold (at least 1k; never above 80% of the window)
The threshold can also be set with the environment variable AUTO_HANDOVER_THRESHOLD or per project in .claude/auto-handover.json ({ "threshold": "120k" }). Notes go to $CLAUDE_CONFIG_DIR/handovers if that variable is set. No plugin options.
After each answer, if the context is past 250k tokens, it compacts the conversation, telling the summary to keep the goal, decisions, files changed and next steps. Before every compaction (your /compact, the built-in auto-compact, or its own) it saves the full conversation as markdown to ~/.claude/compact-saves/: every message and each tool call with its result, long ones clipped. Common secrets (API keys, tokens, JWTs, private keys, password: …) are redacted, and the folder is owner-only (700, files 600). A failed save never blocks the compaction. With memdb installed, the saves become searchable there.
/autocompact status on or off, the threshold, this session's size and the last save
/autocompact on | off automatic compaction (on by default; saves happen either way)
/autocompact threshold 250k set the threshold (at least 10k)
/autocompact now save and compact now
The threshold can also be set with the environment variable AUTO_COMPACT_THRESHOLD. Saves go to $CLAUDE_CONFIG_DIR/compact-saves if that variable is set. If the model's context window is no larger than the threshold, the built-in auto-compact fires first (the transcript is still saved). No plugin options.
Its own compaction runs between turns, so after one it sends a short prompt telling the session to carry on where it left off (or say in one line that it was done or waiting for you); /autocompact now doesn't. Long autonomous runs are smoother if the built-in auto-compact, which compacts mid-turn and keeps going, fires at the same size: set CLAUDE_CODE_AUTO_COMPACT_WINDOW to the threshold plus 13000 (e.g. 213000 for 200k). The mod then stays as a backup and still saves the transcript before every compaction.
Wraps xcodebuild, swift build and swift test commands that Claude runs in Bash. The full output goes to a log file and Claude sees only the errors, failing tests, the summary and the tail, plus the log path. The exit code is preserved. Logs are in ~/.claude/build-trimmer/logs and are deleted after 7 days. Add # no-trim to a command to leave it alone.
Options:
| Option | Default | Meaning |
|---|---|---|
developerDir | empty (off) | DEVELOPER_DIR to build with, e.g. /Applications/Xcode-beta.app/Contents/Developer |
developerDirPathMatch | empty | Use developerDir only when the command or working folder contains this text, e.g. /MyApp. Empty means every build |
extraBuildScripts | empty | Comma-separated script names that run xcodebuild or swift test themselves, e.g. test.sh,ci.sh |
A command that already sets DEVELOPER_DIR is never changed. If python3 is missing the full log is shown instead.
/improve <rough prompt> rewrites a rough prompt into a clear, specific one, using what the session already knows about your project, and puts it in the prompt box so you can review and edit it. Run /improve send to submit the last one. It keeps your intent and does not add goals you did not ask for. No options.
When Claude ends a turn by handing the next move back in plain text ("Want me to…?", "Should I…?", "Let me know if…"), the mod blocks that stop once and tells Claude to either keep working, routing around anything blocked, or ask properly with AskUserQuestion (2–4 options, recommendation first). A turn that already asked with AskUserQuestion, plan approval or the desktop app's wrap-up buttons is let through, and so is the second stop in a row, so it never loops. Safety confirmations (push, send, delete) still get asked, as options. The phrases it looks for are in hooks/check.ts. No options.
A searchable memory database (SQLite full-text search, ranked and stemmed) for Claude. It indexes your Claude memory files (~/.claude/projects/*/memory/*.md) automatically, and also holds:
memdb_add)memdb_index)~/.claude/compact-saves/ is indexed automatically, one entry per exchange (your prompt and the work that answered it), titled with the date and the prompt's first line and filed under the folder the session ran in. Search ranks memories and notes above docs and docs above transcripts, shows at most two old conversations by default (the best exchange of each) and says how many more matched; search with kind transcript for all of them. Deleting a save removes it; the database file is owner-only (600).Claude gets these tools: memdb_search, memdb_add, memdb_get, memdb_index and memdb_forget, and a short note in the system prompt telling it to search before rereading long notes. You get /memdb <words> to search, plus /memdb stats, /memdb list, /memdb get <name> and /memdb sync.
If Python is missing, older than 3.8, or lacks FTS5, the tools answer with a clear message instead of failing.
Options:
| Option | Default | Meaning |
|---|---|---|
python | python3 | Python command to run the script |
dbPath | empty | Database file. Empty means ~/.claude/memdb/memory.db (or under $CLAUDE_CONFIG_DIR) |
promptNote | on | Add the short "memdb is available" note to the system prompt |
The script also works on its own, with no Claude involved. After installing, find it under the plugin folder (or clone this repo):
python3 plugins/memdb/bin/memdb.py --help
python3 plugins/memdb/bin/memdb.py search "database port"
python3 plugins/memdb/bin/memdb.py add staging-port "It is 5433" -d "staging DB port"
MEMDB_PATH overrides the database location.
cache-tax by Karan Bansal (MIT), copied here at upstream commit 432a08d (2.3.0) so it installs with the rest. /keepwarm keeps the one-hour prompt cache warm through a break by sending a tiny ping 50 minutes after the last request; /keepwarm always does that for every session, for up to six hours after each turn. When the cache has gone cold, it stops the first send once and shows what the rewrite will cost. /cache-tax shows the cache state and prices. See its README.
One change from upstream: after a compaction in the middle of a turn (the built-in auto-compact), it starts pinging again as soon as the next request comes back, rather than when the turn ends. Without it, a long autonomous turn that compacted and then sat at a permission prompt or a question went cold. Two tests at the end of tests/register.test.ts cover it.
reflect-mod from claude-reflect by Bayram Annakov (MIT), copied unchanged at upstream commit b6c4232 (3.3.0). After a short prompt that corrects Claude, a model check decides whether it is a reusable rule; if so, a band above the prompt offers to save it to CLAUDE.md with one press. See its README.
/config in a terminal session (each mod's options are rows there), or reinstall and answer the options screen.claude plugin update <mod> (or claude plugin marketplace update claude-mods first).claude plugin disable <mod>; turn back on with claude plugin enable <mod>.claude plugin uninstall <mod>. To drop the marketplace too: claude plugin marketplace remove claude-mods.~/.claude/memdb/; delete that folder if you want the data gone. auto-handover notes stay in ~/.claude/handovers/, auto-compact saves in ~/.claude/compact-saves/ and build-trimmer logs in ~/.claude/build-trimmer/logs/.Each mod is a folder under plugins/. Check one with:
claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>
claude plugin validate . # the marketplace file
MIT licensed, see LICENSE.
hooks/register.tsx 198 lines1import { atom, read, update } from 'claude-code'
2import type { Engine, Register } from 'claude-code'
3
4import {
5 DEFAULT_THRESHOLD,
6 HANDOVER_PROMPT,
7 PROJECT_FILE,
8 SESSION_SERVER,
9 continuePrompt,
10 effectiveThreshold,
11 fmtTokens,
12 goalOf,
13 handoverDir,
14 handoverFile,
15 handoverName,
16 logFile,
17 parseTokens,
18} from './handover'
19
20// The handover this session already wrote, so the automatic one (when on) fires once per session.
21const handedOver = atom({ plugin: 'auto-handover', key: 'handedOver' } as const, '')
22// Main-loop turns this session has finished, counted here (prompts from the app or other sessions included).
23const turnsSeen = atom({ plugin: 'auto-handover', key: 'turnsSeen' } as const, 0)
24
25const USAGE = '/handover [now | status | on | off | threshold <200k>]'
26
27// First that is set: AUTO_HANDOVER_THRESHOLD, the project's .claude/auto-handover.json
28// ({ "threshold": "120k" }), /handover threshold, then 200k.
29const getThreshold = async ($: Engine) => {
30 const fromEnv = parseTokens((await $.env.get('AUTO_HANDOVER_THRESHOLD')) ?? '')
31 if (fromEnv !== undefined) return fromEnv
32 const projectFile = `${await $.session.root()}/${PROJECT_FILE}`
33 if (await $.fs.exists(projectFile).catch(() => false)) {
34 const raw = await $.fs.read(projectFile).catch(() => '')
35 const fromProject = parseTokens(String(safeJson(raw)?.threshold ?? ''))
36 if (fromProject !== undefined) return fromProject
37 }
38 const v = await $.store.get('threshold')
39 return typeof v === 'number' && v >= 1000 ? v : DEFAULT_THRESHOLD
40}
41// Off unless /handover on: built-in auto-compact (CLAUDE_CODE_AUTO_COMPACT_WINDOW) covers the automatic case.
42const isEnabled = async ($: Engine) => (await $.store.get('enabled')) === true
43
44type Outcome = { ok: boolean; text: string }
45
46const safeJson = (raw: string): { threshold?: unknown } | undefined => {
47 try {
48 return JSON.parse(raw)
49 } catch {
50 return undefined
51 }
52}
53
54// Where handover notes go: <config dir>/handovers.
55const getHandoverDir = async ($: Engine) =>
56 handoverDir(
57 (await $.env.get('HOME').catch(() => undefined)) ?? '',
58 await $.env.get('CLAUDE_CONFIG_DIR').catch(() => undefined),
59 )
60
61// One line per step in ~/.claude/handovers/auto-handover.log, newest last, last 200 kept.
62async function logLine($: Engine, text: string) {
63 const at = new Date(await $.clock.now()).toISOString()
64 const log = logFile(await getHandoverDir($))
65 const old = await $.fs.read(log).catch(() => '')
66 const lines = `${old}${at} ${text.replace(/\s+/g, ' ').slice(0, 600)}\n`.split('\n').filter(Boolean).slice(-200)
67 await $.fs.write(log, `${lines.join('\n')}\n`).catch(() => undefined)
68}
69
70// The session server can wait on the app; give up after a minute so the fallback still runs.
71const withTimeout = <T,>($: Engine, p: Promise<T>, ms: number, fallback: T) =>
72 Promise.race([p, $.clock.sleep(ms).then(() => fallback)])
73
74// Writes the handover file, then starts a fresh desktop session on it; where there is no
75// desktop app, leaves the continue prompt in the box to send after /clear.
76async function handover($: Engine, why: string): Promise<Outcome> {
77 const r = await $.model.fork({ prompt: HANDOVER_PROMPT })
78 if (!r.isAnswered) return { ok: false, text: `Handover failed: could not write the note (${r.reason}).` }
79 const note = r.text.trim()
80
81 const [sessionId, cwd, now, use] = await Promise.all([
82 $.session.id(),
83 $.session.cwd(),
84 $.clock.now(),
85 $.session.usage().catch(() => undefined),
86 ])
87 const path = `${await getHandoverDir($)}/${handoverName(now, sessionId)}`
88 await $.fs.write(path, handoverFile({ note, sessionId, cwd, tokens: use?.context.tokens, now, why }))
89 await update($, handedOver, () => path)
90 await logLine($, `${sessionId} wrote ${path} (${why})`)
91
92 const prompt = continuePrompt(path, cwd)
93 // The app starts a session outright only on the person's own words; a turn another session or an
94 // event drove is refused, and then the same session is proposed as a card to start with one click.
95 const startSession = (initiation: 'user_asked' | 'own_initiative') =>
96 withTimeout(
97 $,
98 $.mcp
99 .call(SESSION_SERVER, 'start_session', {
100 initiation,
101 context: 'fresh',
102 title: `Continue: ${goalOf(note)}`,
103 prompt,
104 background: `Handover from session ${sessionId} (${why}). The full note is in ${path}:\n\n${note}`,
105 use_worktree: false,
106 })
107 .catch((err: unknown) => ({ isError: true, content: [{ type: 'text', text: String(err) }] })),
108 60_000,
109 { isError: true, content: [{ type: 'text', text: 'no answer from the session server in 60s' }] },
110 )
111 const textOf = (r: { content: readonly unknown[] }) =>
112 r.content.map(b => (b && typeof b === 'object' && 'text' in b && typeof b.text === 'string' ? b.text : '')).join(' ').trim()
113
114 const started = await startSession('user_asked')
115 const said = textOf(started)
116 await logLine($, `${sessionId} start_session ${started.isError ? 'failed' : 'ok'}: ${said}`)
117 if (!started.isError) {
118 $.ui.toast('Handed over to a new session: carry on there.')
119 return { ok: true, text: `Handover written to ${path} and a new session started from it. Carry on there; this one can be archived.${said ? `\n${said.slice(0, 300)}` : ''}` }
120 }
121
122 const proposed = await startSession('own_initiative')
123 const proposedSaid = textOf(proposed)
124 await logLine($, `${sessionId} propose ${proposed.isError ? 'failed' : 'ok'}: ${proposedSaid}`)
125 if (!proposed.isError) {
126 $.ui.toast('Handover written: start the "Continue" session card to carry on fresh.')
127 return { ok: true, text: `Handover written to ${path}. The app would not start a session from this turn, so a "Continue: …" session card is waiting: start it to carry on fresh.` }
128 }
129
130 const filled = await $.prompt.fill({ text: prompt }).catch(() => ({ isFilled: false }))
131 $.ui.toast('Handover written: run /clear, then send the prompt in the box.')
132 return {
133 ok: true,
134 text: `Handover written to ${path}. No new session could be started here (${said.slice(0, 200) || 'no desktop app'}).${
135 filled.isFilled ? ' The continue prompt is in the prompt box: run /clear, then send it.' : ` Run /clear, then send:\n\n${prompt}`
136 }`,
137 }
138}
139
140export const register: Register = on => {
141 on('session.start', async ($, e, next) => {
142 await $.command
143 .register({ name: 'handover', description: 'Write a handover file and continue in a fresh session', argumentHint: '[now|status|on|off|threshold 200k]' })
144 .catch(() => undefined)
145 return next(e)
146 })
147
148 // At the end of each main-loop turn: past the threshold, hand over once, after the turn has let go.
149 on('turn.complete', async ($, e, next) => {
150 const done = await next(e)
151 if (e.agentId !== undefined || e.reason !== 'answer') return done
152 const seen = (await read($, turnsSeen)) + 1
153 await update($, turnsSeen, () => seen)
154 if ((await read($, handedOver)) || !(await isEnabled($))) return done
155 const use = await $.session.usage().catch(() => undefined)
156 const tokens = use?.context.tokens
157 const limit = effectiveThreshold(await getThreshold($), use?.context.window)
158 if (tokens === undefined || tokens < limit) return done
159 // Never on a session's first turn, so a session that just took over cannot hand straight on.
160 if (seen < 2) return done
161
162 await update($, handedOver, () => 'pending')
163 $.clock.after(1500, async () => {
164 const out = await handover($, `context past ${fmtTokens(limit)}`).catch((err: unknown) => ({ ok: false, text: `Handover failed: ${String(err)}` }))
165 if (!out.ok) await update($, handedOver, () => '')
166 await logLine($, out.text)
167 $.ui.log(`[auto-handover] ${out.text}`)
168 })
169 return done
170 }).catch(($, e, next) => next(e)) // never break a turn over a handover glitch
171
172 on('command.run', { command: 'handover' }, async ($, e) => {
173 const [verb = 'now', ...rest] = e.args.trim().toLowerCase().split(/\s+/).filter(Boolean)
174 if (verb === 'now') return { text: (await handover($, 'asked with /handover')).text }
175 if (verb === 'on' || verb === 'off') {
176 await $.store.set('enabled', verb === 'on')
177 if (verb === 'on') await update($, handedOver, () => '')
178 return { text: `Automatic handover ${verb === 'on' ? 'on' : 'off'}.` }
179 }
180 if (verb === 'threshold') {
181 const n = parseTokens(rest.join(''))
182 if (n === undefined) return { text: `Give a token count, e.g. /handover threshold 200k (at least 1k).` }
183 await $.store.set('threshold', n)
184 return { text: `Automatic handover now fires past ${fmtTokens(n)} tokens.` }
185 }
186 if (verb === 'status') {
187 const [on_, limit, use, done] = [await isEnabled($), await getThreshold($), await $.session.usage().catch(() => undefined), await read($, handedOver)]
188 const eff = effectiveThreshold(limit, use?.context.window)
189 return {
190 text: `Automatic handover ${on_ ? 'on' : 'off'}; fires past ${fmtTokens(eff)} tokens${eff < limit ? ` (80% of the ${fmtTokens(use!.context.window)} window; set ${fmtTokens(limit)})` : ''}. This session: ${
191 use?.context.tokens !== undefined ? `${fmtTokens(use.context.tokens)} tokens` : 'no count yet'
192 }${done ? `, already handed over (${done})` : ''}. Files in ${await getHandoverDir($)}.`,
193 }
194 }
195 return { text: `Usage: ${USAGE}` }
196 }).catch(($, e, next) => next(e))
197}
198hooks/handover.ts 60 lines1// Pure helpers for the auto-handover mod.
2
3export const DEFAULT_THRESHOLD = 200_000
4// Handover notes live in <config dir>/handovers: CLAUDE_CONFIG_DIR when set, else ~/.claude.
5export const handoverDir = (home: string, configDir?: string) =>
6 `${(configDir || `${home.replace(/\/+$/, '')}/.claude`).replace(/\/+$/, '')}/handovers`
7export const logFile = (dir: string) => `${dir}/auto-handover.log`
8// Per-project override of the threshold, relative to the session's project root.
9export const PROJECT_FILE = '.claude/auto-handover.json'
10// The desktop app's own session server, the one behind mcp__ccd_session__start_session.
11export const SESSION_SERVER = 'ccd_session'
12
13// Asked of a fork of the session (its cached transcript), so it costs one short answer.
14export const HANDOVER_PROMPT = `Write a handover note so a brand-new Claude Code session, with none of this conversation, can pick up exactly where this one leaves off. Plain markdown, under 350 words, these headings in order:
15## Goal (one or two lines)
16## Current state (what works, what is half-done, anything left running or uncommitted)
17## Done
18## Next steps (numbered, the very next action first)
19## Key files and paths (full paths)
20## Decisions and gotchas (including anything the user asked for or ruled out)
21Only facts from this session; no preamble, no sign-off, no tool calls. Reply with the note alone.`
22
23// '200k', '1.5m', '50000' → tokens; undefined when it does not parse.
24export const parseTokens = (s: string): number | undefined => {
25 const m = /^\s*(\d+(?:\.\d+)?)\s*([km]?)\s*$/i.exec(s)
26 if (!m) return undefined
27 const n = Number(m[1]) * (m[2].toLowerCase() === 'k' ? 1e3 : m[2].toLowerCase() === 'm' ? 1e6 : 1)
28 return Number.isFinite(n) && n >= 1000 ? Math.round(n) : undefined
29}
30
31export const fmtTokens = (n: number) => (n >= 1e6 ? `${+(n / 1e6).toFixed(2)}m` : `${Math.round(n / 1000)}k`)
32
33// The threshold that applies: the set one, but never past 80% of the window, so it fires before auto-compaction would.
34export const effectiveThreshold = (threshold: number, window?: number) =>
35 window && window > 0 ? Math.min(threshold, Math.floor(window * 0.8)) : threshold
36
37// '2026-10-17-1432-ab12cd34.md'
38export const handoverName = (now: number, sessionId: string) => {
39 const d = new Date(now)
40 const p = (n: number) => String(n).padStart(2, '0')
41 return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}-${sessionId.slice(0, 8)}.md`
42}
43
44export const handoverFile = (o: { note: string; sessionId: string; cwd: string; tokens?: number; now: number; why: string }) =>
45 `# Handover\n\n- From session: ${o.sessionId}\n- Project folder: ${o.cwd}\n- Written: ${new Date(o.now).toISOString()} (${o.why}${o.tokens ? `, context ${fmtTokens(o.tokens)} tokens` : ''})\n\n${o.note.trim()}\n`
46
47// The goal line of the note, for the new session's title.
48export const goalOf = (note: string) => {
49 const lines = note.split('\n').map(l => l.trim())
50 const at = lines.findIndex(l => /^#*\s*\**goal\**/i.test(l))
51 const after = at < 0 ? lines : lines.slice(at + 1)
52 const inline = at >= 0 ? lines[at].replace(/^#*\s*\**goal\**:?\**\s*/i, '').trim() : ''
53 const goal = inline || after.find(l => l && !l.startsWith('#')) || 'previous session'
54 const clean = goal.replace(/[*_`#]/g, '').replace(/\s+/g, ' ').trim()
55 return clean.length > 48 ? `${clean.slice(0, 47)}…` : clean
56}
57
58export const continuePrompt = (path: string, cwd: string) =>
59 `Continue the work of a previous session that was handed over to keep context small. First read the handover file ${path} (it is outside the project, read it by that absolute path). The project folder is ${cwd}. Then carry on from its "Next steps", without redoing what is listed as done; check the current state of files before editing, since the note may lag slightly. Rebuild your task list from the note.`
60types/index.d.ts 13 lines1declare module 'claude-code' {
2 interface PluginState {
3 'auto-handover': {
4 // Path of the handover this session already wrote ('' none yet), so it fires once.
5 handedOver: string
6 // Main-loop turns finished this session.
7 turnsSeen: number
8 }
9 }
10}
11
12export type HandoverPath = string
13