Multi-chat support: one Orchestrator per authorized chat#93
Merged
Conversation
Refactor sessions.json from { mainSessionId } to
{ mainSessions: Record<string, string> } keyed by chat name, with
legacy migration that promotes any existing `mainSessionId` to
`mainSessions.admin` on load.
Introduce getMainSession/setMainSession/clearMainSession helpers so
future multi-chat code touches only one slot at a time. Orchestrator
still passes `"admin"` implicitly — behavior unchanged.
Preparation for issue #91.
Rename the settings field and its env var to make room for additional authorized chats: - settings.json: chatId -> adminChatId - env var: AUTHORIZED_CHAT_ID -> ADMIN_CHAT_ID - App config: authorizedChatId -> adminChatId - Setup wizard: "Admin chat ID" prompt, notes /chats-add Legacy fallbacks kept for one release: - settings.json with `chatId` auto-migrates to `adminChatId` on load - `AUTHORIZED_CHAT_ID` env var still honored when `ADMIN_CHAT_ID` is unset (logs a deprecation warning) No behavior change — single-chat users keep working. Preparation for issue #91.
Add src/authorized-chats.ts + tests. Loads, mutates, and persists
~/.macroclaw/authorized-chats.json. Stores additional chat IDs beyond
the admin chat, each with a human-readable name used for session
bookkeeping, cron routing, and memory tagging.
- AuthorizedChat: { chatId, name, addedAt }
- Name constraints: lowercase alphanumeric + dashes, unique,
"admin" reserved
- add() / remove() / byName() / byChatId() / list()
- Typed errors: DuplicateChatError, InvalidChatNameError,
UnknownChatError
- Missing or corrupt file → empty list with a warn log
Not wired into any consumer yet.
Preparation for issue #91.
Each authorized chat gets its own Orchestrator, keyed by chatId in a Map. Admin orchestrator is always created first. Incoming messages resolve to the right orchestrator by chatId, with unauthorized chats silently ignored.
Admin-only commands to manage authorized chats at runtime. /chats-add creates an Orchestrator for the new chat immediately. /chats-remove disposes the Orchestrator and clears its session entry.
Jobs can now target a specific chat via the 'chat' field. Default (omitted) routes to admin. '*' broadcasts to all orchestrators. App routes cron jobs using #orchestratorByName; unknown chats are warned.
When chatName is provided (all production orchestrators), every prompt is prepended with <chat>name</chat> so the agent knows which chat context it is responding in.
Update architecture diagram, App layer description, and conventions to reflect multi-orchestrator design with AuthorizedChats and per-job chat routing in the Scheduler.
72f9569 to
87ac2db
Compare
Telegram bot commands cannot contain hyphens. Renamed /chats-add → /chatsadd and /chats-remove → /chatsremove (matching the menu entries). /chatsremove now supports three modes: - with a name arg (admin only): remove that chat - without arg from admin: pick-list buttons for each authorized chat - without arg from a non-admin authorized chat: self-removal
chatName is now a required constructor arg, rendered as a chat="..." attribute on every <event> element (not a separate <chat> sibling). System prompt documents the multi-chat model and the chat attribute so the agent tailors its response per chat.
0fd50ff to
d3505d7
Compare
The /chatsremove button-picker added in this PR exercises a code path
that triggers Bun's static ESM-binding check against the mocked
grammy module. Previous mocks only exposed Bot, which worked on main
because the button-picker path wasn't reached. With only Bot mocked,
telegram.ts's `import { InlineKeyboard, InputFile }` fails at load
time under the mock, aborting app.test.ts entirely (app tests silently
drop out of the suite, visible only as "Unhandled error between tests"
in the bun test log).
Mock both symbols so telegram.ts loads cleanly under mock.module.
e5edcbe to
c7b0492
Compare
- /chatsadd rejects a chatId that matches the admin chat. Without this, adding such a chat overwrites the admin entry in the #orchestrators map, and removing it later deletes the admin mapping entirely. - Setup wizard referenced /chats-add (hyphen); the real command is /chatsadd.
bcf5994 to
f65fe6f
Compare
- README: document /chats, /chatsadd, /chatsremove; rename chatId to adminChatId with legacy env-var note; add a Privacy Mode section explaining why bots appear silent in groups until it is disabled via BotFather. - schedule-event skill: add the chat field to job examples and the fields table, with a Chat routing section explaining how to read the incoming event chat attribute and route the response back to the same chat (or * to broadcast).
… admin memory-capture now runs per-chat (chat: "*") so each authorized chat's independent session writes its own daily log. memory-consolidate stays pinned to admin — it distills into MEMORY.md/USER.md, which are shared workspace files and shouldn't be written from multiple chats.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #91
Summary
mainSessionId(with legacy migration)chatId→adminChatId;AUTHORIZED_CHAT_IDenv var still accepted as fallbackAuthorizedChats) to load/persist authorized chat list at runtimeOrchestratorper chat in aMap<chatId, Orchestrator>; incoming messages routed by chatId/chats,/chatsadd <chatId> <name>,/chatsremove [name]— names use Telegram-valid characters (no hyphens). Behaviour:/chats— admin only, lists authorized chats/chatsadd— admin only, creates the Orchestrator and sessions slot/chatsremove <name>— admin only, disposes the Orchestrator and clears sessions/chatsremove(no arg, from admin) — shows a pick-list button for each authorized chat/chatsremove(no arg, from an authorized non-admin chat) — self-removalchatfield;undefined→ admin,"*"→ broadcast to all orchestrators;App.handleCronis the public routing entrypoint (no test-onlyschedulerFactory)chatNameconstructor arg, rendered aschat="..."attribute on every<event>element. System prompt documents the multi-chat model and the chat attribute so the agent tailors responses per chatTest plan
bun run check)/chatsadd, send a message, verify it gets a response/chatsremovewith no arg shows chat-picker buttons/chatsremove(no arg) self-removeschat: "*"broadcasts to all authorized chats