Cross-channel send (copy & redirect)¶
A session that came in over one channel can deliver output through any other channel —
answer a Discord message but drop the file in a Telegram chat, mirror a status update to a
team channel, or reroute the whole reply somewhere else. This is the assistant's outbound
control plane: one unified POST /send on the connector manager that routes to whichever
connector owns the destination, and one CLI verb (asmltr send) that the agent can call from
inside a turn.
How this differs from an inject
Cross-channel send is something the session itself does during a turn (it runs
asmltr send as a tool call). An operator inject is something an
outside operator does to a session. Both ultimately hit the same POST /send, but the
trigger is different.
The unified POST /send (connector manager, 127.0.0.1:3024)¶
Every connector type that can emit messages exposes an /out endpoint and advertises
meta.outbound in its type metadata. The manager's POST /send is a channel-agnostic front
door over all of them: you name a destination and a payload, and it forwards to the right
connector's /out.
// request — pick a destination by channel OR by a specific instance_id
{ "channel": "discord", // connector TYPE; first enabled instance of it is used
"instance_id": "…", // OR target one specific connector instance directly
"target": "123456789", // channel/chat id (or an alias the connector understands)
"kind": "text", // "text" | "file"
"text": "shipping now", // for kind:"text"
"path": "/root/report.pdf", // for kind:"file" — absolute path on the host
"caption": "the report" } // optional caption for a file
// response
{ "ok": true, "via": "discord:main", "…": "connector-specific result fields" }
Resolution rules (from deliver() in connectors/manager/server.js):
instance_idwins if present; otherwisechannelselects the first enabled instance of that type (falling back to the first instance of the type if none are enabled).- The type must declare
meta.outbound, and the instance must have anhttp_port— otherwise you get a400explaining which is missing. An unreachable connector returns502. GET /send/targetslists every outbound-capable destination (channel, instance id, name, enabled flag) — handy for a dashboard picker.
Attachments are channel-gated
kind: "file" only works on connectors whose type advertises attachment support
(surfaced to the agent as supports_attachments_out). Text sends work on any
outbound-capable connector.
The agent verb: asmltr send¶
Inside a turn, the session reaches this through the asmltr CLI (via the Bash tool). The core
tells every session about it in the ASMLTR TOOLBELT
block of its system prompt, so it knows the verb exists without being told each time.
# text to another channel
asmltr send <channel> <target> "<text>"
asmltr send discord 123456789 "shipping now"
# a file/attachment (image, PDF, any file) on a channel that supports it
asmltr send <channel> <target> --file <abs-path> [--caption "…"]
asmltr send discord 123456789 --file /root/report.pdf --caption "the report"
asmltr send (see cmdSend in cli/asmltr.js) is a thin wrapper: it parses --file /
--caption, builds the { channel, target, kind, text|path, caption } body, and POSTs it to
the manager's /send (attaching ASMLTR_MANAGER_TOKEN as a bearer if set). On success it
prints ✓ sent … (<via>); on failure it prints the error the manager returned.
Two patterns: copy vs. redirect¶
The channel where a message arrived replies automatically at the end of a turn — the core
turns the model's final text into a reply action for that connector. asmltr send posts
elsewhere. Combining the two gives you two distinct behaviours:
Copy — post here and there¶
Run asmltr send, then reply normally. The send delivers to the other channel; the normal
reply still posts on the origin channel.
User (in Discord): "let the team channel know the build passed"
Agent:
1. Bash: asmltr send discord <team-channel-id> "✅ build passed"
2. Reply normally: "Done — I posted it to the team channel."
Result: the team channel gets the announcement and the user sees a confirmation where they asked.
Redirect — post only there¶
Run asmltr send, then end the turn with exactly [[NO_REPLY]]. The core recognizes that
sentinel and returns no actions, so the origin connector stays silent — the answer lives
only on the channel you redirected to.
User (in Telegram): "put the full report in the #reports Discord channel"
Agent:
1. Bash: asmltr send discord <reports-channel-id> --file /root/report.pdf --caption "monthly report"
2. Reply with exactly: [[NO_REPLY]]
Result: the file lands in Discord; nothing is echoed back into Telegram.
[[NO_REPLY]] is universal
The sentinel is handled in the core's handle() pipeline, not in any one connector:
Because it short-circuits to an empty action list, every connector stays quiet — not
just Discord. It's the general "I already delivered my answer elsewhere, post nothing here"
signal. (An operator-aborted turn returns [] the same way — see
Session steering & injection.)
Making the agent aware it can attach here¶
When the origin channel itself supports attachments, the core adds an explicit note to the system prompt with the exact command (pre-filled with this channel's id):
ATTACHMENTS: THIS channel supports sending files. To attach a file HERE, write/produce it to a
path, then run `asmltr send <channel> <this-channel-id> --file <abs-path> [--caption "…"]`.
Do NOT tell the user you can't attach files here …
So on an attachment-capable channel the agent uses asmltr send to attach to the current
conversation too — it never has to claim it can't send a file or fall back to another channel.
Gating¶
Cross-channel awareness is part of the toolbelt block, controlled by the same switches:
| Env var | Effect |
|---|---|
ASMLTR_SELF_AWARE |
off removes the whole ASMLTR TOOLBELT block (including the asmltr send instructions) from every system prompt. Any other value (or unset) keeps it on. |
ASMLTR_CROSS_CHANNEL |
Intended gate for cross-channel routing; keep it enabled where you want sessions to route out through other connectors. |
ASMLTR_MANAGER_TOKEN |
Bearer required by POST /send when set; asmltr send attaches it automatically. |
Redaction still applies to the origin reply
asmltr send payloads go straight through the manager to the target connector. The
origin-channel reply (the normal text, or its absence under [[NO_REPLY]]) still passes
through the core's output redaction layer like any other reply. Prefer [[NO_REPLY]] +
asmltr send when you want a single clean delivery rather than a copy on both surfaces.
Where it lives¶
| Piece | File |
|---|---|
Unified POST /send → connector /out |
connectors/manager/server.js (deliver()) |
GET /send/targets (outbound destinations) |
connectors/manager/server.js |
asmltr send CLI verb |
cli/asmltr.js (cmdSend) |
[[NO_REPLY]] sentinel + toolbelt prompt |
core/src/server.js (handle()) |
See also¶
- Cross-session announcements — awareness notes between sessions (no channel side effects).
- Session awareness (ls / map / who) — see what other sessions are doing before duplicating work.
- Session steering & injection — an operator reaching into a live session.