Session files and replay boundaries
Corresponding source
| Subject | Source |
|---|---|
| In-memory Session object and replay | nanobot/session/manager.py::Session, Session.get_history |
| JSONL loading, repair, and atomic save | nanobot/session/manager.py::SessionManager |
| Session Key conventions | nanobot/session/keys.py |
| Turn read and save call sites | nanobot/agent/loop.py::AgentLoop._state_build, _save_turn, _state_save |
Learning objectives
After this lesson, you should be able to:
- read Session JSONL;
- explain metadata, messages, and
last_consolidated; - explain why Session does not replay every disk record unchanged;
- understand the basic strategies for atomic persistence and corruption recovery.
Prerequisites
- Understand JSONL.
- Read the SAVE stage of One Turn.
- Recommended: Files, JSON Lines, and stream processing.
1. Session object
Definition:
nanobot/session/manager.py::Session
Core fields:
| Field | Meaning |
|---|---|
key | unique Session identifier |
messages | persisted messages |
created_at | creation time |
updated_at | last update time |
metadata | Session-level Goal, checkpoint, UI, and related state |
last_consolidated | message boundary already archived to Memory history |
If last_consolidated is corrupted or out of range, __post_init__ resets it so an invalid offset cannot hide all history.
2. File location
The default location is:
<workspace>/sessions/*.jsonlSessionManager.safe_key() converts a Session Key to a safe filename. The filename alone does not define ownership; the metadata record still stores the original key.
3. JSONL structure
The first saved line is a metadata record. Each following line is one message:
{"_type":"metadata","key":"cli:direct","created_at":"...","updated_at":"...","metadata":{},"last_consolidated":0}
{"role":"user","content":"Hello","timestamp":"..."}
{"role":"assistant","content":"Hello!","timestamp":"..."}Verify actual fields in the current SessionManager.save().
Advantages of this format:
- records can be read line by line;
- recovery can preserve valid lines after corruption;
- list views can read only metadata and a small preview;
- no need to parse one enormous JSON array.
4. add_message() only appends
Session.add_message():
- adds a role;
- adds content;
- adds a timestamp;
- merges auxiliary keyword arguments;
- updates
updated_at.
It does not handle:
- complete schema validation;
- Tool Call boundaries;
- context budgets;
- disk writes.
Upper layers legalize and persist the result.
5. get_history() returns a replayable view
A Session may persist many messages, but get_history() returns history suitable for ContextBuilder according to:
last_consolidated;max_messages;max_tokens;- user/tool boundaries;
- Tool Call pairing;
- command/internal-message filtering;
- timestamp options;
- media/CLI/MCP breadcrumbs.
Session.messages
→ exclude the consolidated prefix
→ select recent messages/tokens
→ locate a legal user starting point
→ repair Provider replay
→ optionally annotate user timestamps
→ history6. Why only user messages receive time annotations
_annotate_message_time() prefixes only user Turns with time.
The source comment explains why:
- if every historical assistant message included
[Message Time: ...]; - the model might imitate that format in its own replies through in-context learning;
- internal metadata could then leak to the user.
A user timestamp provides enough relative context for the adjacent assistant response.
This is a practical example of context content shaping current model behavior.
7. Legal suffix
retain_recent_legal_suffix() keeps the most recent replayable fragment after hard trimming. A legal suffix cannot:
- begin with an orphaned tool result;
- leave an assistant Tool Call without its result;
- break user/assistant/tool protocol boundaries.
“Keep the last N messages” is therefore not a simple list slice.
8. Atomic persistence
SessionManager.save() uses:
write .jsonl.tmp
→ flush
→ optionally fsync file
→ os.replace
→ optionally fsync directoryNormal runtime saves do not necessarily fsync every time. Graceful shutdown or an explicit durable save can enable it.
This ensures:
- a normal write does not truncate the target file in place;
- replace is atomic on the same filesystem;
- a failed temporary write is cleaned up.
Directory fsync may be skipped on Windows when permissions prevent it.
9. Corruption recovery
When loading, SessionManager:
- parses line by line;
- skips or records corruption;
- attempts to recover valid content;
- avoids dropping every entry from a read-only list/metadata view because one record is damaged.
Recovery does not make disk corruption harmless. Preserve the original file during debugging; do not clear it first.
10. Possible Session metadata
Common internal state includes:
goal_state;runtime_checkpoint;pending_user_turn;_last_summary;- workspace scope;
- WebUI title/display state;
- continuation count.
Most of these keys are internal contracts. Extensions should not write the same names casually.
Observation task
In the isolated learning workspace:
- complete two CLI conversation rounds;
- locate the matching Session JSONL;
- compare
Session.messageswith file records; - inspect tests calling
get_history(max_messages=...); - observe when user timestamps are added;
- record metadata keys without modifying the file.
Source and tests
nanobot/session/manager.pynanobot/session/keys.pytests/agent/test_session_manager_history.pytests/agent/test_session_atomic.pytests/agent/test_cursor_recovery.pytests/agent/test_session_delete.pytests/agent/test_max_messages_config.py
Self-check
- Why does the first line of Session JSONL contain metadata separately?
- Why can
get_history()not be onlymessages[-120:]? - Why could assistant timestamps contaminate future replies?
- What problem does
os.replacesolve, and what separate problem does fsync solve? - Why must
last_consolidatedbe range-checked?