# Sessions, branching and side conversations Session lifecycle: naming, branching, switching, compaction and child sessions. These run on DSH's own session services (`ctx.sessions.create/fork`, `ctx.compaction.compactNow`, Cordis reload), so a Pi package's branch really is a DSH session with lineage — visible in the session list, resumable, and compactable by the host. A package that opens a side conversation gets DSH's native subagent UI: the bridge creates a real child agent/session through `ctx.agents`; the child is listed, named after the package, opened in its own view and continuable. This does not exercise the separate `ctx.subagents` provider seam. See [`examples/side-conversation`](../../examples/side-conversation/). **24 upstream-shaped Pi rule rows** — 6 same semantics · 18 mapped, difference stated. | Pi surface | Kind | Status | What it does on DSH | |---|---|---|---| | [`appendEntry`](#appendentry-pi) | `pi.*` | Mapped, difference stated | Persisted as a genuine Pi custom entry line in the per-session Pi-format archive (the same file getSessionFile() names, parseable by Pi's own SessionManager) and replayed on session start; DSH's main log stays untouched because its persistence read path refuses out-of-vocabulary event types and Session.append cannot set the ignorable escape hatch. | | [`setSessionName`](#setsessionname-pi) | `pi.*` | Same semantics | Renames the DSH session through ctx.sessionTitle, so every DSH surface shows it and the title is pinned against automatic regeneration, and announces it through session_info_changed. A composition that mounts no title service falls back to a session_info entry in the Pi-format archive, as does a blank name (DSH requires visible characters in a title; Pi does not). | | [`getSessionName`](#getsessionname-pi) | `pi.*` | Same semantics | Reads DSH's own session title, so it agrees with what DSH displays and sees titles DSH generated itself; falls back to the archive's session_info entry when no title service is mounted. | | [`setLabel`](#setlabel-pi) | `pi.*` | Mapped, difference stated | Persisted as a Pi label entry line in the per-session Pi-format archive and reflected by the sessionManager projection. | | [`session_start`](#session_start-event) | `event` | Same semantics | Mapped to agent/session-start. | | [`session_shutdown`](#session_shutdown-event) | `event` | Mapped, difference stated | Async Cordis teardown joins pending message projection and ordered shutdown handlers; reload dispatches reason=reload before remount. Duplicate disposal notifications are suppressed. During full process shutdown other host services may already be unavailable, so model-dependent exit work is not guaranteed. | | [`session_info_changed`](#session_info_changed-event) | `event` | Mapped, difference stated | Fired by setSessionName() and projected from DSH session/title events. | | [`session_before_compact`](#session_before_compact-event) | `event` | Mapped, difference stated | Projected from DSH compaction/start as a notification; cancel/replace cannot reach DSH's compactor. | | [`session_compact`](#session_compact-event) | `event` | Mapped, difference stated | Fires once per SUCCESSFUL compaction, from DSH's summary event, with the summary rendered to the string Pi's CompactionEntry declares. A manual compaction is identified as manual; DSH does not record which automatic trigger fired, so automatic ones report "threshold" and willRetry is always false. | | [`session_before_switch`](#session_before_switch-event) | `event` | Mapped, difference stated | Registration is accepted; Pi session switching never occurs on DSH surfaces, so the handler never fires. Loading is unaffected. | | [`session_before_fork`](#session_before_fork-event) | `event` | Mapped, difference stated | Registration is accepted; Pi tree forking never occurs on DSH surfaces, so the handler never fires. Loading is unaffected. | | [`session_before_tree`](#session_before_tree-event) | `event` | Mapped, difference stated | Registration is accepted; Pi session-tree navigation never occurs on DSH surfaces, so the handler never fires. Loading is unaffected. | | [`session_tree`](#session_tree-event) | `event` | Mapped, difference stated | Registration is accepted; Pi session-tree navigation never occurs on DSH surfaces, so the handler never fires. Loading is unaffected. | | [`cwd`](#cwd-ctx) | `ctx.*` | Same semantics | Mapped to the active DSH agent session working directory. | | [`hasUI`](#hasui-ctx) | `ctx.*` | Same semantics | Reports whether a real interactive surface exists: true for a mounted public terminal surface (dsh-TUI tuiScenes or dsh-pi-tui piTuiExtensions) or for a live DSH question answerer; false for a genuinely headless composition and for child-agent questions that DSH refuses. | | [`mode`](#mode-ctx) | `ctx.*` | Same semantics | Reports tui only where a real full-screen terminal seat exists — dsh-TUI scenes or dsh-pi-tui raw mounts. Every web composition is rpc on purpose (2026-08-29 product decision): the browser renders product UI (side-chat window, MCP tab, pills), never a projected TUI, so packages asked "can I show interactive full-screen UI here?" get the honest no and take their own official degradation. | | [`sessionManager`](#sessionmanager-ctx) | `ctx.*` | Mapped, difference stated | A real read-only projection using the host snapshotEvents API or the legacy event reader. Native file attachments become the host-rendered read-only file locator text in Pi. DSH durable messages plus the session's Pi-only entries (customs, labels, branch summaries — stored as genuine Pi entry lines in the per-session archive file), exposed through Pi's exact 14-method surface as a single-branch tree. API reads use the public snapshotEvents() surface where available, with the older events surface as fallback. getSessionFile() is undefined for Pi in-memory side sessions (native DSH audit remains); otherwise it atomically exports a complete Pi JSONL transcript under the redirected sessions directory for file-only consumers; this derived copy is never imported as native state. Cold history is read through public persistence inspect/read handles, and native deletion retires the derived exports. Host-authored context projects as custom messages rather than human turns. buildContextEntries is compaction-aware — entries a compaction summarized away are gone, exactly as they are for the model — while getEntries stays the append-only log, which is the same split Pi makes. | | [`shutdown`](#shutdown-ctx) | `ctx.*` | Mapped, difference stated | Pi defines shutdown behavior as host-provided (runner.ts bindExtensions); this host absorbs the request — the user owns DSH process exit — and informs the user once. The package keeps running. | | [`compact`](#compact-ctx) | `ctx.*` | Mapped, difference stated | Pi's fire-and-forget trigger, translated to DSH's official manual compaction (ctx.compaction.compactNow on the live agent). onComplete receives the real summary text and the shadowed-content token estimate as tokensBefore; firstKeptEntryId is empty because the DSH log has no Pi entry ids. Without a compaction service the gap flows through Pi's onError callback and the capability ledger. | | [`newSession`](#newsession-ctx) | `ctx.*` | Mapped, difference stated | Really creates a DSH session (ctx.sessions.create) with parent lineage; withSession runs against a projection context bound to it, whose sendMessage/sendUserMessage/appendEntry write into THAT session rather than the one the call came from. DSH has no host-level "current session pointer" a plugin could move — which session the surface shows stays a host choice, announced once. | | [`fork`](#fork-ctx) | `ctx.*` | Mapped, difference stated | Really forks on DSH's official prefix-fork surface (ctx.sessions.fork with lineage metadata). Anchors are durable-log entries (projected ids "dsh-"); Pi's default position "before" is honored, and the boundary shrinks to the nearest completed-turn edge because DSH seeds must not split an open turn. Sidecar entries cannot anchor a fork. | | [`navigateTree`](#navigatetree-ctx) | `ctx.*` | Mapped, difference stated | Expressed through DSH's session model: the DSH tree lives BETWEEN sessions (fork lineage), not inside one log, so navigation forks at the target boundary. summarize runs Pi's vendored branch summarizer over the abandoned durable slice (model call on the DSH llm bridge) and lands the summary as a branch_summary entry in the new session's projection; label lands as a label entry. | | [`switchSession`](#switchsession-ctx) | `ctx.*` | Mapped, difference stated | Targets a LIVE DSH session by id (or a Pi-style path whose basename is ".jsonl"); withSession runs against its projection. Switching to persisted sessions is host-owned — resume them from the DSH surface first. Which session the surface shows stays a host choice. | | [`reload`](#reload-ctx) | `ctx.*` | Mapped, difference stated | Really remounts every mounted package's extension entries through a fresh loader (registrations replaced via the same-name path, event handlers reset), so edited plugin code takes effect. Skills, prompts, and themes are host-managed and reload with dsh itself. | ## How each one is built Every surface below names the DSH mechanism that carries it — the seam, service or waterfall — so the mapping can be checked against the harness rather than taken on trust. ### `appendEntry` `pi.*` · Mapped, difference stated A Pi entry line appended to the session's Pi-format archive file, replayed at session start. These entries are sole originals, not copies of anything native. DSH's own log has no channel for event types declared outside the harness — its read path refuses unknown types without the envelope's ignorable marker, which Session.append cannot set — so writing them there corrupts the session for every other reader. The native registration surface is explicitly deferred upstream "until such a consumer exists"; migrating these entries there is the endgame. ### `setSessionName` `pi.*` · Same semantics ctx.sessionTitle.rename, which is also what pins the title against DSH's automatic regeneration, followed by a session_info_changed dispatch. A composition with no title service — or a blank name, which DSH refuses and Pi allows — falls back to a session_info entry in the Pi-format archive. ### `getSessionName` `pi.*` · Same semantics ctx.sessionTitle.get, so the answer agrees with what DSH displays and includes titles DSH generated itself; archive session_info fallback when no title service is mounted. ### `setLabel` `pi.*` · Mapped, difference stated A Pi label entry line in the session's Pi-format archive, read back through the sessionManager projection. ### `session_start` `event` · Same semantics Dispatched from the cordis agent/session-start notification. ### `session_shutdown` `event` · Mapped, difference stated Uses the public async Cordis effect disposer while the native session still exists, with agent/disposed as the legacy fallback. The host does not expose a pre-shutdown gate that keeps every model and credential service alive until plugin callbacks complete. ### `session_info_changed` `event` · Mapped, difference stated Two sources, one event: the bridge's own setSessionName, and DSH's durable session title event projected into Pi's shape. ### `session_before_compact` `event` · Mapped, difference stated Projected from DSH's compaction/start as a notification. It is an emit, not a waterfall, so a cancel or a replacement has no channel to reach DSH's compactor and is not pretended. ### `session_compact` `event` · Mapped, difference stated Projected from DSH's compaction summary event, rendered to the exact string Pi's CompactionEntry declares. Manual compactions are identifiable; DSH does not record which automatic trigger fired, so automatic ones report Pi's threshold reason. ### `session_before_switch` `event` · Mapped, difference stated The handler goes into the bridge's handler map like any other, and nothing dispatches it: no DSH seam produces Pi session switching. Registration is kept rather than refused so a package that subscribes at load time still loads. ### `session_before_fork` `event` · Mapped, difference stated The handler goes into the bridge's handler map like any other, and nothing dispatches it: no DSH seam produces Pi tree forking. Registration is kept rather than refused so a package that subscribes at load time still loads. ### `session_before_tree` `event` · Mapped, difference stated The handler goes into the bridge's handler map like any other, and nothing dispatches it: no DSH seam produces Pi session-tree navigation. Registration is kept rather than refused so a package that subscribes at load time still loads. ### `session_tree` `event` · Mapped, difference stated The handler goes into the bridge's handler map like any other, and nothing dispatches it: no DSH seam produces Pi session-tree navigation. Registration is kept rather than refused so a package that subscribes at load time still loads. ### `cwd` `ctx.*` · Same semantics Read off the live DSH agent's session; the bridge keeps no working directory of its own. ### `hasUI` `ctx.*` · Same semantics The terminal answer comes from the capability-selected TerminalSurfaceAdapter. The question answer is generation-selected by capability: rc lines expose a provider slot, so the bridge registers a throwaway provider and reads the documented DUPLICATE_PROVIDER rejection as "a real provider is already there"; the 0.1.2 line replaced the slot with agent-scoped user-questions/request waterfall answerers, so the bridge lists would-be listeners through cordis Lifecycle.dispatch (a typed public member) and falls back to a recent browser contact on the presentation route. Inside a child agent only the terminal seat can make this true, because DSH refuses child-agent questions. ### `mode` `ctx.*` · Same semantics Derived from the live optional TerminalSurfaceAdapter only. Surface selection is by public service/capability (`tuiScenes` or `piTuiExtensions` + unstable.surface.handle), never by Pi consumer package. ### `sessionManager` `ctx.*` · Mapped, difference stated A read-only projection folded from two ordered sources — DSH's durable session log (single authority for conversation, projected live) and Pi-only entries recovered from the per-session archive — into the single chain Pi's 14-method surface walks. File consumers receive a derived native transcript in that archive; it is not a second restore authority. Compaction awareness comes from the same fold: entries a compaction summarized away are dropped from the context view and kept in the log view. ### `shutdown` `ctx.*` · Mapped, difference stated Pi defines shutdown as host-provided, so this host absorbs it: the request is recorded in the capability ledger and surfaced to the user once. Nothing calls process.exit — the DSH process belongs to the user. ### `compact` `ctx.*` · Mapped, difference stated Calls DSH's official compaction.compactNow on the live agent and adapts the outcome onto Pi's onComplete/onError callbacks. With no compaction service mounted the gap flows through onError rather than an exception. ### `newSession` `ctx.*` · Mapped, difference stated ctx.sessions.create with parent lineage, then a projection context bound to the new session so everything inside withSession (sendMessage, appendEntry, ...) writes into THAT session. ### `fork` `ctx.*` · Mapped, difference stated ctx.sessions.fork at a durable sequence number decoded from the projected entry id (dsh-), snapped to the nearest completed-turn edge because a DSH seed must not split an open turn. ### `navigateTree` `ctx.*` · Mapped, difference stated A fork at the target boundary, plus — when the caller asks for one — Pi's vendored branch summarizer run over the abandoned slice with the DSH llm bridge filling Pi's own streamFn injection point. ### `switchSession` `ctx.*` · Mapped, difference stated Resolves the id (or the basename of a Pi-style .jsonl path) against live DSH sessions and binds a projection context to it. ### `reload` `ctx.*` · Mapped, difference stated Remounts every mounted package's extension entries through a fresh jiti loader: registrations are replaced through the same-name path and handler lists reset, so edited plugin code takes effect without restarting DSH. --- Back to the [capability index](README.md) · the whole verdict in [pi-abi-coverage.md](../pi-abi-coverage.md).