Reflection 005: Agent Remembers
Andy Smith
The agent can talk. Telegram messages go in, Claude responses come back. But every message is a blank slate. “Remember 42” followed by “What number?” gets nothing useful. The agent has amnesia.
This iteration adds session persistence. The agent remembers.
Hypothesis
Claude Code already has session management — --resume picks up a previous conversation, --session-id starts a named one. The hard part isn’t the mechanism, it’s the mapping: one Telegram chat needs to map to one Claude session, reliably, across messages.
If we store a UUID per chat ID and pass it to --resume, the agent should maintain context within a conversation. And if --resume fails (corrupted session, expired state), falling back to a fresh --session-id should recover gracefully without the user noticing anything worse than a memory reset.
Increment
Session storage
Each Telegram chat gets a session file in $HOME/sessions/:
$HOME/sessions/
├── 123456789 # contains: a1b2c3d4-...
├── 987654321 # contains: e5f6g7h8-...
The file name is the Telegram chat ID. The content is a UUID. Simple flat-file storage — no database, no external service. The container’s home directory is writable (uid 1000, set up in iteration 003), so this works out of the box.
Three functions manage the lifecycle:
get_session_id() {
local chat_id="$1"
local file="${SESSIONS_DIR}/${chat_id}"
if [ -f "$file" ]; then
cat "$file"
fi
}
new_session() {
local chat_id="$1"
local uuid
uuid=$(cat /proc/sys/kernel/random/uuid)
echo "$uuid" > "${SESSIONS_DIR}/${chat_id}"
echo "$uuid"
}
reset_session() {
local chat_id="$1"
rm -f "${SESSIONS_DIR}/${chat_id}"
}
UUIDs come from /proc/sys/kernel/random/uuid — available in any Linux container, no extra packages needed. The fallback chain (uuidgen, then timestamp-based) handles edge cases, but in practice the /proc path always works in Docker.
Two-phase invocation
The interesting part is how process_message uses sessions. It’s a two-phase approach:
# Phase 1: try resuming existing session
session_id=$(get_session_id "$chat_id")
if [ -n "$session_id" ]; then
response=$(claude -p \
--resume "$session_id" \
"$text") || session_id=""
fi
# Phase 2: fall back to new session
if [ -z "$session_id" ]; then
session_id=$(new_session "$chat_id")
response=$(claude -p \
--session-id "$session_id" \
"$text")
fi
Phase 1: if a session file exists, try --resume. If Claude can’t resume (session expired, state corrupted), the command fails and session_id gets cleared.
Phase 2: if there’s no session or resume failed, create a new UUID and start fresh with --session-id. The user experiences a memory reset but no error.
The key difference between the two flags: --resume requires an existing session to pick up. --session-id creates or names a new one. Using --resume for the happy path and --session-id for recovery gives us both continuity and resilience.
Commands
Two commands reset the session:
/start— Telegram sends this when a user first opens the bot. Now also resets the session, so restarting the bot conversation starts fresh./new— explicit “forget everything” command. Users can reset when the conversation goes off track.
Both call reset_session() — delete the session file. The next message creates a new UUID automatically.
No infrastructure changes
No new packages in the Docker image. No changes to flake.nix. The sessions directory lives in the agent’s home directory, which is already writable. /proc/sys/kernel/random/uuid is provided by the kernel. The entire feature is ~40 lines of bash.
Result
The agent remembers. Within a conversation, context carries over — you can refer to earlier messages, build on previous answers, ask follow-ups. The agent maintains a coherent thread.
User: Remember 42.
Agent: OK.
User: What number?
Agent: 42.
User: /new
User: What number?
Agent: I don't have any number in mind.
Sessions are ephemeral — they live in the container’s filesystem and don’t survive restarts. For a PoC, this is fine. Persistent volumes or external session stores are future work, and the flat-file design makes migration straightforward: mount a volume at $HOME/sessions/ and sessions survive restarts.
The two-phase invocation handles edge cases gracefully. If Claude’s internal session state expires or corrupts, the agent silently starts fresh rather than returning errors. The user loses context but the conversation continues. This is the right trade-off for a stateless container.
Forty lines of bash. No new dependencies. The agent went from goldfish memory to conversational continuity.