Module 07
Multi-Session Handoff
On this page
In plain words
When your hostel mess shift ends, you leave a note for the next person: gas is low, rice did not come, here is the key. An agent session ends the same way, but its memory just vanishes. So at the end of every session a small script writes a handoff packet from the files the session already produced, and the next session reads it and starts working in the first minute.
How it flows
- 1Session ends
- 2Clean the workbench
- 3Generator reads artifacts
- 4Packet written
- 5Next session loads it
- 6Runs next_action
A tiny example
snapshot = load_workbench()
packet = {
"summary": snapshot.summary,
"failed_attempts": only_failures(snapshot.log),
"next_action": first_step(snapshot),
}
assert packet["next_action"]
write_md(packet)
write_json(packet)Notice the assert: without a next_action the file is only a status report, so the generator refuses to ship it.
What you will learn
- Why the end of an agent session is where most work gets lost.
- The seven things a handoff packet must carry.
- Why the packet should be generated by code, not typed by hand.
- How to leave the workbench clean so the next session can trust it.
The problem, simply
See, think about a hostel mess duty roster. Rahul does the evening shift. He knows the gas cylinder is almost empty, the rice supplier did not come, and the new cook needs the store room key. Then his shift ends and he goes to sleep.
Next morning Sneha takes over. Nobody told her anything, so she spends the first hour discovering the same three things Rahul already knew. Same waste, every day, all semester.
Agent sessions are exactly this. A session has a limited context window, which is just how much text the model can hold in its head at once. When it fills up, the session must end. The agent has run commands, hit failures, learned which approach does not work. Then the window closes and all of that is gone.
The next session opens with a blank head. It re-runs the same tests, re-asks you the same questions. Thirty minutes to recover thirty seconds of knowledge, and that cost is paid again at every session boundary for the life of the task.
The idea
The fix is a small file written at the end of every session, called a handoff packet. It is Rahul's note to Sneha, but generated by a script and in a fixed shape.
The seven fields
| Field | The question it answers |
|---|---|
summary | What was done, in one paragraph |
changed_files | What the diff touched |
commands_run | What was actually executed |
failed_attempts | What was tried and why it did not work |
open_risks | What may bite next time, with severity |
next_action | The one concrete step to start with |
verdict_pointer | Where the test and review reports live |
Remember: a packet with all six other fields but no next_action is just a status report, not a handoff. A status report informs you. A handoff makes the next session productive in its first minute.
Generate it, do not type it
If writing the handoff is a habit, it gets skipped on a bad day, exactly the day you needed it most. So make it code. The generator reads the files the workbench already produced and prints the packet.
So the agent's job is not to write a nice summary. It is to leave the workbench in a state the generator can summarize.
- 1Session runs
- 2Artifacts pile up
- 3Clean up state
- 4Generator reads
- 5Packet written
- 6Next session starts
Two forms of the same packet
Write it twice. handoff.md is for you. handoff.json is for the next agent to parse. Both come from the same source files, and if they ever disagree, JSON is the truth.
Trim the log, but not evenly
The agent may log hundreds of command runs in one session. All of that cannot go in a packet. So trim it, but unfairly: keep the last few entries plus every entry that failed (non-zero exit code).
Why unfair? Because failures are the expensive knowledge. Drop the fact that pytest -k payment failed three times on the same import error, and the next session discovers it a fourth time.
Leave a clean workbench
A perfect handoff note is worthless if the next session opens to a half-applied change, three .tmp files and tests that error before they start.
So cleanup is a separate step before the generator. Check that the tree is committed, temp files are gone, tests are green or the red one is named in open_risks, and you are on the right branch. Only then should the generator write a packet.
WarningWarning: A handoff built on a dirty tree is worse than no handoff, because the next session trusts it.
Handoff is not compaction
You will hear the word compaction: the runtime compresses old messages to fit more into the same window. That extends the current session. A handoff instead closes the session and starts a fresh one.
The common mistake is to keep compacting until quality quietly collapses. Better: wrap up at around 50 to 75 percent of your context budget, while the context is still clean. Writing the packet then is cheap. Writing it at 95 percent, when the model is already losing its place, gives you a vague packet.
Worked example
Suppose Priya is building a UPI refund flow. Her session ends and the generator produces:
- summary: added refund endpoint, two tests still failing
- failed_attempts: tried mocking the payment gateway at module level, import order broke it
- open_risks: refund amount uses float, may lose paise (severity: high)
- next_action: run
pytest tests/test_refund.py::test_partial, fix the float to integer paise - branch:
feat/upi-refund, last known good commit:a91c33
Next morning Karthik opens a fresh session, loads that JSON, and his first command is already decided.
- 1Priya ends session
- 2Packet generated
- 3Karthik loads packet
- 4Runs next_action
- 5Real work in minute one
One active packet per branch
In a team, the biggest failure is not bad model output, it is a stale packet: someone follows a two-day-old handoff and undoes fresh work.
So every packet carries branch, last_known_good_commit and a status of active, superseded or archived. Only one packet stays active per branch and topic.
Build it
"""Toy handoff generator. Standard library only."""
import json
# Pretend these came from the workbench during a session.
STATE = {
"branch": "feat/upi-refund",
"last_known_good_commit": "a91c33",
"changed_files": ["payments/refund.py", "tests/test_refund.py"],
"summary": "Added the refund endpoint. Two tests still failing.",
}
FEEDBACK = [
{"cmd": "pip install -r req.txt", "exit": 0},
{"cmd": "pytest tests/test_refund.py", "exit": 1, "note": "import order broke the mock"},
{"cmd": "black payments/", "exit": 0},
{"cmd": "pytest -k partial", "exit": 1, "note": "float rounding on paise"},
{"cmd": "git add -A", "exit": 0},
{"cmd": "git commit -m wip", "exit": 0},
]
RISKS = [{"what": "refund amount stored as float", "severity": "high"}]
def trim(log, keep_last=2):
"""Keep the last few entries PLUS every failure, in original order."""
tail = log[-keep_last:]
picked = [e for e in log if e["exit"] != 0 or e in tail]
return picked
def build_packet(state, log, risks):
fails = [e for e in log if e["exit"] != 0]
packet = dict(state)
packet["commands_run"] = trim(log)
packet["failed_attempts"] = [e["note"] for e in fails]
packet["open_risks"] = risks
packet["verdict_pointer"] = "reports/verification.json"
# next_action is never blank: derive it from the newest failure.
packet["next_action"] = (
"Re-run: " + fails[-1]["cmd"] if fails else "Open a fresh task from the board"
)
packet["status"] = "active"
return packet
def to_markdown(p):
lines = ["# Handoff", "", p["summary"], "", "## Next action", p["next_action"], "", "## Risks"]
lines += ["- %s (%s)" % (r["what"], r["severity"]) for r in p["open_risks"]]
return "\n".join(lines)
if __name__ == "__main__":
packet = build_packet(STATE, FEEDBACK, RISKS)
assert packet["next_action"], "a packet without next_action is only a status report"
print(to_markdown(packet))
print("\n--- machine-readable form ---")
print(json.dumps(packet, indent=2))
print("\nlog entries: %d -> kept in packet: %d" % (len(FEEDBACK), len(packet["commands_run"])))Look at the last line first: six commands went in, four survived. Both failures are kept even though one was early in the log. And next_action is computed, not typed, so it can never come out empty.
Where you will see this
- Claude Code and similar coding agents, which write a session summary before the window fills up.
- Cursor and other editors that keep a project notes file so a new chat starts with the project's state.
- Pull request descriptions: the same generated markdown makes a good PR body, so reviewers need not open five files.
- Customer support bots handing a ticket to a human with a "what has been tried" block.
- On-call handovers, where the outgoing engineer files what is broken and what to watch.
Common mistakes
- Writing it by hand. Feels fine on a good day, gets skipped on a bad day. Generate it from files that already exist.
- No
next_action. Then the next session must re-derive the plan, which is the exact cost you were trying to remove. - Trimming the log evenly. "Last ten entries" throws away the early failure that explains everything. Always keep every non-zero exit.
- Handing off a dirty workbench. Uncommitted changes and stray temp files make the next session clean up instead of build.
- Letting old packets stay active. Two active handoffs on one branch means two agents undoing each other. Mark old ones archived.
If they ask in an interview
Q: How do you keep an AI agent productive across many sessions when the context window is limited?
A: I end each session with a generated packet carrying summary, changed files, commands run, failed attempts, open risks, next action and a pointer to the test reports. The next session loads the JSON version and starts from a decided first step instead of rediscovering state.
Q: What is the difference between compaction and a handoff?
A: Compaction compresses the current conversation so the same session can continue. A handoff closes the session and starts a fresh one from a small written artifact. I prefer to wrap up around half to three-quarters of the context budget, because a packet written while the context is still clean is much better than one written at the wall.
Q: Multiple agents work on the same repository. How do you stop them from stepping on each other?
A: Every packet carries the branch, the last known good commit and a status of active, superseded or archived, and only one stays active per branch and topic. Stale handoffs cause more damage than weak model output, so archiving is part of the workflow.
Try these
- Add an
assumptions_to_validatefield for things the builder assumed but never tested, and see how it changes the next action. - Change
trimso a passing session keeps fewer entries than a failing one, and defend that asymmetry in two lines. - Make the generator idempotent: run it twice, get identical output. Find what is unstable, like a timestamp, and decide what to do about it.
- Add a
questions_for_the_humanlist, with your own rule for when a doubt belongs in the packet instead of the chat.
Words, simply
| Word | Meaning in simple words |
|---|---|
| Context window | How much text the model can hold in its head at one time |
| Handoff packet | The small generated file that tells the next session where things stand |
| Next action | The one concrete command or step the next session starts with |
| Status report | A summary with no next action; nice to read, not useful to resume |
| Compaction | Compressing old messages so the same session can keep going |
| Feedback trim | Keeping the last few log entries plus every failed one |
| Verdict pointer | The path to the test and review reports, so claims can be checked |
| Stale handoff | An old packet still marked active; the main multi-agent failure |
Quick recap
- A session ends, the work does not; the handoff packet is what carries the work across.
- Generate it from workbench files, never type it, and never ship it without
next_action. - Clean the workbench first, keep every failure in the trimmed log, and keep only one active packet per branch.