All persistent data lives in ~/.ollim-bot/. The directory is created on
first use and automatically managed as a git repository — storage.py
auto-commits after most writes.
The root contains agent-facing files (routines, reminders, webhooks, spec
symlinks) that the agent reads and writes directly. Infrastructure state
(sessions, credentials, ping budget, etc.) lives in the state/
subdirectory, keeping the agent’s working directory clean.
~/.ollim-bot/
├── routine-reminder-spec.md → # Symlink to docs/routine-reminder-spec.md
├── webhook-spec.md → # Symlink to docs/webhook-spec.md
├── routines/
│ └── *.md # Recurring routine specs
├── reminders/
│ └── *.md # One-shot reminder specs
├── webhooks/
│ └── *.md # Webhook specs
└── state/
├── bot.pid # PID file for single-instance check
├── credentials.json # Google OAuth credentials (from Cloud Console)
├── token.json # Google OAuth token (auto-generated)
├── sessions.json # Current session ID (plain string)
├── session_history.jsonl # Append-only session lifecycle events
├── pending_updates.json # Background fork updates queued for main session
├── inquiries.json # Button inquiry prompts (7-day TTL)
├── fork_messages.json # Discord message → fork session mapping (7-day TTL)
└── ping_budget.json # Ping budget state (ephemeral, no git commit)
Session files
All files in this section and below live in the state/ subdirectory.
These files track the agent’s conversation state across restarts.
| File | Format | Description |
|---|
sessions.json | Plain string | Active Agent SDK session ID. A raw string, not JSON. Read on startup to resume. |
session_history.jsonl | JSONL | Append-only log of session lifecycle events. One JSON object per line. |
Session events
Each line in session_history.jsonl contains:
| Field | Type | Description |
|---|
session_id | str | The Agent SDK session ID |
event | str | One of: created, compacted, swapped, cleared, interactive_fork, bg_fork, isolated_bg |
timestamp | str | ISO 8601 datetime in the configured timezone (OLLIM_TIMEZONE) |
parent_session_id | str | null | Set for compacted, swapped, and fork events |
{"session_id": "sess_abc", "event": "created", "timestamp": "2026-02-24T10:00:00-08:00", "parent_session_id": null}
{"session_id": "sess_def", "event": "bg_fork", "timestamp": "2026-02-24T10:05:00-08:00",
"parent_session_id": "sess_abc"}
Compaction is detected two ways: the /compact command detects it
via the compact_boundary SystemMessage from the SDK, while
save_session_id() catches SDK auto-compaction by detecting session
ID changes. A _swap_in_progress flag suppresses false compacted
events during swap_client().
Fork and inquiry files
These files track ephemeral Discord interaction state. Both use a 7-day
TTL — entries older than 7 days are pruned on every read.
| File | Format | Description |
|---|
fork_messages.json | JSON array | Maps Discord message IDs to fork session IDs, enabling reply-to-fork. |
inquiries.json | JSON object | Maps 8-char hex keys to inquiry prompts. Persists button actions across restarts. |
fork_messages.json
[
{
"message_id": 1234567890123456789,
"fork_session_id": "sess_fork_abc",
"parent_session_id": "sess_main",
"ts": 1740000000.0
}
]
| Field | Type | Description |
|---|
message_id | int | Discord message snowflake ID |
fork_session_id | str | Agent SDK session ID for the fork |
parent_session_id | str | null | Session the fork branched from |
ts | float | Unix timestamp for TTL pruning |
inquiries.json
{
"a1b2c3d4": {"prompt": "Can you summarize my emails?", "ts": 1740000000.0}
}
| Field | Type | Description |
|---|
| (key) | str | 8-character hex UUID |
prompt | str | The full prompt string for the button inquiry |
ts | float | Unix timestamp for TTL pruning |
Pending updates
Background forks write summaries to pending_updates.json via the
report_updates MCP tool. The main session reads and clears these on
its next turn.
[
{
"ts": "2026-02-24T10:15:00-08:00",
"message": "Checked your morning emails — 3 need replies, none urgent."
}
]
| Field | Type | Description |
|---|
ts | str | ISO 8601 datetime in the configured timezone (OLLIM_TIMEZONE) |
message | str | Summary from the background fork |
Concurrent background forks use an asyncio.Lock to protect the
read-modify-write cycle on this file, preventing lost updates.
Ping budget
ping_budget.json stores the refill-on-read token bucket state for
proactive pings. This file is not git-committed — it is ephemeral
state that resets naturally.
{
"capacity": 5,
"available": 3.7,
"refill_rate_minutes": 90,
"last_refill": "2026-02-24T09:30:00-08:00",
"critical_used": 1,
"critical_reset_date": "2026-02-24",
"daily_used": 2,
"daily_used_reset": "2026-02-24"
}
| Field | Type | Default | Description |
|---|
capacity | int | 5 | Maximum pings in the bucket |
available | float | — | Current fractional ping count |
refill_rate_minutes | int | 90 | Minutes per 1 ping refill |
last_refill | str | — | ISO 8601 datetime of last refill computation |
critical_used | int | — | Critical pings used today (resets daily) |
critical_reset_date | str | — | ISO date for critical_used reset |
daily_used | int | — | Total pings used today |
daily_used_reset | str | — | ISO date for daily_used reset |
On every load(), time-based refill is computed and daily counters are
reset if the date has changed. The updated state is saved back
immediately.
Scheduling directories
Routines, reminders, and webhooks are each stored as .md files in
their own subdirectory. Filenames are auto-generated slugs from the
message content (max 50 characters). The id field in YAML frontmatter
is authoritative — filenames are for human readability.
| Directory | Contents | Description |
|---|
routines/ | *.md | Recurring cron-scheduled routines |
reminders/ | *.md | One-shot and chainable reminders |
webhooks/ | *.md | Webhook endpoint specs with prompt templates |
All three use the same storage format: YAML frontmatter delimited by
---, followed by a markdown body. See
File formats for the full field specs.
routines/morning-briefing.md
---
id: abc123
cron: "30 8 * * 1-5"
description: Morning briefing
---
Review my tasks and calendar for today, then give me a summary.
The agent has direct file access to routines/ and reminders/ — it
creates and manages these files without going through CLI commands.
Google credentials
| File | Source | Description |
|---|
credentials.json | Manual — Google Cloud Console | OAuth 2.0 client creds |
token.json | Auto-generated on first auth flow | OAuth refresh and access tokens |
Neither file is git-committed. token.json is auto-generated
when you first authenticate.
Process lock
bot.pid contains the PID of the running bot process. On startup,
_check_already_running() reads this file and checks
/proc/<pid>/cmdline to prevent duplicate instances. The file is
deleted on exit via atexit.
Spec-file symlinks
On startup, _ensure_spec_symlinks() in main.py creates symlinks in
~/.ollim-bot/ pointing to specification documents in the project’s
docs/ directory:
| Symlink | Target | Referenced by |
|---|
routine-reminder-spec.md | docs/routine-reminder-spec.md | System prompt (routines/reminders section) |
webhook-spec.md | docs/webhook-spec.md | System prompt (webhooks section), webhook handler |
The system prompt directs the agent to “enter a fork and read
routine-reminder-spec.md” (or webhook-spec.md) when it needs the
full YAML specification. Because the agent’s working directory is
~/.ollim-bot/, the symlinks make these files accessible by name
without hardcoding paths to the source tree.
Symlinks are only created if the target doesn’t already exist — they
survive bot restarts and are never overwritten.
Git tracking
The ~/.ollim-bot/ directory is initialized as a git repository.
storage.py auto-commits after writes to most files. Files that are
not git-committed:
| File | Reason |
|---|
state/ping_budget.json | Ephemeral state that refills over time |
state/bot.pid | Process lock deleted on exit |
state/credentials.json | Sensitive OAuth client credentials |
state/token.json | Sensitive OAuth tokens |
state/sessions.json | Frequent overwrites on every agent turn |
state/fork_messages.json | Ephemeral mapping with 7-day TTL |
state/pending_updates.json | Ephemeral cross-fork messaging |
state/inquiries.json | Ephemeral button state with 7-day TTL |
All other files — session history, routines, reminders, and webhooks —
are auto-committed on every write.
All writes use atomic temp-file-then-rename
(tempfile.mkstemp + os.replace) to prevent data corruption.
Do not manually edit files while the bot is running.
Next steps