Session files and replay boundaries

Corresponding source

SubjectSource
In-memory Session object and replaynanobot/session/manager.py::Session, Session.get_history
JSONL loading, repair, and atomic savenanobot/session/manager.py::SessionManager
Session Key conventionsnanobot/session/keys.py
Turn read and save call sitesnanobot/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

1. Session object

Definition:

  • nanobot/session/manager.py::Session

Core fields:

FieldMeaning
keyunique Session identifier
messagespersisted messages
created_atcreation time
updated_atlast update time
metadataSession-level Goal, checkpoint, UI, and related state
last_consolidatedmessage 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/*.jsonl

SessionManager.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
  → history

6. 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.

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 directory

Normal 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:

  1. complete two CLI conversation rounds;
  2. locate the matching Session JSONL;
  3. compare Session.messages with file records;
  4. inspect tests calling get_history(max_messages=...);
  5. observe when user timestamps are added;
  6. record metadata keys without modifying the file.

Source and tests

  • nanobot/session/manager.py
  • nanobot/session/keys.py
  • tests/agent/test_session_manager_history.py
  • tests/agent/test_session_atomic.py
  • tests/agent/test_cursor_recovery.py
  • tests/agent/test_session_delete.py
  • tests/agent/test_max_messages_config.py

Self-check

  1. Why does the first line of Session JSONL contain metadata separately?
  2. Why can get_history() not be only messages[-120:]?
  3. Why could assistant timestamps contaminate future replies?
  4. What problem does os.replace solve, and what separate problem does fsync solve?
  5. Why must last_consolidated be range-checked?

Next step

Read Compaction, Memory, and Dream.