Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Session Persistence and Resume

Every conversation-session in Zeph (CLI, TUI, Telegram, Discord, Slack, ACP, or zeph serve-sessions) can maintain a durable, replayable event log — a crash-safe, forkable record of everything that happened in that session, independent of the SQLite messages table used for search and recall.

Why a Separate Event Log?

The existing SQLite messages projection is optimized for semantic search and context assembly, not for exact replay. It doesn’t capture tool-call/result pairing precisely enough to reconstruct a conversation byte-for-byte after a crash, and it has no concept of “this session was forked from that one at this exact point.”

The event log (zeph-session crate) exists specifically to answer: what exactly happened in this session, in order, and can I replay it deterministically?

The Event Log

Each session writes to its own append-only JSONL file:

<data_dir>/sessions/<session_id>/events.jsonl

Every line is one SessionEventSessionStarted, UserMessage, AssistantMessage, ToolCall, ToolResult, Condensation, Compaction, or ForkPoint — tagged with a sequence number (seq), a timestamp, and (for tool events) a turn ID. The log is the source of truth; the SQLite projection is reconciled from it, not the other way around.

Crash safety: an event is durably appended and flushed before the corresponding SQLite write happens, never after. If the process crashes between the two, the log is ahead — on next open, a torn trailing write (partial JSON from a crash mid-append) is detected and truncated cleanly rather than causing a parse error, and the SQLite projection is reconciled forward from the log to catch up.

SessionId and ConversationId

Every session has a SessionId (a UUID string) linked one-to-one with a ConversationId (the existing SQLite conversation primary key). Looking up a session by id resolves its linked conversation, and vice versa — this bijection is what lets /conv resume//conv fork swap the active conversation on a running agent without losing the connection between the durable log and the searchable SQLite history.

Browsing and Resuming Sessions

/conv — In-Conversation Slash Command

/conv list                  # table of all sessions: id, title, status, event count, updated
/conv show <id>             # one session's metadata
/conv resume <id>           # swap the CURRENT conversation onto <id>, replaying its history
/conv fork <id>             # copy <id> into a fresh session, then swap onto the copy

/conv is a channel-agnostic slash command — it works identically whether typed in the CLI, the TUI, or a chat channel, because it’s implemented once against the shared AgentAccess interface rather than per-channel.

/conv resume and /conv fork perform a live, mid-session swap: the agent’s message history, active ConversationId, and durable-log target all switch to the resumed/forked session between turns — no restart required. In the TUI, a “Replaying conversation…” status indicator shows while the swap is in progress.

zeph sessions — CLI Subcommand

zeph sessions list                      # table of all sessions
zeph sessions show <id> [--events]      # metadata, or full event-log dump with --events
zeph sessions resume <id>               # launch a live interactive agent bound to <id>
zeph sessions resume <id> --print       # dump events to stdout instead (no agent started)
zeph sessions fork <id> [--at N]        # eager-copy <id> into a fresh session
zeph sessions export <id> <path>        # write the event log to a file
zeph sessions import <path>             # load an exported event log as a new session

Forking

Forking eager-copies a session’s event log up to a cut point (--at N, or the full log by default) into a fresh session with its own id:

  • The child’s log opens with a SessionStarted event recording forked_from: (parent_id, seq).
  • The parent’s log gets a non-destructive ForkPoint provenance record — forking never mutates the parent’s own history.
  • The child gets its own ConversationId, independent of the parent.

A session can only be forked at a point within its own logged history — old sessions predating event-log persistence (see Legacy Sessions below) can only be forked at the import boundary, not at an arbitrary historical point, since granular tool-call replay isn’t available before that boundary.

Condensation

Distinct from live, in-memory context compaction (see Context Budgets), condensation operates at the event-log level: when a session’s durable history grows large, a Condenser (the default implementation, LlmCondenser, reuses the same summarization pipeline as in-memory compaction) replaces a range of events with a single Condensation event carrying a structured summary. Replay folds a Condensation event into one system message representing everything it replaced — a later condensation is guaranteed not to overlap an earlier one’s replaced range.

Legacy Sessions

Installs upgrading from before session persistence existed have SQLite message history for conversations with no event log at all. That history is not retroactively synthesized into a full log — tool-call/result pairing isn’t recoverable from the old projection, so any attempt would be lossy and potentially misleading.

Instead, the first time such a session is resumed (via /conv resume, zeph sessions resume, or an ACP client reconnecting), Zeph writes a SessionStarted header plus a single event summarizing “this session had N prior messages before durable logging existed,” then proceeds normally — every turn after that point is logged in full detail. A session bootstrapped this way can be forked and replayed from that import boundary onward, just not from before it.

Configuration

[session]
enabled = true               # maintain a durable event log per session (default: true)
data_dir = ".zeph/sessions"  # directory under which event logs are stored
encrypt = false               # opt-in AEAD encryption — reserved, not yet implemented
max_event_log_mb = 256        # size guard that triggers condensation

[session.condense]
condense_provider = ""        # provider name from [[llm.providers]]; empty = primary provider
threshold = 0.85               # fraction of context budget that triggers condensation
keep_recent = 20               # minimum recent events preserved after condensation

When [session] enabled = false, only the SQLite messages projection is written — the pre-session-persistence behavior. encrypt is a placeholder for a future AEAD implementation; the current protection for event-log files is OS file permissions (0o600 on Unix).

See Also