bots-garden/mini-mepublic Fork
⑂ v0.0.1
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
💾 Saved. 45c23b4 · on v0.0.1 · k33g · Sep 16, 2026
persistent-sessions.md · 41 lines · 4.5 KBmarkdown
Blame HistoryOpen raw

Persistent sessions — explanation

What is this about?

A session is one conversation: the system prompt, the questions, the answers and the tool calls in between. Until 2026-09-16 it lived in memory and died with the process; an editor that restarted mm could only open a new thread, which is what Zed's "Loading or resuming sessions is not supported by this agent" said. Persistent sessions (sessions.enabled) write each conversation to a file and let both front ends get it back.

Why files, per project

Sessions live under the project, in .mm/sessions/, one JSON file each, rewritten whole after every turn. Three reasons. A conversation is about a project — its files, its commands — and belongs next to them, not in a home-wide database where projects mix. Files are inspectable: jq answers "what did the model read?" without a tool. And a whole-file rewrite is what compression needs: /compact replaces old turns by a summary, and the file must say the same as the memory, not the memory plus everything it used to hold.

The id is the creation time plus a random tail (20260916-101530-4f2a9c1e), so a listing sorts by time and two front ends never collide.

Why a start is a new session

When mm starts, it starts a new session and says how to get the old ones back, rather than resuming the latest on its own. This mirrors docker agent (cagent): its run command never resumes by itself — --session -1 or the TUI's /sessions browser do — and it is the safer default. The last conversation may be about something else; resuming it silently would hand the model a stale context, and the user would only notice when the answers went wrong. An explicit /resume costs one line and keeps the surprise out.

The terminal writes the file on the first question, not at start: a launch that asked nothing leaves no trace, and /sessions lists conversations, not launches.

Why session/load and a full replay

The Agent Client Protocol offers two ways back into a session. session/load is in the stable spec: the agent replays the conversation as session/update notifications and the editor rebuilds the thread from them. session/list and session/resume are unstable extensions: the agent only re-registers the session, and the editor keeps whatever it was showing. docker agent chose the second (loadSession: false, Resume capability, SQLite store in ~/.cagent/session.db). mini-me chose the first, because it is the stable one and because a replay puts the thread back even in an editor that kept nothing.

"Replay" means re-sending records. Each tool call comes back as a single, already-completed tool_call carrying the input the model gave and the output the tool returned at the time. Nothing is executed again: reloading a thread that once ran rm runs nothing.

A reloaded or resumed session continues under the current system prompt, not the one stored with it: the prompt is the agent's, and a change to agent.yaml must reach old conversations too.

/new in the two front ends

In the terminal /new simply leaves the old file alone and lets the next question open a new one. Over ACP the editor owns the session id and keeps using it, so the id's file must become the fresh session; the conversation so far is moved to a new id first. Either way nothing is lost on disk, and a later reload of the thread gives the current conversation, not the one /new closed.

Rejected alternatives

  • Resume the latest session automatically at start. Rejected for the stale-context reason above, and because docker agent does not do it either.
  • Append-only log per session. Rejected: compression shrinks the history, and a log would keep saying what the memory no longer holds.
  • SQLite, like docker agent. Rejected for a minimal agent: one more dependency, opaque files, and a per-user database where mini-me wants per-project files.
  • session/resume (unstable) instead of session/load. Rejected: not in the stable spec, and it relies on the editor having kept the thread.
  • Replaying only text messages. Rejected by the user: the tool calls are the part of a coding session worth seeing again.

How it relates to the rest

internal/store is the file layer, used by internal/agent (a keeper that saves after each turn and serves /sessions and /resume) and by internal/acp (a file per session, session/load, the replay in replay.go). internal/session.WithSystem swaps the stored prompt for the current one. Compression (internal/compact) is unaware of persistence: the front ends save after it runs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Persistent sessions — explanation

## What is this about?

A session is one conversation: the system prompt, the questions, the answers and the tool calls in between. Until 2026-09-16 it lived in memory and died with the process; an editor that restarted `mm` could only open a new thread, which is what Zed's "Loading or resuming sessions is not supported by this agent" said. Persistent sessions (`sessions.enabled`) write each conversation to a file and let both front ends get it back.

## Why files, per project

Sessions live under the project, in `.mm/sessions/`, one JSON file each, rewritten whole after every turn. Three reasons. A conversation is about a project — its files, its commands — and belongs next to them, not in a home-wide database where projects mix. Files are inspectable: `jq` answers "what did the model read?" without a tool. And a whole-file rewrite is what compression needs: `/compact` replaces old turns by a summary, and the file must say the same as the memory, not the memory plus everything it used to hold.

The id is the creation time plus a random tail (`20260916-101530-4f2a9c1e`), so a listing sorts by time and two front ends never collide.

## Why a start is a new session

When `mm` starts, it starts a new session and says how to get the old ones back, rather than resuming the latest on its own. This mirrors docker agent (cagent): its `run` command never resumes by itself — `--session -1` or the TUI's `/sessions` browser do — and it is the safer default. The last conversation may be about something else; resuming it silently would hand the model a stale context, and the user would only notice when the answers went wrong. An explicit `/resume` costs one line and keeps the surprise out.

The terminal writes the file on the first question, not at start: a launch that asked nothing leaves no trace, and `/sessions` lists conversations, not launches.

## Why `session/load` and a full replay

The Agent Client Protocol offers two ways back into a session. `session/load` is in the stable spec: the agent replays the conversation as `session/update` notifications and the editor rebuilds the thread from them. `session/list` and `session/resume` are unstable extensions: the agent only re-registers the session, and the editor keeps whatever it was showing. docker agent chose the second (`loadSession: false`, `Resume` capability, SQLite store in `~/.cagent/session.db`). mini-me chose the first, because it is the stable one and because a replay puts the thread back even in an editor that kept nothing.

"Replay" means re-sending records. Each tool call comes back as a single, already-completed `tool_call` carrying the input the model gave and the output the tool returned at the time. Nothing is executed again: reloading a thread that once ran `rm` runs nothing.

A reloaded or resumed session continues under the current system prompt, not the one stored with it: the prompt is the agent's, and a change to `agent.yaml` must reach old conversations too.

## `/new` in the two front ends

In the terminal `/new` simply leaves the old file alone and lets the next question open a new one. Over ACP the editor owns the session id and keeps using it, so the id's file must become the fresh session; the conversation so far is moved to a new id first. Either way nothing is lost on disk, and a later reload of the thread gives the current conversation, not the one `/new` closed.

## Rejected alternatives

- **Resume the latest session automatically at start.** Rejected for the stale-context reason above, and because docker agent does not do it either.
- **Append-only log per session.** Rejected: compression shrinks the history, and a log would keep saying what the memory no longer holds.
- **SQLite, like docker agent.** Rejected for a minimal agent: one more dependency, opaque files, and a per-user database where mini-me wants per-project files.
- **`session/resume` (unstable) instead of `session/load`.** Rejected: not in the stable spec, and it relies on the editor having kept the thread.
- **Replaying only text messages.** Rejected by the user: the tool calls are the part of a coding session worth seeing again.

## How it relates to the rest

`internal/store` is the file layer, used by `internal/agent` (a `keeper` that saves after each turn and serves `/sessions` and `/resume`) and by `internal/acp` (a file per session, `session/load`, the replay in `replay.go`). `internal/session.WithSystem` swaps the stored prompt for the current one. Compression (`internal/compact`) is unaware of persistence: the front ends save after it runs.