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 ⊕ the counterpart’s own bots.<bot>.people."tg:user:<id>" ceiling in access.yaml — none by default, chat-access.md § The member ceiling), so name what the secretary uses — e.g. dm: { ceiling: [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>) and your grants — never the counterpart’s. Its memory is the counterpart’s session’s: the chat is a person session (memory.md § Memory per person and per room), so the role’s{person}lines resolve to the counterpart, your own lines and corpora stay out unless the counterpart’s rule grants them, and memory is on only when that rule’s ceiling holdsmemory:read. A Business chat with another owner is an owners’ session. - 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. Give a counterpart a role of their own in constitution/access.yaml, on their own rule: 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, rights and model. The same rule carries what of YOUR memory their chat may reach.
// omnislash.config.ts
bots: [
// The bot's own role IS the stranger's role — everyone without a role of their own.
{ name: 'my-secretary', tokenEnv: 'SECRETARY_BOT_TOKEN', role: 'secretary-stranger', secretary: true },
],
# constitution/access.yaml
bots:
my-secretary:
dm: { ceiling: [memory:read, memory:write, messaging:read] } # every counterpart's ceiling — cuts the role's grant
people:
"tg:user:111111111": # a counterpart's Telegram user id
role: secretary-maria
memory: { read: [main, people/maria] } # what of yours her chat may reach
"tg:user:222222222": { role: secretary-alex }
---
# 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.
memory: { write: maria, recall: [] }
---
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 that id’s
peopleentry when the agent is resolved, before any model runs. - The bot’s
roleis the fallback, i.e. the stranger’s role: a counterpart with no entry, or with an entry that names no role, runs it (dm.rolenever serves a Business chat). Give that role the guardrails you want a stranger to meet — and a templated line,write: secretary-{person}, so each stranger keeps a line of their own instead of sharing one (doctor Chat access warns on a literal one). - Your memory reaches a counterpart only by a grant. The chat is a person session, so whatever the counterpart’s role names outside its own lines and its own feature directory (
main,content,people/maria) is dropped there; thememorygrant on theirpeopleentry (or ondm) is what their chat may reach. Put a person’s own folder there, not in the role: a grant on one person’s rule cannot leak into another’s chat. See memory.md § Memory per person and per room. - Rights and model are per counterpart because they are per role, and the ceiling is per person too: the
ceilingofdefault⊕dm⊕ theirpeopleentry. The role file’scapabilitiesgrants; the rule’sceilingonly cuts it. In a Business chat apeopleentry’scommands,limitanddeclinenever apply — a counterpart runs no commands, has no turn budget and is never declined. - Hot-editable. The role body and
access.yamlare both read on every message from the brain checkout, so adding a person or changing their grants reaches the very next message — no redeploy. Adding a grant or a role is a widening change: the agent asks you first (access_widen). - Validated on every load. A role no
*.agent.mddeclares, or a grant naming an undeclared source, fails the file’s load (at boot, or refused at runtime with the last valid file kept). The oldbots[].secretary.contactsinomnislash.config.tsfails the config load naming its new place;secretaryistrueorfalse.
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 access.yaml on every message, and the same step clears both overrides, so the access file 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.