Repository navigation
Feature: Session Lifecycle
Unless noted, client.* names below are the opencode v1 mapping; the v2 equivalents and degrades are in opencode-plugin.md.
opencode emits bus events for session lifecycle changes. Thatch subscribes to these events to manage the extraction pipeline, send session-start reminders, handle child session cleanup, and resolve wrap-up commands.
- Session-start reminder + hygiene heartbeat for top-level sessions
- Child session tracking (
childToParentmap,parentSnapshots) - Direct extraction: trigger child session when parent goes idle with pending buffer
- Extraction child lifecycle: created on parent idle, drains parent's snapshot on completion, deleted after
- Sub-agent lifecycle: task-dispatched sub-agents complete accepted entries but are not deleted
- Session error recovery: child errors requeue parent's accepted entries
- Session deletion recovery: child deletion requeues; parent deletion completes accepted entries
- Wrap-up commands:
/thatch/compactand/thatch/exitarm a greenlight check resolved on the next idle
Subscribes to all session bus events. Dispatches based on event.type.
- Record
childToParent.set(childID, parentID) - Snapshot the parent's current pending buffer:
parentSnapshots.set(childID, [...extraction.peek(parentID)]) - Return early -- child sessions don't get the session-start reminder
The snapshot is journal-recovery data only (restored child bookkeeping after a reload); live delivery is claim-based: the child's payload fetch records exactly which entries it received, and its completion signal consumes only those.
- Send session-start reminder via
client.session.promptwithnoReply: trueandsynthetic: true - The reminder includes the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero
- See hygiene.md
When a child created by triggerExtraction goes idle:
-
completeClaimed(childID)-- consume only the entries this child claimed via its payload fetch. A no-claim idle is a no-op: a child that never fetched processed nothing, and its idle signal must not drop entries another extractor holds. Held entries with no completer are bounded by the 15-minute stale reaper - Fire a toast with extraction metrics (new/updated/deleted counts) -- only if memories were actually written
- Clean up all maps:
extracting,childToParent,parentSnapshots,childMetrics,extractionChildren -
consume(childID)-- drain child's own buffer - Delete the child session via
client.session.delete
When a task-dispatched sub-agent (not created by triggerExtraction) goes idle:
-
completeClaimed(childID)-- complete only what THIS child claimed via its payload fetch (a no-claim idle consumes nothing) - Does NOT drain the buffer or delete the session -- the task tool that dispatched the sub-agent needs to read its output
When a parent session (no parentID) goes idle:
- Wrap-up resolution first (below): a pending
/thatch/compactor/thatch/exitresolves here, and a greenlit action returns early -
requeueStaleAccepted()first: accepted entries whose completion signal never came (15 min) return to pending - If not compacting, not already extracting, and buffer has pending interactions:
triggerExtraction(sessionID) - On failure: log error, clear
extractingflag (the next idle retries -- there is no model-facing nudge fallback)
User-invoked slash commands, shipped as command markdown synced by the plugin (see src/commands.ts). The template instructs the model to flush pending fact extraction (thatch_get_extraction_payload + thatch_extraction_done), finish promised memory writes, and surface unaddressed todos -- then end its response with a greenlight token (THATCH_COMPACT_READY / THATCH_EXIT_READY) only when safe to proceed. Text typed after the command is forwarded into a labeled # User Message section ahead of the # Pre-*-wrap-up checklist section (opencode's $ARGUMENTS substitution): the headers give the user's words provenance, so they cannot be misread as part of the wrap-up instructions. opencode's substitution is purely mechanical with no default-value form (verified against v1.18.30), so the template cannot switch on emptiness; instead the section always carries a fallback line telling the model to treat an empty section as n/a (bare command, no user text).
-
command.execute.beforearms the session inpendingWrapUpwhen the command runs - On the session's next idle, the plugin fetches the session's messages via the SDK client and checks the final assistant message's trailing text for the token (trimmed, exact match)
- Token present: trigger the TUI action and return early -- compaction is starting (the checklist drained the buffer) or the process is exiting
- compact:
client.tui.executeCommand({ body: { command: "session_compact" } }). The execute-command route only accepts legacy alias names;session_compactmaps to the TUI'ssession.compactaction, the same thing the built-in/compactruns - exit: unregister the session from the chat directory first (a greenlit exit's host is about to vanish; other sessions must stop addressing mail to it), then
client.tui.publish({ body: { type: "tui.command.execute", properties: { command: "app.exit" } } }). No exit alias exists, so the TUI keymap command is published directly. The exit template's checklist also asks the model to callthatch_chat_unregisteras its last persistence step; the plugin-side unregister is the deterministic backstop. Compaction deliberately does NOT unregister - the session continues
- compact:
- Token absent: warning toast pointing at the blockers in the response, then fall through -- the model may have buffered tool interactions that still need extraction
installOpencodeCommands syncs the command markdown into
$XDG_CONFIG_HOME/opencode/command/thatch/ on every plugin load, writing only
files whose on-disk content differs (template updates self-heal). opencode
loads config -- including command discovery -- before plugins, so a
first-ever install is invisible until the next server start.
-
requeueClaimed(childID)if it fetched (the delivery was never processed), elserequeueAccepted(parentID)-- the error is a terminal death signal, so a claim-less child falls back to whole-set requeue for immediate recovery - Clear
extractingfor the parent (the next idle re-triggers) - Clean up all maps for the child
- Extraction child:
requeueClaimed(childID), elserequeueAccepted(parentID)(never processed) - Task-kind child:
requeueClaimed(childID)only -- task children are deleted ROUTINELY, so a claim-less task child's deletion must not requeue the whole accepted set (it would yank an in-flight extractor's payload back to pending for duplicate extraction) - Clean up all maps for the child
-
completeAccepted(id)-- a deleted parent takes its accepted entries with it - Clear
extractingfor the parent - Clear
pendingWrapUpfor the parent
- Clear the
compactingflag so chat.message nudges resume - See compaction-recovery.md
-
childToParent:
Map<childID, parentID>-- maps child sessions to their parents -
parentSnapshots:
Map<childID, Interaction[]>-- snapshot of parent's buffer at child creation time (journal recovery only; live delivery is claim-based) -
childMetrics:
Map<childID, {new, updated, deleted}>-- extraction metrics per child -
extracting:
Set<parentID>-- parent IDs with an active direct-extraction child (gates re-triggering) -
extractionChildren:
Set<childID>-- distinguishes extraction children from task-dispatched sub-agents -
compacting:
Set<sessionID>-- sessions currently being compacted (suppresses nudges) -
pendingWrapUp:
Map<sessionID, {token, kind}>-- wrap-up commands awaiting their greenlight check; armed bycommand.execute.before, resolved on the next idle, cleared on session deletion
- Extraction pipeline (extraction.md): direct extraction is triggered by
session.status idle; child lifecycle managed here - Nudge pipeline (nudge-pipeline.md):
compactingset suppresses all nudges; task-dispatched sub-agents (inchildToParent, not inextractionChildren) suppress all tiers - Hygiene (hygiene.md): hygiene report runs at
session.createdfor top-level sessions - Compaction recovery (compaction-recovery.md):
session.compactedevent clears the compacting flag - Memory store (memory-store.md): child sessions write memories via
memory_remember, which completes their claimed delivery viatool.execute.after
-
src/runtime.ts-- event handler (all session events),triggerExtraction,cleanupChild, internal state maps -
src/opencode/v1.ts,src/opencode/v2.ts-- host adapters wiring the runtime into each opencode plugin API
- Child sessions don't get the session-start reminder (early return in
session.createdwith parentID). - Delivery is claim-based: the payload fetch records what the fetcher received; only the fetcher's completion consumes it. A no-claim completion is a no-op, never a whole-set drop.
- Extraction children are deleted after going idle; task-dispatched sub-agents are NOT deleted (task tool reads output).
- Child errors requeue what the child held (never processed). Child deletion also requeues.
- Parent deletion completes accepted entries (takes them with it).
User
- Guide: Behavior Engine
- Guide: Cli
- Guide: Code Review
- Guide: Commands
- Guide: Cross Session Chat
- Guide: Deduplication
- Guide: Default Behaviors
- Guide: Extraction
- Guide: Hygiene
- Guide: Memory
- Guide: Notifications
- Guide: Prediction Engine
- Guide: Overview
- Guide: Setup
- Guide: Skills
- Guide: Watchers
Developer
Dev Feature Guides
- Feature: Behavior Engine
- Feature: Cicd
- Feature: Cli
- Feature: Commands
- Feature: Compaction Recovery
- Feature: Cross Session Chat
- Feature: Database
- Feature: Deduplication
- Feature: Extraction
- Feature: Hygiene
- Feature: Memory Store
- Feature: Multi Host
- Feature: Notifications
- Feature: Nudge Pipeline
- Feature: Opencode Plugin
- Feature: Prediction Engine
- Feature: Qa System
- Feature: Overview
- Feature: Repo Identity
- Feature: Session Lifecycle
- Feature: Setup
- Feature: Sideband
- Feature: Watchers