Messages, Session keys, and entry points
Corresponding source
| Subject | Source |
|---|---|
| Common message objects | nanobot/bus/events.py::InboundMessage, OutboundMessage |
| Asynchronous message queues | nanobot/bus/queue.py::MessageBus |
| Session Key rules | nanobot/session/keys.py; nanobot/agent/loop.py::AgentLoop._effective_session_key |
| Bus entry and concurrent dispatch | nanobot/agent/loop.py::AgentLoop.run, AgentLoop._dispatch |
| Direct entry | nanobot/agent/loop.py::AgentLoop.process_direct |
Learning objectives
After this lesson, you should be able to:
- explain the responsibilities of
InboundMessageandOutboundMessage; - describe how the default Session Key is created;
- distinguish the MessageBus path from the
process_direct()path; - locate where unified sessions and overrides affect Session ownership.
Prerequisites
- Understand
dataclass. - Understand Queue.
- Read Five entry points and their runtime boundaries.
1. InboundMessage is the common input format
Definition:
nanobot/bus/events.py::InboundMessage
Core fields:
| Field | Meaning |
|---|---|
channel | message source, such as cli, api, or websocket |
sender_id | sender identifier |
chat_id | conversation or channel identifier |
content | user text or system-injected text |
timestamp | inbound timestamp |
media | media paths or references |
metadata | Channel/runtime auxiliary data |
session_key_override | explicit override of default Session ownership |
Default property:
session_key_override or f"{channel}:{chat_id}"Session is therefore isolated by “entry-point type + conversation identifier” by default.
sender_idis not part of the default Session KeyDefault Session ownership uses
channelandchat_id, not the sender ID. How a group chat, thread, or Channel adapter chooseschat_iddirectly affects context isolation.
2. The effective Session Key may change again
InboundMessage.session_key is the default supplied by the message object, but AgentLoop also computes an effective Session Key for:
- unified sessions;
- thread-scoped overrides;
- Subagents or internal continuations;
- explicit session IDs from selected runtime surfaces.
When debugging crossed Session history, observe all four:
msg.session_key
effective_session_key
the key actually read by SessionManager
metadata.key in the disk fileRelated source:
nanobot/session/keys.pynanobot/agent/loop.py::_effective_session_keynanobot/agent/loop.py::_dispatch
3. OutboundMessage is the common delivery format
Definition:
nanobot/bus/events.py::OutboundMessage
Core fields:
channelchat_idcontentreply_tomediametadatabuttons
metadata may contain:
- message-routing data;
- stream/progress markers;
- latency;
- Channel-independent structured
_agent_uidata.
Do not assume that this internal metadata is a stable public API.
4. MessageBus is only two Queues
nanobot/bus/queue.py::MessageBus maintains:
inbound: Queue[InboundMessage]
outbound: Queue[OutboundMessage]Its responsibilities are deliberately small:
- publish inbound messages;
- consume inbound messages;
- publish outbound messages;
- consume outbound messages;
- expose queue lengths.
It does not handle:
- Session persistence;
- message replay;
- runtime events;
- Provider calls;
- Channel permissions;
- retries.
This narrow responsibility decouples Channels from the Agent core.
5. Two ways to enter AgentLoop
Path A: MessageBus
flowchart LR CH["Channel / interactive CLI"] --> IN["bus.publish_inbound"] IN --> RUN["AgentLoop.run"] RUN --> DIS["_dispatch"] DIS --> PM["_process_message"] PM --> OUT["bus.publish_outbound"] OUT --> CM["ChannelManager / CLI consumer"]
AgentLoop.run():
- continuously consumes inbound messages;
- handles priority commands and runtime control;
- puts a new message for an active Session into its pending queue;
- creates a Task for other messages;
- lets
_dispatch()acquire the Session lock.
Path B: process_direct()
flowchart LR E["CLI -m / API / SDK"] --> PD["process_direct"] PD --> LOCK["Session lock"] LOCK --> PM["_process_message"] PM --> RET["Return result directly"]
This path still reuses the same _process_message() and state machine. It simply does not depend on a Channel’s inbound/outbound dispatcher.
6. Serialization within one Session
AgentLoop._dispatch() acquires a separate asyncio.Lock for each effective Session Key:
Same key → same lock → serialized Turns
Different keys → different locks → may run concurrentlyA global concurrency gate also limits the total number of requests handled at once.
When a new message arrives while a Turn is active in the same Session, it may enter that Session’s pending queue instead of starting a competing Turn. AgentRunner can inject it later at a safe boundary.
7. Session conventions by entry point
| Entry point | Typical key |
|---|---|
| One-shot CLI | explicit --session, default cli:direct |
| Channel | usually <channel>:<chat_id> |
| API | api:<session_id>, default api:default |
| SDK | determined by SDK argument or default |
| Subagent return | override aligned with the parent Session |
Do not assume that CLI, API, and WebUI automatically share history. They can share it only when they resolve to the same effective key or unified session is enabled.
8. Observation task
Set a breakpoint in AgentLoop.process_direct() or _dispatch() and inspect:
msg.channelmsg.chat_idmsg.session_keysession_keyself._session_locks.keys()self._pending_queues.keys()
Run:
nanobot agent -c $nanobotConfig -w $nanobotWorkspace -s "cli:lesson-a" -m "Remember A"
nanobot agent -c $nanobotConfig -w $nanobotWorkspace -s "cli:lesson-b" -m "What did I ask you to remember?"Expected result: the second Session should not depend on the first Session’s conversation history.
Source and tests
| Subject | Location |
|---|---|
| Message dataclasses | nanobot/bus/events.py |
| Queue | nanobot/bus/queue.py |
| Session Key helpers | nanobot/session/keys.py |
| Main consumer loop | AgentLoop.run |
| Session serialization | AgentLoop._dispatch |
| Direct entry point | AgentLoop.process_direct |
| Unified-session tests | tests/agent/test_unified_session.py |
| Workspace/Session scope | tests/agent/test_workspace_scope.py |
Self-check
- Why does MessageBus not persist history?
- Why must
sender_idandchat_idremain distinct? - How can
process_direct()still reuse the AgentLoop state machine? - Why should a new message in one Session not immediately start a competing Turn?
Next step
Read AgentLoop state machine.