Concepts / Channel ownership

Channel ownership

When a human talks to an agent over Telegram, their conversation needs to stay glued to one session across many messages — and a scheduled task that messages them later must land in that same thread with context, not start a confusing new one. Channel ownership is how AgentParley keeps a conversation coherent.

Bindings: one conversation, one session

An inbound message arrives on a channel instance (e.g. your Telegram bot tg) in an external conversation (a specific chat). AgentParley maps that (instance, conversation) pair to exactly one session and persists the binding. Every later message in that chat routes to the same session — so the human stays in one continuous thread instead of spawning a fresh agent each time.

text
(instance: tg, conversation: 12345)  ──►  session 1789…  (the owner)

The binding is created atomically the first time a conversation is seen, so two messages arriving at once can't create two sessions for the same chat. That session is the conversation's owner.

The active channel

Each session remembers an active channel — the instance and conversation its replies go to. It's set automatically when a human message comes in, and it's what send_message and an agent's plain final text are delivered to. If a session has no active channel (a silent background run), its output just shows in the console feed.

Reaching a human proactively

What about a session that the human didn't just message — a cron mission that needs to ping them? It can't rely on an active channel being set. AgentParley resolves the agent's last-known conversation on a given instance and points the session there:

text
SetActiveChannelToDefault(session, "tg")
  → looks up the agent's most recent conversation on tg
  → if found, that becomes the session's active channel
  → if the human has never messaged on tg, there's nothing to address (no-op)

Why cron routes through the owner

Here's the problem ownership solves. Say you're mid-conversation with an agent, and an hourly cron job on that same agent checks your inbox and wants to message you. If the cron session just blasted a message, you'd suddenly be "talking to" a different session with none of your dialog's context — and your reply would land in the wrong place.

Instead:

  • A cron job with a notify channel sets its session's active channel to the agent's last-known conversation on that channel.
  • The message is delivered to that conversation — so it reaches you in the right thread.
  • The delivery is recorded as provenance in the owning session's history (see below), so the conversation the human is actually in stays aware of what was said on its behalf.
  • When you reply, the binding routes you back to the owner session — with full context — not to the cron session.

Cross-session delivery provenance

When a session that isn't the owner speaks on a channel, AgentParley appends a note to the owner's conversation log — an assistant message recording what was said on its behalf —without waking the owner. Nothing runs right then; but the next time the owner does run (because the human replied), it already has the context:

text
[owner session's history]
…
assistant: "{the cron's message}"
            — sent to the user on your behalf by the hourly inbox check;
              call get_session("…") for the full context.
This keeps a single human ↔ agent thread coherent across many background tasks, and it's part of why the raw log is never destroyed: provenance is preserved for replay and audit.

Delivery outcome is honest

Outbound delivery reports whether it actually landed. If a session has a real channel target but no recipient conversation (nobody to address), the send fails rather than silently dropping — so an agent is never told "sent" when nothing was. A feed-only session (no external channel) succeeds by rendering in the console.

Building a new channel kind? See the Channels developer guide. This page is about how ownership and routing behave at runtime.