Secretary Mode — your bot answers private chats on your behalf
Mark a bot as a secretary and connect it to a Telegram Premium account’s Telegram Business → Chatbots settings: the engine will answer messages in the private chats you selected, as you — the counterpart sees a normal reply from your account, with zero bot chrome (no status cards, no buttons, no “typing…” theatrics, no reactions).
Enable it
// omnislash.config.ts
bots: [
{ name: 'my-assistant', tokenEnv: 'TELEGRAM_BOT_TOKEN' },
{ name: 'my-secretary', role: 'secretary-stranger', tokenEnv: 'SECRETARY_BOT_TOKEN', secretary: true },
],
my-assistant omits role, so it is the chief’s bot: it carries the engine’s own messages (status notices, approval cards with no chat of their own, restart notices). The secretary runs its own role (at most one bot may omit role) and never carries them.
Then, on your phone: Settings → Telegram Business → Chatbots → pick the bot → choose which chats it may work in and what it may do (at minimum “Reply to messages”). The account you connect must be an owner — in TELEGRAM_ALLOWED_IDS or identities: { tier: owner } in constitution/access.yaml (chat-access.md). An owner connecting the bot IS the authorization: your counterparts need no entry in access.yaml, they can never invoke commands, and no hourly turn limit applies to them. What their turns may REACH is still yours to say: a counterpart’s turn is capped at the bot’s DM ceiling (bots.<bot>.default ⊕ bots.<bot>.dm capabilities in access.yaml — none by default, chat-access.md § The member ceiling), so name what the secretary uses — e.g. dm: { capabilities: [messaging:read] } for the history tools below. Anyone else who connects the bot to their own Business account gets nothing — the connection is recorded and logged, but their chats are never answered, so nobody can borrow your brain’s persona. The check runs on every message: remove someone from the owner tier and their secretary stops answering at once; add them and it starts, no reconnect needed.
What the engine does
- Answers the counterpart in each selected chat, keeping per-chat conversation state. State is keyed by you (the connected owner) — reconnecting or editing the connection never loses it.
- Records your own manual replies without running a turn: the agent sees what you already said, and the reply timestamp feeds the back-off signal below.
- Acts as you for auth: the agent runs with YOUR principal (
tg:user:<your id>), your grants, your memory — never the counterpart’s. - Routes questions to you: when the agent needs a human (a question, an approval), the card goes to YOUR private chat with the bot — the counterpart never sees it.
- Tells you when it fails: if the agent errors while answering, the counterpart sees nothing — no error banners sent in your name. You get a note in your own chat with the bot naming the unanswered chat, so a dropped message is never invisible to you. It reads as one human sentence (“the model provider rejected the credentials”), not as a stack trace. The status channel gets a second copy of the same failure carrying the runtime’s own error text — silenced failures are the one kind nobody could otherwise diagnose. That text is scrubbed of credentials first, but it is raw engine output, so keep the status channel to people you’d show a log to. If your chat with the bot is also the status channel, the two collapse into the one raw message.
- Respects your rights settings: if the connection is disabled or lacks “Reply to messages”, the engine stays silent instead of erroring into the chat. Pausing the bot in one chat (Telegram’s per-chat pause) is respected too.
- Tells the agent where it is: every counterpart turn opens with a context line — whose chat it is (“you reply AS
”), who the counterpart is (name, username, chat id), and when you last replied manually — so the agent never guesses who it’s speaking for or to. - Reads real chat history — with your own account, scoped to that one chat: if you’ve authorized your own Telegram account via
/tg_auth, the secretary’stelegram_history(and friends) automatically use YOUR account for the chat it is answering in — including messages from before the bot was connected. The blast radius is deliberately tiny: from a business chat the agent can only touch that one conversation — other chats, your dialog list and folders, global search, your contact book, joining or creating chats, rawtelegram_invoke*calls, and explicit account switching are all refused; per-person tools (telegram_user_info,telegram_block, adding or removing a contact by user) work on the counterpart only. Your own agent in your DM with the bot keeps full access as usual.
Engagement control
By default a secretary bot answers every counterpart message. The bot’s engage() function sees business messages with an onBehalf signal and can narrow that — the classic move is a back-off window after you replied manually:
// features/secretary/chat-policy.ts — imported by omnislash.config.ts as
// bots: [{ name: 'my-secretary', tokenEnv: 'SECRETARY_BOT_TOKEN', secretary: true, policy: secretaryPolicy }]
import { defineChatPolicy } from '@omnislash-ai/config/public';
export const secretaryPolicy = defineChatPolicy({
engage(s) {
// Stay silent for 10 minutes after I replied in this chat myself.
if (s.onBehalf?.ownerRepliedAt && Date.now() - s.onBehalf.ownerRepliedAt < 10 * 60_000) {
return false;
}
return undefined; // defer to the secretary default (answer)
},
});
s.onBehalf is present only for business messages: { author: 'counterpart', ownerRepliedAt?: number } (epoch ms of your last manual message in THIS chat). Your own messages never reach the policy — they are recorded, not answered. A counterpart reaches engage() as a member (s.tier), with the engine’s DM rule and no commands. See engagement.md for the full contract.
One role per counterpart
One secretary talks to many people, and “how I write to a close friend” ≠ “how I write to a stranger” — and neither is only about wording. Declare secretary as an object to route a role per counterpart: the role file is the whole unit of control, so its body is that person’s persona and its memory:, capabilities and modelRef are that person’s memory line, rights and model.
bots: [
{
name: 'my-secretary',
tokenEnv: 'SECRETARY_BOT_TOKEN',
// The bot's own role IS the stranger's role — everyone you did not name.
role: 'secretary-stranger',
secretary: {
contacts: {
// counterpart Telegram user id → role id
'222222222': 'secretary-alex',
'111111111': 'secretary-maria',
},
},
},
],
---
# features/secretary/secretary-maria.agent.md
modelRef: smart
capabilities: [memory:read, memory:write, messaging:read]
# Her own line: what she tells you never lands in anyone else's envelope,
# and the main line stays reachable when the agent goes looking for it.
memory: { write: maria, read: [main] }
---
You write as the owner, to Maria …
Semantics:
- Routing is deterministic, not model-side. A business chat’s id IS the counterpart’s Telegram user id, and each business chat has its own chat agent — the role is picked by id lookup when that agent is resolved, before any model runs.
- The bot’s
roleis the fallback, i.e. the stranger’s role. There is nodefaultkey: an id you did not name runs the bot’s own role, so give that role the guardrails you want a stranger to meet (its own empty memory line, nomemory:write, narrow capabilities). - Memory, rights, model and flags are per counterpart because they are per role — the existing role mechanisms, nothing new. A close friend can have
memory: { write: <slug>, read: [main] }and full memory capabilities while a stranger has neither. See memory.md for what a line is and howreadwidens it. - Hot-editable, with one exception. The role BODY is read on every turn from the brain checkout, so a brain-sync’d edit reaches the very next message — no redeploy. The id→role binding is not: it lives in
omnislash.config.ts, which is baked into the image, so adding a person is a deploy. - Validated at boot. Shape (
defaultremoved, empty ids, empty role names) fails the config load; a role name no*.agent.mddeclares fails the bot’s startup. Neither can surface as a broken live chat.secretary: trueandsecretary: {}stay valid as bare intent marks.
The role file is the truth for business chats
Every agent reads its role file’s modelRef and capabilities on each turn, so an edit there reaches live agents on their next message. What an agent can keep on top are its own overrides — a model chosen for it (the dashboard form, /model, agent_update) and capabilities added to its role’s. A business-chat agent keeps none: its role is re-routed from secretary.contacts on every message, and the same step clears both overrides, so the config and the role file always win.
The consequence to know before you reach for it: changing a business agent’s model from the dashboard or with /model is undone by the next incoming message. Change modelRef in the role file instead — that is the knob for these agents. Your own DM with the bot keeps a /model you set.
Notes
- Telegram only lets bots reply in chats with incoming messages in the last 24 hours — older chats silently degrade.
/settingsand other chat chrome don’t exist in business chats — control the bot from your own DM with it.- Secretary mode is the Bot API side: Premium, private chats you selected, nothing else. To react to anything your user account sees — groups, channels, chats outside the Business list, edits, deletions, read receipts — hook the
mtproto:message.*events instead (telegram-account.md). A private chat served by both carriessecretaryBound: trueon those events, so a hook can leave it to the secretary.