Overview
The bot is single-user, so it maintains exactly one main session at a time. When the bot starts (or reconnects after a crash), it reads the stored session ID and passes it as theresume parameter to ClaudeSDKClient. If
no session ID exists, a fresh conversation begins.
Session persistence is handled by sessions.py. The Agent SDK handles
conversation history internally — ollim-bot only needs to track the session
ID string.
Session persistence
The main session ID is stored at~/.ollim-bot/state/sessions.json. Despite the
.json extension, this file contains a plain session ID string, not JSON.
| Function | Behavior |
|---|---|
load_session_id() | Returns the session ID string, or None if the file is missing, empty, or starts with { |
save_session_id(id) | Atomic write via tempfile + os.replace. Logs lifecycle events (suppressed during swaps) |
delete_session_id() | Removes the file. Called by Agent.clear() |
Writes are atomic — a temporary file is written first, then moved into
place with
os.replace. This prevents corruption if the bot crashes
mid-write.Agent._get_client() loads the session ID and creates the SDK client with resume=session_id:
Session lifecycle events
Every session transition is recorded in~/.ollim-bot/state/session_history.jsonl. Each line is a SessionEvent:
| Field | Type | Description |
|---|---|---|
session_id | str | The session ID involved in this event |
event | SessionEventType | One of the event types below |
timestamp | str | ISO 8601 timestamp in the configured timezone |
parent_session_id | str | None | The session this one branched from (for forks, compaction, swaps) |
Event types
| Event | Trigger |
|---|---|
created | save_session_id() called with no prior session ID on disk |
compacted | Auto-compaction (detected by save_session_id()), or manual /compact (logged by Agent.compact()) |
swapped | Agent.swap_client() promotes a fork to the main session |
cleared | Agent.clear() called (via /clear command) |
interactive_fork | First StreamEvent or ResultMessage received on a fork client |
bg_fork | Background fork session started |
isolated_bg | Isolated background fork (no parent session) started |
save_session_id() automatically detects created and compacted events
by comparing the new ID against the current one on disk. During
swap_client(), a _swap_in_progress flag suppresses this auto-detection
so that the swap is logged as swapped rather than compacted.
Compaction
Compaction is detected through two paths:- Manual (
/compact):Agent.compact()streams the SDK response and looks for acompact_boundarySystemMessage. When found, it extractspre_tokensfrom the compact metadata, logs acompactedevent, and returns productivity stats (turns, session age, tokens compacted). - Automatic: The SDK may auto-compact during normal conversation.
save_session_id()detects this by comparing the new session ID against the one on disk — if they differ, it logs acompactedevent with the old ID asparent_session_id.
The /clear lifecycle
When a user runs /clear, Agent.clear() executes the following sequence:
Exit any active fork
If an interactive fork is active, exits it with
ForkExitAction.EXIT (clean discard).Log the cleared event
Reads the current session ID and logs a
cleared event to session_history.jsonl.Drop the client
Sets
_client = None, then interrupts and disconnects the SDK client.
Errors are suppressed since the subprocess may have already exited.Fork session tracking
When a background fork sends messages to Discord, ollim-bot needs to map those Discord message IDs back to the fork’s session ID. This enables the reply-to-fork feature: replying to a background fork message resumes that fork’s session as an interactive fork. Tracking uses a contextvar-based collector pattern:| Function | Purpose |
|---|---|
start_message_collector() | Initializes a contextvar list before a background fork runs |
track_message(message_id) | Appends a Discord message ID to the active collector (no-op if no collector) |
flush_message_collector(fork_id, parent_id) | Writes collected IDs to fork_messages.json, clears collector |
cancel_message_collector() | Discards collected IDs without writing |
lookup_fork_session(message_id) | Returns the fork session ID for a Discord message, or None |
~/.ollim-bot/state/fork_messages.json as a JSON array of records:
| Field | Type | Description |
|---|---|---|
message_id | int | Discord message ID |
fork_session_id | str | The fork session that sent this message |
parent_session_id | str | None | The main session at the time of the fork |
ts | float | Unix timestamp of when the record was written |
Files reference
| File | Format | Purpose |
|---|---|---|
~/.ollim-bot/state/sessions.json | Plain string | Current main session ID |
~/.ollim-bot/state/session_history.jsonl | JSONL (SessionEvent) | Append-only lifecycle event log |
~/.ollim-bot/state/fork_messages.json | JSON array | Discord message to fork session mapping |
