Skip to content

refactor: clean up Rich Message receive implementation#95

Open
changhansung wants to merge 349 commits into
suzuke:mainfrom
songsid:fix/tg-rich-message-cleanup
Open

refactor: clean up Rich Message receive implementation#95
changhansung wants to merge 349 commits into
suzuke:mainfrom
songsid:fix/tg-rich-message-cleanup

Conversation

@changhansung

Copy link
Copy Markdown
Contributor
  • Remove last (msg as any).rich_message → use proper grammy 1.44.0 type
  • Verified: extractRichText handles paragraph/heading/pre/table correctly
  • Verified: middleware catches rich-only messages, handler has fallback
  • Verified: isBotMessage set in both paths
  • No debug logs remain
  • Build clean, no warnings

1. classic-channel-manager.ts:
   - ClassicChannel.collab field (persisted to classicBot.yaml)
   - toggleCollab(channelId): toggle + save, returns new state
   - isCollab(channelId): check collab status

2. discord-adapter.ts:
   - Register /collab slash command
   - Only block own messages (not all bots) — let fleet-manager decide
   - Pass isBotMessage flag in message emit
   - Emit bot user ID with 'started' event

3. channel/types.ts:
   - InboundMessage.isBotMessage?: boolean

4. fleet-manager.ts:
   - /collab handler: admin-only toggle, responds with new state
   - handleInboundMessage: bot messages only pass through to collab channels
   - handleClassicChannelMessage collab mode:
     - Log all messages (including bots) to chat-logs
     - Trigger on exact @mention (<@BOT_USER_ID>) only
     - @everyone/@here/@ROLE don't trigger (not <@id> format)
     - Strip @mention, forward clean text to agent
   - Store botUserId from adapter 'started' event
Part 1: pre_task_command
- types.ts: InstanceConfig.pre_task_command?: string
- classic-channel-manager.ts: ClassicChannel.preTaskCommand, load/save,
  getPreTaskCommand(channelId)
- daemon.ts: in pasteLock, deliverMessage(pre_task_command) before the
  actual message (waits for idle between both)
- fleet-manager.ts: startClassicInstance passes preTaskCommand from
  classicChannels config into InstanceConfig

Part 2: General steering
- GENERAL_INSTRUCTIONS: add 'Instance Configuration Tips' section with
  reviewer pre_task_command, collab mode, and cost_guard suggestions
