- Home
- Documentation
- sessions
- Handoff Generation Pipeline
Handoff Generation Pipeline
/handoff generation pipeline
Section titled “/handoff generation pipeline”This document describes how the coding-agent implements /handoff today: trigger path, generation prompt, completion capture, session switch, and context reinjection.
Covers:
- Interactive
/handoffcommand dispatch AgentSession.handoff()lifecycle and state transitions- How handoff output is captured from assistant output
- How old/new sessions persist handoff data differently
- UI behavior for success, cancel, and failure
Does not cover:
- Generic tree navigation/branch internals
- Non-handoff session commands (
/new,/fork,/resume)
Implementation files
Section titled “Implementation files”../src/modes/controllers/input-controller.ts../src/modes/controllers/command-controller.ts../src/session/agent-session.ts../src/session/session-manager.ts../src/extensibility/slash-commands.ts
Trigger path
Section titled “Trigger path”/handoffis declared in builtin slash command metadata (slash-commands.ts) with optional inline hint:[focus instructions].- In interactive input handling (
InputController), submit text matching/handoffor/handoff ...is intercepted before normal prompt submission. - The editor is cleared and
handleHandoffCommand(customInstructions?)is called. CommandController.handleHandoffCommandperforms a preflight guard using current entries:- Counts
type === "message"entries. - If
< 2, it warns:Nothing to hand off (no messages yet)and returns.
- Counts
The same minimum-content guard exists again inside AgentSession.handoff() and throws if violated. This duplicates safety at both UI and session layers.
End-to-end lifecycle
Section titled “End-to-end lifecycle”1) Start handoff generation
Section titled “1) Start handoff generation”AgentSession.handoff(customInstructions?):
- Reads current branch entries (
sessionManager.getBranch()) - Validates minimum message count (
>= 2) - Creates
#handoffAbortController - Builds a fixed, inline prompt requesting a structured handoff document (
Goal,Constraints & Preferences,Progress,Key Decisions,Critical Context,Next Steps) - Appends
Additional focus: ...if custom instructions are provided
Prompt is sent via:
await this.prompt(handoffPrompt, { expandPromptTemplates: false });expandPromptTemplates: false prevents slash/prompt-template expansion of this internal instruction payload.
2) Capture completion
Section titled “2) Capture completion”Before sending prompt, handoff() subscribes to session events and waits for agent_end.
On agent_end, it extracts handoff text from agent state by scanning backward for the most recent assistant message, then concatenating all content blocks where type === "text" with \n.
Important extraction assumptions:
- Only text blocks are used; non-text content is ignored.
- It assumes the latest assistant message corresponds to handoff generation.
- It does not parse markdown sections or validate format compliance.
- If assistant output has no text blocks, handoff is treated as missing.
3) Cancellation checks
Section titled “3) Cancellation checks”handoff() returns undefined when either condition holds:
- no captured handoff text, or
#handoffAbortController.signal.abortedis true
It always clears #handoffAbortController in finally.
4) New session creation
Section titled “4) New session creation”If text was captured and not aborted:
- Flush current session writer (
sessionManager.flush()) - Start a brand-new session (
sessionManager.newSession()) - Reset in-memory agent state (
agent.reset()) - Rebind
agent.sessionIdto new session id - Clear queued context arrays (
#steeringMessages,#followUpMessages,#pendingNextTurnMessages) - Reset todo reminder counter
newSession() creates a fresh header and empty entry list (leaf reset to null). In the handoff path, no parentSession is passed.
5) Handoff-context injection
Section titled “5) Handoff-context injection”The generated handoff document is wrapped and appended to the new session as a custom_message entry:
<handoff-context>...handoff text...</handoff-context>
The above is a handoff document from a previous session. Use this context to continue the work seamlessly.Insertion call:
this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);Semantics:
customType:"handoff"display:true(visible in TUI rebuild)- Entry type:
custom_message(participates in LLM context)
6) Rebuild active agent context
Section titled “6) Rebuild active agent context”After injection:
sessionManager.buildSessionContext()resolves message list for current leafagent.replaceMessages(sessionContext.messages)makes the injected handoff message active context- Method returns
{ document: handoffText }
At this point, the active LLM context in the new session contains the injected handoff message, not the old transcript.
Persistence model: old session vs new session
Section titled “Persistence model: old session vs new session”Old session
Section titled “Old session”During generation, normal message persistence remains active. The assistant handoff response is persisted as a regular message entry on message_end.
Result: the original session contains the visible generated handoff as part of historical transcript.
New session
Section titled “New session”After session reset, handoff is persisted as custom_message with customType: "handoff".
buildSessionContext() converts this entry into a runtime custom/user-context message via createCustomMessage(...), so it is included in future prompts from the new session.
Controller/UI behavior
Section titled “Controller/UI behavior”CommandController.handleHandoffCommand behavior:
- Calls
await session.handoff(customInstructions) - If result is
undefined:showError("Handoff cancelled") - On success:
rebuildChatFromMessages()(loads new session context, including injected handoff)- invalidates status line and editor top border
- reloads todos
- appends success chat line:
New session started with handoff context
- On exception:
- if message is
"Handoff cancelled"or error name isAbortError:showError("Handoff cancelled") - otherwise:
showError("Handoff failed: <message>")
- if message is
- Requests render at end
Cancellation semantics (current behavior)
Section titled “Cancellation semantics (current behavior)”Session-level cancellation primitive
Section titled “Session-level cancellation primitive”AgentSession exposes:
abortHandoff()→ aborts#handoffAbortControllerisGeneratingHandoff→ true while controller exists
When this abort path is used, the handoff subscriber rejects with Error("Handoff cancelled"), and command controller maps it to cancellation UI.
Interactive /handoff path limitation
Section titled “Interactive /handoff path limitation”In current interactive controller wiring, /handoff does not install a dedicated Escape handler that calls abortHandoff() (unlike compaction/branch-summary paths that temporarily override editor.onEscape).
Practical impact:
- There is session-level cancellation support, but no handoff-specific keybinding hook in the
/handoffcommand path. - User interruption may still occur through broader agent abort paths, but that is not the same explicit cancellation channel used by
abortHandoff().
Aborted vs failed handoff
Section titled “Aborted vs failed handoff”Current UI classification:
-
Aborted/cancelled
abortHandoff()path triggers"Handoff cancelled", or- thrown
AbortError - UI shows
Handoff cancelled
-
Failed
- any other thrown error from
handoff()/ prompt pipeline (model/API validation errors, runtime exceptions, etc.) - UI shows
Handoff failed: ...
- any other thrown error from
Additional nuance: if generation completes but no text is extracted, handoff() returns undefined and controller currently reports cancelled, not failed.
Short-session and minimum-content guardrails
Section titled “Short-session and minimum-content guardrails”Two guards prevent low-signal handoffs:
- UI layer (
handleHandoffCommand): warns and returns early for< 2message entries - Session layer (
handoff()): throws the same condition as an error
This avoids creating a new session with empty/near-empty handoff context.
State transition summary
Section titled “State transition summary”High-level state flow:
- Interactive slash command intercepted
- Preflight message-count guard
#handoffAbortControllercreated (isGeneratingHandoff = true)- Internal handoff prompt submitted (visible in chat as normal assistant generation)
- On
agent_end, last assistant text extracted - If missing/aborted → return
undefinedor cancellation error path - If present:
- flush old session
- create new empty session
- reset runtime queues/counters
- append
custom_message(handoff) - rebuild and replace active agent messages
- Controller rebuilds chat UI and announces success
#handoffAbortControllercleared (isGeneratingHandoff = false)
Known assumptions and limitations
Section titled “Known assumptions and limitations”- Handoff extraction is heuristic: “last assistant text blocks”; no structural validation.
- No hard check that generated markdown follows requested section format.
- Missing extracted text is reported as cancellation in controller UX.
/handoffinteractive flow currently lacks a dedicated Escape→abortHandoff()binding.- New session lineage metadata (
parentSession) is not set by this path.