Breaking changes to Hook API: - pi.send(text, attachments?) replaced with pi.sendMessage(message, triggerTurn?) - Creates CustomMessageEntry instead of user messages - Properly handles queuing during streaming via agent loop - Supports optional turn triggering when idle - New pi.appendEntry(customType, data?) for hook state persistence - New pi.registerCommand(name, options) for custom slash commands - Handler types renamed: SendHandler -> SendMessageHandler, new AppendEntryHandler Implementation: - AgentSession.sendHookMessage() handles all three cases: - Streaming: queues message with _hookData marker, agent loop processes it - Not streaming + triggerTurn: appends to state/session, calls agent.continue() - Not streaming + no trigger: appends to state/session only - message_end handler routes based on _hookData presence to correct persistence - HookRunner gains getRegisteredCommands() and getCommand() methods New types: HookMessage<T>, RegisteredCommand, CommandContext
8.8 KiB
Session Tree Implementation Plan
Reference: session-tree.md
Phase 1: SessionManager Core ✅
- Update entry types with
id,parentIdfields (using SessionEntryBase) - Add
versionfield toSessionHeader - Change
CompactionEntry.firstKeptEntryIndex→firstKeptEntryId - Add
BranchSummaryEntrytype - Add
CustomEntrytype for hooks - Add
byId: Map<string, SessionEntry>index - Add
leafId: stringtracking - Implement
getPath(fromId?)tree traversal - Implement
getTree()returningSessionTreeNode[] - Implement
getEntry(id)lookup - Implement
getLeafUuid()andgetLeafEntry()helpers - Update
_buildIndex()to populatebyIdmap - Rename
saveXXX()toappendXXX()(returns id, advances leaf) - Add
appendCustomEntry(customType, data)for hooks - Update
buildSessionContext()to usegetPath()traversal
Phase 2: Migration ✅
- Add
CURRENT_SESSION_VERSION = 2constant - Implement
migrateV1ToV2()with extensible migration chain - Update
setSessionFile()to detect version and migrate - Implement
_rewriteFile()for post-migration persistence - Handle
firstKeptEntryIndex→firstKeptEntryIdconversion in migration
Phase 3: Branching ✅
- Implement
branch(id)- switch leaf pointer - Implement
branchWithSummary(id, summary)- create summary entry - Implement
createBranchedSession(leafId)- extract path to new file - Update
AgentSession.branch()to use new API
Phase 4: Compaction Integration ✅
- Update
compaction.tsto work with IDs - Update
prepareCompaction()to returnfirstKeptEntryId - Update
compact()to returnCompactionResultwithfirstKeptEntryId - Update
AgentSessioncompaction methods - Add
firstKeptEntryIdtobefore_compacthook event
Phase 5: Testing ✅
migration.test.ts- v1 to v2 migration, idempotencybuild-context.test.ts- context building with tree structure, compaction, branchestree-traversal.test.ts- append operations, getPath, getTree, branchingfile-operations.test.ts- loadEntriesFromFile, findMostRecentSessionsave-entry.test.ts- custom entry integration- Update existing compaction tests for new types
Remaining Work
Compaction Refactor
- Use
CompactionResulttype for hook return value - Make
CompactionEntry<T>generic with optionaldetails?: Tfield for hook-specific data - Make
CompactionResult<T>generic to match - Update
SessionEventBaseto passsessionManagerandmodelRegistryinstead of derived fields - Update
before_compactevent:- Pass
preparation: CompactionPreparationinstead of individual fields - Pass
previousCompactions: CompactionEntry[](newest first) instead ofpreviousSummary?: string - Keep:
customInstructions,model,signal - Drop:
resolveApiKey(usemodelRegistry.getApiKey()),cutPoint,entries
- Pass
- Update hook example
custom-compaction.tsto use new API - Update
getSessionFile()to returnstring | undefinedfor in-memory sessions - Update
before_switchto havetargetSessionFile,switchto havepreviousSessionFile
Reference: #314 - Structured compaction with anchored iterative summarization needs details field to store ArtifactIndex and version markers.
Branch Summary Design
Current type:
export interface BranchSummaryEntry extends SessionEntryBase {
type: "branch_summary";
summary: string;
}
Questions to resolve:
- Add
abandonedLeafIdfield to reference what was abandoned? - Store metadata about why the branch happened?
- Who generates the summary - user, LLM, or both options?
- Design and implement branch summarizer
- Add tests for
branchWithSummary()flow
Entry Labels ✅
- Add
LabelEntrytype withtargetIdandlabelfields - Add
labelsById: Map<string, string>private field - Build labels map in
_buildIndex()via linear scan - Add
getLabel(id)method - Add
appendLabelChange(targetId, label)method (undefined clears) - Update
createBranchedSession()to filter out LabelEntry and recreate from resolved map buildSessionContext()already ignores LabelEntry (only handles message types)- Add
label?: stringtoSessionTreeNode, populated bygetTree() - Display labels in UI (tree view, path view) - deferred to UI phase
/labelcommand - deferred to UI phase
CustomMessageEntry
Hook-injected messages that participate in LLM context. Unlike CustomEntry<T> (for hook state only), these are sent to the model.
export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
type: "custom_message";
customType: string; // Hook identifier
content: string | (TextContent | ImageContent)[]; // Message content (same as UserMessage)
details?: T; // Hook-specific data for state reconstruction on reload
display: boolean; // Whether to display in TUI
}
Behavior:
- Type definition matching plan
appendCustomMessageEntry(customType, content, display, details?)in SessionManagerbuildSessionContext()includes custom_message entries as user messages- Exported from main index
- TUI rendering:
display: false- hidden entirelydisplay: true- rendered with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors)registerCustomMessageRenderer(customType, renderer)in HookAPI for custom renderers- Renderer returns inner Component, TUI wraps in styled Box
Hook API Changes
Renamed:
renderCustomMessage()→registerCustomMessageRenderer()
New: sendMessage()
Replaces send(). Always creates CustomMessageEntry, never user messages.
type HookMessage<T = unknown> = Pick<CustomMessageEntry<T>, 'customType' | 'content' | 'display' | 'details'>;
sendMessage(message: HookMessage, triggerTurn?: boolean): void;
Behavior:
- If streaming → queue, append after turn ends (never triggers turn)
- If idle AND
triggerTurn: true→ append and trigger turn - If idle AND
triggerTurn: false(default) → just append, no turn - TUI updates if
display: true
For hook state (CustomEntry), use sessionManager.appendCustomEntry() directly.
New: registerCommand()
interface CommandContext {
args: string; // Everything after /commandname
session: LimitedAgentSession; // No prompt(), use sendMessage()
ui: HookUIContext;
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
}
registerCommand(name: string, options: {
description?: string;
handler: (ctx: CommandContext) => Promise<string | void>;
}): void;
Handler return:
void- command completedstring- text to send as prompt (like file-based slash commands)
New: ui.custom()
For arbitrary hook UI with keyboard focus:
interface HookUIContext {
// ... existing: select, confirm, input, notify
/** Show custom component with keyboard focus. Call done() when finished. */
custom(component: Component, done: () => void): void;
}
See also: CustomEntry<T> for storing hook state that does NOT participate in context.
HTML Export
- Add collapsible sidebar showing full tree structure
- Allow selecting any node in tree to view that path
- Add "reset to session leaf" button
- Render full path (no compaction resolution needed)
- Responsive: collapse sidebar on mobile
UI Commands
Design new commands based on refactored SessionManager:
/branch - Current behavior (creates new session file from path)
- Review if this is still the right UX with tree structure
- Consider: should this use
createBranchedSession()orbranch()?
/branch-here - In-place branching (new)
- Use
branch(id)to move leaf pointer without creating new file - Subsequent messages become new branch in same file
- Design: how to select branch point? (similar to current
/branchUI?)
/branches - List/navigate branches (new)
- Show tree structure or list of branch points
- Allow switching between branches (move leaf pointer)
- Show current position in tree
Notes
- All append methods return the new entry's ID
- Migration rewrites file on first load if version < CURRENT_VERSION
- Existing sessions become linear chains after migration (parentId = previous entry)
- Tree features available immediately after migration
- SessionHeader does NOT have id/parentId (it's metadata, not part of tree)
- Session is append-only: entries cannot be modified or deleted, only branching changes the leaf pointer