Cross-Agent Session Handoff Baton
From spec-memo Wiki, the living architecture & domain knowledge base
Feature Overview [edit]
A handoff baton is a single-use, forward-looking transfer record that lets one agent session pass tactical context (next steps, failed approaches, open questions) to the next session in the same repository, even across different coding harnesses. Batons are owner/branch isolated by default so concurrent sessions or teammates cannot steal each other's context; an explicit shared flag makes a baton project-wide.
Primary journeys:
- An agent closes a session with a handoff payload via the
promptMCP tool (action: 'session_end',handoffobject) or CLImemo session endflags. - The baton is written as JSON under the project vault
.sync/handoffs/. - A later agent calls
bootstrap(orpromptaction: 'session_start'). - The matching baton is peeked, rendered at the top of the brief as
## 🤝 Active Session Handoff, and claimed exactly once. - An operator manages batons with CLI
memo session handoff,memo session handoff --all, andmemo session handoff --cancel. - An operator inspects and dismisses batons through the status monitor Active Handoffs panel.
Scope: this slice adds no MCP tool. It extends the existing prompt and bootstrap tools, plus CLI extras and status HTTP/UI.
Business Rules & Logic [edit]
Ingestion [edit]
- The
handoffpayload acceptsnextSteps(array of strings, required when a handoff is present). - Optional fields:
failedApproaches,openQuestions,branch,owner, andshared(defaultfalse). createHandofffilters empty entries and throwshandoff.nextSteps must contain at least one non-empty step.when no non-empty step remains.writeHandoffOnSessionEndreturnsundefinedfor an empty payload instead of throwing.
Owner and branch resolution [edit]
owner= payload owner, elsegit config user.email, elsegit config user.name, else OS username, elselocal.branch= payload branch, elsegit rev-parse --abbrev-ref HEAD, elseunknown.- Resolution failures fall back silently and do not crash.
Storage and superseding [edit]
- Batons live at
projects/{projectId}/.sync/handoffs/. - Non-shared files are
{encodedOwner}__{encodedBranch}.json(slashes replaced with_, percent-encoded, capped at 120 chars). - Shared batons use the single fixed filename
_shared.json. - A new baton for the same owner+branch overwrites that owner+branch file, so the newest baton wins.
- Batons for sibling branches and other owners are untouched.
- Shared batons overwrite each other independently.
Eligibility matching [edit]
- Eligible set: unclaimed, non-shared batons matching the current owner and current branch.
- Among those, the newest
createdAtis chosen. - If none match, the newest unclaimed shared baton is chosen.
- Owner/branch match takes strict precedence over shared.
- A shared baton of a different branch is still eligible for any branch.
Single-use claiming [edit]
claimHandoffre-reads the file from disk before writing.- It rejects already-claimed records.
- It rejects id-mismatched records (superseded).
- On success it writes
claimed: true,claimedAt, andclaimedBySession. - Telemetry emits
handoff_claimed.
Bootstrap delivery [edit]
bootstrapcallspeekEligibleHandoffbefore its budget pass.- It renders the baton and reserves the handoff/objective byte size as immutable before trimming traps and decisions.
- The baton is claimed only after the brief fits inside
maxBytes(default 8 KB). - Claiming happens under a vault lock.
- Claim failures degrade to a notice and the standard brief.
- Missing files return
null; malformed JSON is ignored; a malformed baton never crashesbootstrap.
Cancellation and dismissal [edit]
cancel_handoff(andmemo session handoff --cancel) deletes only the current owner+branch non-shared file.- When no such baton exists it returns
{ cancelled: false }and the CLI exits0. dismissHandoffByIddeletes an unclaimed baton matching the supplied id (used by the status panel).
Session objective [edit]
session_startmay record anobjectivestring.- Objectives are stored at
projects/{projectId}/.sync/objectives/{owner__branch}.jsonand bound to owner+branch. session_endclears the objective.session_startwithout an objective also clears any stale objective for that owner/branch.getSessionObjectivereturnsnullwhen the referenced session file no longer exists.
Telemetry and isolation invariants [edit]
createHandoffrecordsoperation: 'handoff_created'.claimHandoffrecordsoperation: 'handoff_claimed'.- Both carry
sessionId,branch,owner,shared, andhandoffIdmetadata. - A session on a different branch or under a different owner never sees or consumes a private baton.
- Only
shared: truecrosses those boundaries. - Claiming is single-use: a second attempt on the same baton fails rather than re-delivering.
Technical Architecture [edit]
Core module src/handoff.ts [edit]
createHandoff,listPendingHandoffs,matchEligibleHandoff,peekEligibleHandoff.claimHandoff,cancelHandoffForContext,dismissHandoffById,getActiveHandoffForContext.renderHandoffMarkdown,setSessionObjective,getSessionObjective,clearSessionObjective.deliverAndClaimHandoff,writeHandoffOnSessionEnd,resolveGitBranch,resolveOwner.- Shared baton filename constant
_shared.json; subdirs.sync/handoffsand.sync/objectives.
Types (src/types.ts) [edit]
HandoffPayload:nextSteps,failedApproaches,openQuestions,branch,owner,shared.HandoffRecord: addsid,harness,createdAt,sessionId,claimed,claimedAt,claimedBySession.SessionObjective:owner,branch,objective,sessionId,updatedAt.
Integration points [edit]
src/prompt.tswrites batons onendSessionRecordand archiveshandoffArchiveinto session frontmatter.src/prompt.tsclaims onstartSessionRecord.cancelHandoffRecordandshowHandoffRecordback the CLI.src/bootstrap.tspeeks, renders, budgets, and claims.
MCP surface [edit]
promptactionssession_end(withhandoff/shared),session_start(withobjective), andcancel_handoff.bootstrapreturnshandoffMarkdown,handoff, andsessionObjectivein the brief.- No new tool is added.
CLI (src/cli.ts) [edit]
memo session end <id> --handoff-steps "A,B" --handoff-failed "X" --handoff-questions "Q" [--shared]builds the payload from comma-separated flags.memo session handoffshows the active baton.memo session handoff --alllists every pending baton.memo session handoff --cancelcancels the current one.- The CLI-only actions
handoff_showandhandoff_listroute directly toshowHandoffRecordand are not part of the MCP action enum.
Status HTTP (status listener :3124) [edit]
GET /api/handoffs?project={id}→{ items, total, projectId, lastSeenRoot }.- It returns
400whenprojectis missing orall. DELETE /api/handoffs/{id}?project={id}dismisses one unclaimed baton.- Both pass through
sanitizeToolOutput.
UI [edit]
- The Active Handoffs panel (
id="handoffs-panel",id="handoffs-list") appears on the Prompts & Intent Stories tab. - It groups pending batons and offers per-baton Dismiss buttons.
- The panel is embedded on the Prompts & Intent Stories tab (
data-tab="tab-prompts"); the current?tab=deep-link handler coversbackups,wiki, andvaultsonly, so?tab=promptsdoes not auto-open it.
Provenance [edit]
0036-session-handoff-baton.spec.md.- Delivered through the
prompttool documented in Prompt History, Sessions & Activity.