Messages, Session keys, and entry points

Corresponding source

SubjectSource
Common message objectsnanobot/bus/events.py::InboundMessage, OutboundMessage
Asynchronous message queuesnanobot/bus/queue.py::MessageBus
Session Key rulesnanobot/session/keys.py; nanobot/agent/loop.py::AgentLoop._effective_session_key
Bus entry and concurrent dispatchnanobot/agent/loop.py::AgentLoop.run, AgentLoop._dispatch
Direct entrynanobot/agent/loop.py::AgentLoop.process_direct

Learning objectives

After this lesson, you should be able to:

  • explain the responsibilities of InboundMessage and OutboundMessage;
  • 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

1. InboundMessage is the common input format

Definition:

  • nanobot/bus/events.py::InboundMessage

Core fields:

FieldMeaning
channelmessage source, such as cli, api, or websocket
sender_idsender identifier
chat_idconversation or channel identifier
contentuser text or system-injected text
timestampinbound timestamp
mediamedia paths or references
metadataChannel/runtime auxiliary data
session_key_overrideexplicit 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_id is not part of the default Session Key

Default Session ownership uses channel and chat_id, not the sender ID. How a group chat, thread, or Channel adapter chooses chat_id directly 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 file

Related source:

  • nanobot/session/keys.py
  • nanobot/agent/loop.py::_effective_session_key
  • nanobot/agent/loop.py::_dispatch

3. OutboundMessage is the common delivery format

Definition:

  • nanobot/bus/events.py::OutboundMessage

Core fields:

  • channel
  • chat_id
  • content
  • reply_to
  • media
  • metadata
  • buttons

metadata may contain:

  • message-routing data;
  • stream/progress markers;
  • latency;
  • Channel-independent structured _agent_ui data.

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 concurrently

A 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 pointTypical key
One-shot CLIexplicit --session, default cli:direct
Channelusually <channel>:<chat_id>
APIapi:<session_id>, default api:default
SDKdetermined by SDK argument or default
Subagent returnoverride 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.channel
  • msg.chat_id
  • msg.session_key
  • session_key
  • self._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

SubjectLocation
Message dataclassesnanobot/bus/events.py
Queuenanobot/bus/queue.py
Session Key helpersnanobot/session/keys.py
Main consumer loopAgentLoop.run
Session serializationAgentLoop._dispatch
Direct entry pointAgentLoop.process_direct
Unified-session teststests/agent/test_unified_session.py
Workspace/Session scopetests/agent/test_workspace_scope.py

Self-check

  1. Why does MessageBus not persist history?
  2. Why must sender_id and chat_id remain distinct?
  3. How can process_direct() still reuse the AgentLoop state machine?
  4. Why should a new message in one Session not immediately start a competing Turn?

Next step

Read AgentLoop state machine.