pi-handoff-chain1.2.0
Kill & Handoff protocol for pi — checkpointed context resets, watermark-enforced landing, and fresh-session task chaining for multi-agent workflows
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-handoff-chain
Kill & Handoff protocol for pi — checkpointed handoffs, watermark-enforced landing, and fresh-session task chaining for solo developers running multi-agent workflows.
The problem
LLM context windows are finite, expensive, and degrade as they fill up. Long-lived agent sessions rot: you pay to re-send stale noise, and when the window finally fills, the session dies mid-task with nothing written down.
The protocol
The unit of work is one session-sized task, not a whole project. Each agent:
- Saves at milestones — and resets —
checkpointwrites an in-progressHANDOFF.mdand clears the context that produced it (a save and a restart, not a log line) - Gets force-landed at the watermark — a watchdog monitors context usage every turn; past the threshold it injects a mandatory "stop new work, land the plane" directive
- Ends on a commit, not a conversation —
finishwrites the final handoff document and returnsterminate: true, so the session stops immediately with zero "victory lap" tokens - Chains to a fresh brain —
/nextstarts a brand-new session seeded with the handoff document, with zero memory of the predecessor
Files are the only reliable memory. Context is disposable.
Context reset engine
A checkpoint that only writes a file is a checkpoint that never gets used. If the context survives the write, the agent keeps dragging every dead end, stale file read, and abandoned plan into the next mile of the task — and when it eventually re-reads HANDOFF.md, the fresh document has to compete with a screenful of contradicting noise. So checkpoint is a context reset point: the snapshot must actually replace the context.
checkpoint takes reset:
| reset | what happens | when it happens |
|---|---|---|
"compact" (default) |
This session's context is cleared and replaced by the document you just wrote. Then the agent is nudged to keep working from it. | at turn_end, after the tool result is persisted |
"session" |
Hard cut: a brand-new session (ctx.newSession()) seeded with the document; the same task continues there with zero inherited context. |
at agent_settled, via /cut |
"none" |
Only the file is written — the old behaviour. | never |
The compact reset is free: instead of paying an LLM to summarize the old context, the extension answers pi's session_before_compact event with the handoff document itself as the compaction summary. Nothing is summarized, nothing is invented — the document is the new context, with pi's recent-entry boundary kept on top of it so the immediate working set survives.
reset: "session" exists for when even the kept tail is too dirty: same task, brand-new brain, parentSession recorded for traceability. Same machinery as /next, different intent — /next chains to the next task, /cut wipes and continues this one.
Timing (measured, not assumed)
Getting the reset to actually land took three fixes worth knowing about:
- Never reset from inside the tool's
execute.ctx.compact()aborts the current run first; calling it while the tool batch is still live takes the tool's own result down with it. The reset is armed inexecuteand fired atturn_end, when results are already on disk. - The compaction pipeline runs after
agent_settled. A reset that reads its document out of the same state that the settle handler clears arrives atsession_before_compactwith nothing to inject — silently falling back to pi's default lossy summary. The document is parked in its own variable (resetSummary) that only the compaction handlers consume. - You cannot send a message while compaction is in flight.
sendUserMessagethrowsCannot submit a prompt while compaction is in progress, so the post-reset resume (and the auto-/cutdispatch) waits on awhenIdle()poll instead of asetTimeout(0).
A circuit breaker (HANDOFF_MAX_RESETS, default 12) stops an agent that checkpoints every turn from ping-ponging reset → resume → reset forever.
In headless mode (pi -p) the reset is skipped by design: one process per turn already is a context boundary.
The switch: enable_handoff
One key in pi's settings turns the whole protocol off without uninstalling anything.
| priority | source | example |
|---|---|---|
| 1 (highest) | HANDOFF_ENABLED env var |
HANDOFF_ENABLED=false pi -p "..." — one run |
| 2 | project .pi/settings.json |
{ "enable_handoff": false } |
| 3 | global ~/.pi/agent/settings.json |
{ "enable_handoff": true } |
| 4 | default | true (key absent) |
Values are tolerant of hand-editing: true/false, "on"/"off", "yes"/"no", "1"/"0".
pi exposes no settings API to extensions, so the extension reads and merges those files itself, and writes with a read-modify-write that leaves every other key in the file intact.
Off means absent, not "registered but refusing":
finishandcheckpointare dropped from the active tool set — the model never sees them, and theirpromptSnippet/promptGuidelinestext disappears from the system prompt with them- every event handler early-returns: no context resets, no watermark steering, no auto-
/cut /nextand/cutrefuse to chain
Drive it from inside pi:
/handoff # show HANDOFF.md (unchanged)
/handoff status # resolved value + which layer is winning, per source
/handoff off # write enable_handoff:false → global settings
/handoff on project # write it to ./.pi/settings.json instead
Verified behaviour: with the key false, the model lists only read, bash, edit, write — no handoff tools — and /handoff off → /handoff on project flips it live while preserving unrelated keys such as theme.
The switch is re-read at session_start, at agent_start, and immediately after /handoff on|off. Editing the file by hand takes effect on the next run; /handoff status tells you which layer won.
Install
pi install npm:pi-handoff-chain
Or straight from git:
pi install git:github.com/martintsan/pi-handoff-chain
Verify: start a new pi session — finish, checkpoint tools and /next, /cut, /handoff commands are available in every project. /handoff status confirms the resolved switch value and its source.
Kill switch without uninstalling: "enable_handoff": false — see The switch.
Remove:
pi remove git:github.com/martintsan/pi-handoff-chain
What you get
finish tool (terminal)
Call as the final action of a task. Writes a structured HANDOFF.md (status / summary / decisions / artifacts / next task / blockers) to the project root, then terminates the session immediately via terminate: true.
Three legal ways to land:
| status | meaning |
|---|---|
done |
task contract fulfilled |
partial |
watermark hit or time's up — remainder fully described in next_task |
blocked |
needs a human — the chain stops and waits |
checkpoint tool (non-terminal, context-resetting)
Same document format, no termination. Call after every meaningful milestone on long tasks, before and after risky operations. If the session dies at any instant, the on-disk handoff is at most one step stale — and if it survives, the context that produced the milestone gets replaced by it. reset: "compact" | "session" | "none", default compact (see Context reset engine).
Watermark watchdog
On every turn_end, current context usage is checked (ctx.getContextUsage()). At or above 60% of the model's window, the extension steers a directive into the conversation:
STOP starting new work. Commit finished artifacts. Call
finishwithpartialand a self-containednext_task. Do not ask questions; land the plane.
Tune with HANDOFF_WATERMARK (e.g. 0.7). Fires once per agent run.
/next command
Reads HANDOFF.md, asks for human approval, then ctx.newSession():
- records the previous session as parent (traceability)
- injects the handoff document as the new session's initial context
- sends a kickoff prompt ("you are a fresh agent with zero memory; verify artifacts; execute Next task; end with
finish")
Pass custom kickoff text: /next focus on the failing tests first
/cut command
Hard context reset for the same task: wipes the context, starts a fresh session seeded with HANDOFF.md, and continues from the checkpoint instead of moving to a next task. Ask for it with /cut, or get it automatically from checkpoint(reset: "session") — the automatic path skips the confirmation prompt because you already asked for it.
/handoff command
Preview the current HANDOFF.md.
Headless mode (optional driver)
scripts/agent-chain.sh runs the whole chain unattended:
./scripts/agent-chain.sh TASK.md
Each turn is a fresh pi -p process (clean context). The agent must finish → process exits → driver extracts the next task → next process. Protections:
- Recovery spell: if a turn exits without a handoff, the driver resumes (
pi -c) and instructs the agent to reconstruct the handoff from the (possibly compacted) session before stopping - Circuit breaker:
MAX_TURNS(default 10) caps runaway chains - Blocked detection:
blockedstatus halts the chain for human review
Task-writing discipline (the actual skill)
This plugin makes handoffs mechanical; it cannot make tasks small enough. Before starting a task, ensure:
- Single artifact — one identifiable deliverable per session
- Machine-verifiable — a validator can judge it (tests pass, schema passes, count met)
- Context budget ≤ 50% — leave headroom
"Complete the project" is not a task. A project is a DAG of session-sized tickets whose state lives on disk. Decompose first, or the chain will stall.
Configuration
| env var | default | meaning |
|---|---|---|
HANDOFF_ENABLED |
(unset) | override enable_handoff for this process (true/false) |
HANDOFF_WATERMARK |
0.6 |
context usage fraction that triggers forced landing |
HANDOFF_CHECKPOINT_RESET |
compact |
default reset mode for checkpoint (compact / session / none) |
HANDOFF_MAX_RESETS |
12 |
per-session cap on automatic resets (anti ping-pong breaker) |
MAX_TURNS |
10 |
(driver script) circuit-breaker cap |
LOG_DIR |
.chain-logs |
(driver script) per-turn log directory |
Hacking
./scripts/typecheck.sh # symlink the globally installed pi package (or use the npm peer dep) + tsc --noEmit
The reset engine's behaviour depends on pi event ordering (turn_end → agent_settled → compaction), which is documented above and in the source comments; change it with a sandbox RPC session before trusting it.
Publishing
The package already carries what the pi ecosystem needs: the pi-package keyword (which is what the gallery indexes) and a pi.extensions manifest. publishConfig.access is public and prepublishOnly runs the typecheck, so a publish is one command:
npm login # interactive, one-time
npm publish # runs ./scripts/typecheck.sh first, then publishes pi-handoff-chain@<version>
Verified prerequisites: the name pi-handoff-chain is unclaimed on the registry, all three peer deps resolve publicly (@earendil-works/pi-coding-agent, @earendil-works/pi-ai, typebox), and the tarball ships only extensions/, scripts/, tsconfig.check.json, README.md, LICENSE.
Gallery listing is automatic — pi.dev/packages displays npm packages tagged pi-package, so there is nothing to submit separately. For a preview in the gallery card, add pi.image (PNG/JPEG/GIF/WebP) or pi.video (MP4) to the pi block in package.json; both need a hosted URL.
Provenance (CI only)
npm provenance cannot be generated from a local machine — per npm's docs it requires a supported cloud CI provider on a cloud-hosted runner (GitHub Actions or GitLab CI/CD). For that route, publish from GitHub Actions with permissions: { id-token: write } and npm publish --provenance --access public (or npm trusted publishing, which adds provenance without the flag). Deliberately not set in publishConfig: provenance: true there would make every local npm publish fail.
Credits
Protocol pattern inspired by the solo-founder multi-agent workflow behind HAN•GL (kill the session after the artifact lands; hand off through documents; the human keeps the approval gates).
License
MIT