Skip to content

Sessions

A Session is the universal conversation primitive in aeqi. Multi-participant by default. Subsumes chat, inbox, comments, activity, channels, mentions — every conversation- or activity-shaped concept reduces to it.

Locked 2026-05-02. If you find a separate comments table, subscribers table, watchers table, or notifications table — that's not us. Use Sessions.

What a Session is

A Session has N participants and an ordered stream of messages. Adding someone is an explicit verb (add_participant), not a side effect of mentioning them. Every message has a from_kind (user | agent | role | system) and a from_id.

sessions {
  id, agent_id?, session_type, name, status,
  gateway_channel_id?     -- when bridged through Telegram/email/etc.
}

session_participants {
  session_id, identity_kind, identity_id,
  joined_at, joined_by, history_visibility
}

session_messages {
  id, session_id, from_kind, from_id,
  role, content, payload?, timestamp
}

What Sessions subsume

Concept Implementation
Agent chat Session with [user, agent] participants.
Group chat (founder + CEO + advisor) Session with N participants. Native multi-party.
Channel-bridged conversation (Telegram, WhatsApp, email) Session with gateway_channel_id set + an external participant per remote party.
Inbox item ("agent X is awaiting your reply") Session you're in, has unread + a message with payload.kind=decision_request.
Comments on an Idea Messages in idea.session_id with from_kind ∈ {user, agent, role}.
Activity log on an Idea or Quest Same session, messages with from_kind=system and structured payload.
Subscribers / watchers session_participants rows. Subscribe = add_participant.
@-mentions Mention auto-subscribes; plus a notification ping.
Quest activity (state changes, tool calls, agent execution) System messages in the linked idea's session.

One primitive. Lensed differently.

The three address verbs

There is exactly one verb per intent. No ask_director, no escalate, no notify, no dm, no send.

message_to(target, body, kind?)
add_participant(session_id, target)
mention(target)

message_to finds-or-creates a session for the target and appends. The target is one of:

Target Resolution
{session_id} Append to that session.
{agent_id} The 1:1 session between the caller and that agent.
{user_id} The 1:1 session between the caller and that user.
{role_id} The session for that role. Routes to whoever currently occupies it.
{idea_id} The session attached to that idea (comments).

add_participant is explicit: extending a session's participant set emits a system message ("Alice joined") and starts pushing messages to the new participant.

mention is referenced inline (@alice); it auto-subscribes the target if not already a participant and fires a notification.

Discriminator: `from_kind`

The from_kind discriminates message author types:

  • from_kind=user — a human typed it.
  • from_kind=agent — an agent's LLM produced it.
  • from_kind=role — produced on behalf of a role (e.g., the CEO seat, regardless of who occupies it).
  • from_kind=system — emitted by the runtime (state change, tool call, decision request, activity).

The runtime renders system messages differently — no avatar, no bubble, just an inline activity row. Idea comments lensed by from_kind ∈ {user, agent, role} show only authored messages.

Channel-bridged sessions

A channel (Telegram, WhatsApp, email — see Channels) is a transport, not a chat primitive. When attached to a session via gateway_channel_id, outbound messages dispatch through the channel's transport (Telegram bot, SMTP, etc.), and inbound messages from the remote side fire session.message_received against the bridged session.

Every external party gets a session_participants row with identity_kind=external so the participant strip stays honest.

Role-addressed routing

A session can target a Role rather than a specific occupant (message_to(target=<role_id>)). Messages route to whoever currently occupies the role. If the role turns over, the new occupant inherits the queue. The session itself persists across occupant changes — that's the point.

Awaiting state

A session that needs a reply from a specific participant carries awaiting_at (the timestamp when it was set) and surfaces in their inbox until cleared. The cleared state is implicit: the next message from the awaited participant resets the awaiting marker.

The legacy awaiting_at column will retire in Wave 5; the canonical mechanism is a system message with payload.kind=decision_request plus an awaiting=<participant_id> field.

What Sessions don't cover

  • Group authority. Sessions don't grant permissions; they're the conversation surface. Authority lives in Roles.
  • Treasury. Treasury moves go through proposals or session keys, not session messages.
  • Persistent agent state across restarts. That's Ideas — Sessions are conversation; Ideas are knowledge.
  • Agents — agents talk inside sessions.
  • Roles — role-addressed routing.
  • Quests — Quests have an attached session for activity + tool calls.
  • Channels — the transport layer for bridged sessions.
  • Mention-gating — how channel mentions wake agents.
  • Inline mention spawn — mentioning an unknown name spawns the agent.