Skip to content

feat: bot-per-persona β€” each persona as its own Telegram bot (#29)#39

Open
mattmezza wants to merge 6 commits into
mainfrom
feat/bot-per-persona
Open

feat: bot-per-persona β€” each persona as its own Telegram bot (#29)#39
mattmezza wants to merge 6 commits into
mainfrom
feat/bot-per-persona

Conversation

@mattmezza

Copy link
Copy Markdown
Owner

Closes #29.

Run each persona as its own Telegram bot β€” its own contact in your address book. The persona that handles a message is determined by which bot received it.

Design β€” one composite channel name

A persona-bot registers as agent.channels["telegram:<persona>"]; the default bot stays bare telegram. Inbound passes channel="telegram:<persona>" to process(). That one convention does three jobs at once:

  1. History isolation β€” in a Telegram DM chat_id == user_id for every bot, so the composite name is what stops two bots' DMs with the same user from sharing a (channel, user, chat) key.
  2. Outbound + approval routing β€” agent.channels["telegram:<persona>"].send(...), scheduler job.channel="telegram:<persona>", and approvals reply via the bot that fired them (the channel string is threaded from process() straight to _await_approval β†’ self.channels.get(channel)).
  3. Persona resolution β€” telegram:<name> β†’ persona <name> with no lookup (new top rung of the _resolve_persona ladder from Per-chat persona binding + isolated contexts (channel-agnostic)Β #14).

What changed

  • Persona model β€” optional bot_token (+ optional allowed_user_ids, else inherit the global list). Empty token β‡’ not a bot, reachable only via the default bot. Stored on the personae table (new columns + migration for existing DBs); round-trips through markdown frontmatter and the admin editor.
  • main.py β€” same process, N bots: the default bot plus one TelegramChannel per persona that carries a token, each polled in-process. One bad/revoked/duplicate token can't take down the others (or WhatsApp/the scheduler); partial bring-up failure tears down already-started bots so none are left orphaned.
  • _resolve_persona β€” top rung parses telegram:<name>; falls through to the existing ladder for unknown names.
  • Scheduler/jobs β€” channel accepts telegram:<persona> (back-compat: bare telegram = default bot, no migration). A persona job is generated as that persona while keeping the system execution mode (auto-approved writes, no memory/reflection).
  • Admin UI β€” an Own Telegram bot card on the persona editor (token + allowed users); the token is redacted on read like the global one.
  • Docs β€” personae.mdx (new Bot-per-persona section, rung 0, roadmap repointed to Group multi-agent rooms β€” turn-taking + loop guardΒ #30), channels.mdx, README.

Out of scope (per the issue)

Group multi-agent / turn-taking (#30); per-bot config beyond token + allowed-users.

Quality

Built understanding-first, then implemented, then ran two multi-agent adversarial review passes:

  • Review β€” 4 reviewer lenses (correctness, issue-completeness, concurrency/lifecycle, security), each finding independently verified against the real code. 13 confirmed findings; the substantive ones are fixed in this branch (startup-abort/orphan-poller hardening, persona-correct scheduled jobs, token redaction, token dedup, no cross-bot progress spam, no dead topic auto-bind on persona bots).
  • Verify β€” 6 adversarial probes over the fixes: 0 broken, 0 regressions (it actually removed two pre-existing shutdown/startup bugs). GO.

All 418 tests green, ruff clean. New tests cover composite-channel resolution, persona-bot field persistence + token redaction, and persona-correct scheduled delivery.

@mattmezza mattmezza force-pushed the feat/bot-per-persona branch from d9f1c92 to da0a145 Compare June 27, 2026 21:55
Persona gains optional Telegram identity fields: bot_token (its own bot)
and allowed_user_ids (per-bot ACL; empty inherits the global list). Schema
migration adds the columns to existing personae DBs. Round-trips through
markdown frontmatter and the store.
)

Adds rung 0 to _resolve_persona: a "telegram:<persona>" channel binds
straight to that persona β€” the bot that received the message is the
persona. TelegramChannel now carries a channel_name (default "telegram")
that it reports to the agent on every inbound message and topic bind, so
history, persona resolution and approval routing all silo per bot.
…nds (#29)

main: after the default bot, instantiate one TelegramChannel per persona
that carries a bot_token, registered as channel "telegram:<persona>" and
polled in-process; shutdown now stops every telegram bot. scheduler:
_get_owner_chat_id resolves the owner for telegram:<persona> jobs from the
bot's own allowlist (global fallback). Bare "telegram" unchanged.
Persona editor gains an "Own Telegram bot" card (token + allowed user IDs);
PersonaUpsertIn carries the fields and the upsert parses the ACL into ints.
Round-trips through both guided and raw markdown modes.
personae.mdx: new Bot-per-persona section, rung 0 in the resolution ladder,
roadmap repointed to group multi-agent (#30). channels.mdx: per-persona bot
note under Telegram. README: persona feature line.
…t tokens

Adversarial review fixes:
- main: one bad/duplicate persona token no longer aborts startup β€” each bot
  starts independently; tokens are deduped; partial-failure tears down already
  -started bots so none are left orphaned polling. Shutdown stops every bot
  independently (one failure can't strand the rest).
- scheduler/agent: a telegram:<persona> job is generated AS that persona via a
  new process(persona_name=…) override, keeping the 'system' execution mode
  (auto-approved writes, no memory/reflection) β€” fixes jobs being written in
  the default identity.
- admin: persona bot_token is redacted on read (like the global token) and not
  leaked via the raw markdown view.
- telegram: persona bots skip topic auto-bind (rung 0 ignores it) and the global
  browser-status mirror (no cross-bot progress spam).
@mattmezza mattmezza force-pushed the feat/bot-per-persona branch from da0a145 to a599f90 Compare June 27, 2026 22:29
@mattmezza

Copy link
Copy Markdown
Owner Author

The real test: two Telegram bots (the only true end-to-end)

  1. Two tokens from @Botfather. Keep one as your default (channels.telegram.bot_token); the second goes on a persona.
  2. Run the agent locally: make dev-agent (uvicorn + admin UI at http://localhost:8000/admin).
  3. Admin UI β†’ edit a persona (e.g. coach) β†’ Own Telegram bot β†’ paste the second token. Leave Allowed user IDs blank to inherit the global allowlist β€” but make sure your Telegram ID is in the global allowlist, or the bot will silently ignore you (_is_allowed). Save.
  4. Restart the agent β€” tokens are read once at startup (_start_agent's bot loop), so a persona-token change needs a restart: the admin "Restart now" button (it re-runs _start_agent β†’ re-reads the personae DB β†’ spins up the new bot), or Ctrl-C + rerun.
  5. You now have two contacts in Telegram. Verify the four guarantees:
  • Resolution β€” DM the coach bot β†’ it answers as the coach (voice/identity/scoped tools).
  • History isolation (the headline fix) β€” tell the default bot a fact, then ask the coach bot about it β†’ it won't know. Both DMs have chat_id == user_id, so the telegram:coach channel is the only thing keeping them apart.
  • Approval routing β€” ask the coach bot to do a write action (one that needs approval) β†’ the Approve/Deny buttons arrive on the coach bot, not the default.
  • Scheduler β€” create a job with delivery channel telegram:coach (admin Jobs tab, or tools/jobs.py with --channel telegram:coach) β†’ delivered by the coach bot, written as the coach.
  1. Resilience (optional β€” exercises the review-hardening): set a persona's token to garbage β†’ restart β†’ the default + other bots still come up (log: Failed to start Telegram bot telegram:coach β€” skipping). Give two personas the same token β†’ the second is skipped with a warning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bot-per-persona β€” each persona as its own Telegram bot

1 participant