Fleet start syncs src/general-knowledge/*.md to General instance's
.kiro/steering/ directory. Knowledge files are version-controlled —
updating AgEnD automatically gives General new skills.

Includes initial skills.md: tmux health check, reviewer session
management, fork instance (session cloning).
Previously, any instructions change (including knowledge file sync)
forced skipResume=true, losing conversation context on every restart.

Now: resume is always preserved. When instructions change, a [system]
notice is injected before the next user message telling the agent to
re-read its steering files.

Also: syncGeneralKnowledge only writes files when content actually
changed, preventing unnecessary instructions hash diffs.
Telegram ClassicBot allows /start in private chats and regular groups
(not the primary forum group) to spawn lightweight agent instances.

Changes:
- telegram.ts: allow non-primary-group messages through access control
  for classic mode routing (fleet-manager handles its own ACL)
- classic-channel-manager.ts: add allowed_groups/allowed_users to
  ClassicBotYaml interface + isGroupAllowed/isUserAllowed methods
- fleet-manager.ts: add Telegram classic mode routing in
  handleInboundMessage — /start, /stop, /chat commands + private chat
  auto-forward (no /chat prefix needed)
- topic-commands.ts: register /start, /stop, /chat as default-scope
  bot commands via setMyCommands API

Coexists with topic mode on the same bot token:
- Primary forum group (group_id) → topic mode (unchanged)
- Other groups/private chats → classic mode
Telegram classic mode messages (threadId=null, chatId != primaryGroupId)
were blocked by fleet-level access control before reaching the classic
mode routing block. Now these messages bypass fleet ACL and rely on
classicBot.yaml's own isGroupAllowed/isUserAllowed checks.
Core multi-adapter architecture enabling simultaneous Telegram + Discord:

types.ts:
- ChannelConfig: add optional 'id' field (adapter identifier)
- FleetConfig: add 'channels?: ChannelConfig[]' (backward compat with 'channel')

config.ts:
- loadFleetConfig: normalize 'channel' singular → 'channels' array
- Auto-assign id = type when not specified

fleet-manager.ts:
- Add adapters Map<string, ChannelAdapter> for multi-adapter lookup
- Add instanceAdapterBinding Map to track which adapter each instance uses
- Add getAdapterForInstance() / bindInstanceAdapter() helpers
- Split startSharedAdapter into loop + startSingleAdapter + startAdditionalAdapter
- Primary adapter (first channel) stays as this.adapter for backward compat
- Additional adapters get full event wiring (message, callback_query, slash_command)
- handleOutboundFromInstance: route via getAdapterForInstance
- handleToolStatusFromInstance: route via getAdapterForInstance
- Bind instances to adapters on inbound message (topic, classic, general)
- stopAll: stop all adapters

Backward compatible: single 'channel' config works exactly as before.
Multi-channel: add 'channels' array to run both platforms simultaneously.
…und react

1. bindInstanceAdapter: skip general_topic instances (always use primary)
2. stopAll: single loop over adapters map, no double-stop
3. General inbound: use msg.adapterId to resolve correct adapter for
   react() and processAttachments() instead of hardcoded this.adapter
Quickstart now supports simultaneous Telegram + Discord setup:

Fresh setup:
- Step 2 offers: 1. Telegram, 2. Discord, 3. Both
- 'Both' runs Telegram flow then Discord flow sequentially
- Single platform: writes channel (singular) for backward compat
- Both: writes channels array format

Existing fleet.yaml:
- New option '2. Add another platform'
- Auto-detects which platforms are already configured
- Runs the missing platform's flow
- Converts channel → channels array, appends new platform
- Appends token to .env

Refactored:
- Extracted runTelegramFlow() and runDiscordFlow() as reusable functions
- PlatformResult interface for structured flow output
- buildChannelConfig() helper for config object generation
… update

1. startAdditionalAdapter: add topic_closed handler, admin slash commands
   (/compact, /save, /load, /ctx, /collab), remove unused _fleet param
2. quickstart 'Add another platform': .env token replace (not skip) when
   key already exists — supports token rotation

Verified stable (no changes needed):
3. Instance names: classicInstanceName uses channelName+channelId, topic
   mode uses fleet.yaml instance keys — unaffected by multi-adapter
4. --resume: skipResume in daemon.ts only triggers on crash loop or failed
   resume — multi-adapter changes don't touch daemon lifecycle
Telegram API timeout in registerBotCommands was crashing the entire
startAll flow, preventing classic instances from starting. Now both
are non-fatal — fleet continues with available adapters.
When multiple channels are configured (e.g. Discord + Telegram),
create general-discord and general-telegram instead of a single
general. Single channel keeps the name 'general' (backward compat).
- findGeneralInstance(adapterId) matches by name suffix
- autoCreateTopics assigns topic_id by name: *-telegram→1, *-discord→general_channel_id
- handleInboundMessage passes msg.adapterId to findGeneralInstance
…rtup

fleet.yaml with channels: (plural array) left fleet.channel undefined,
causing topicMode=false and skipping adapter startup + classic instances.
Now checks both fleet.channel and fleet.channels.
…p_id

Bug: when fleet.yaml uses channels[] array format, fleetConfig.channel
was undefined, causing primaryGroupId to be empty and all Telegram
messages to be incorrectly treated as classic candidates.

Fixes:
1. config.ts: always set channel to first entry of channels[] as
   fallback — all existing this.fleetConfig?.channel references work
2. fleet-manager: add getChannelConfig(adapterId) helper to look up
   per-adapter channel config from channels[]
3. handleInboundMessage: use getChannelConfig(msg.adapterId) for
   group_id lookup instead of hardcoded fleetConfig.channel.group_id

This ensures correct routing in both single-channel and multi-channel
modes. Telegram General Topic messages route correctly, Discord
unaffected.
Detects platform from:
- Single channel config type → all instances use that platform
- Classic bot instances → DC (Discord only currently)
- Multi-channel: topic_id length heuristic (17+ digits = Discord snowflake)

Column placed between Team and Ctx, width 4 chars.
Each adapter now has its own AccessManager stored in accessManagers map.
handleInboundMessage resolves the correct AccessManager by msg.adapterId,
falling back to the primary. Telegram users are validated against
Telegram's allowed_users, Discord against Discord's.
…ommands

1. saveFleetConfig: preserve channels[] format when present, fall back
   to channel singular otherwise
2. notifyInstanceTopic: use getAdapterForInstance + per-adapter group_id
3. notifySourceTopic/notifyScheduleFailure: use per-instance adapter
4. registerBotCommands: iterate all Telegram channels in channels[],
   register bot commands for each (not just primary)
5. Mirror topic: uses primary channel fallback (acceptable limitation)
- bindInstanceAdapter: add fromInbound flag — inbound messages don't
  overwrite general_topic bindings, but startup pre-binding works
- Pre-bind general instances at startup: match instance name against
  adapter id (general-telegram → telegram, general-discord → discord)
- Plain 'general' (no suffix) stays unbound → uses primary adapter
/status, /restart, /sysinfo, /update commands in topic-commands.ts
were using this.ctx.adapter (primary) to reply, causing errors when
the message came from a non-primary adapter (e.g. Telegram chatId
sent to Discord API).

- Add getReplyAdapter(msg) helper to TopicCommands
- All command handlers use msg.adapterId to resolve correct adapter
- FleetContext interface: add optional adapters Map
- FleetManager: make adapters map readonly (accessible to sub-modules)
changhansung and others added 29 commits June 16, 2026 20:10
fix: /status adds tmux capture fallback for context percentage
fix: remove /chat from TG ClassicBot menu
fix: TG ClassicBot chat-log + error notification delivery
Reviewer feedback: rawText starting with / would trigger /chat forwarding.
Guard: any /command without @mention is logged as empty (won't forward).
fix: guard against /command forwarding without @mention
1. Log bot outbound replies to classic instance chat-log
2. TG classic: allow bot messages through if they @mention our bot
   (previously all bot messages dropped due to !threadId early return)

Both are multi-adapter refactor regressions (ecb3169e714649).
fix: TG ClassicBot bot reply logging + bot-to-bot @mention
…pter

1. TG adapter was never setting isBotMessage (from.is_bot) — now included
2. Debug logs at adapter access control + fleet-manager entry point
3. This is likely the root cause: without isBotMessage, the bot-to-bot
   fix in beta.17 never triggered
debug: TG bot message trace + fix missing isBotMessage
fix: extract text from TG Rich Message for bot-to-bot communication
debug: dump raw TG msg when bot text is empty
kiro-cli errors if LLM ends with only a tool call and no text output.
All instances now know to output '.' after completing reply.
fix: instruct instances to produce final text after reply tool
Odd number of ``` in a message causes CLI to interpret Enter as
newline (waiting for fence closure) instead of submit. Strip all
fences when count is odd.
fix: strip unclosed code fences before CLI paste
fix: correctly extract text from TG Rich Message (Bot API 10.1)
chore: upgrade grammy 1.44.0 (Bot API 10.1 rich_message receive support)
debug: bot msg log + revert rich_message to (as any)
…n for rich-only msgs)

grammy doesn't fire bot.on('message') when msg has rich_message but no text.
Added middleware to catch raw update, extract text via extractRichText, and emit as normal message event.
fix: grammy middleware for Rich Message reception
@changhansung changhansung force-pushed the fix/tg-rich-message-cleanup branch from 87d630f to c0988a9 Compare June 17, 2026 13:03
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.

1 participant