refactor(channels,router): move all policy to router; bridge is transport
Follow-up to b159722. That shrank the bridge's shouldEngage to a flood
gate + coarse sticky-subscribe signal. This completes the move —
policy lives exclusively in the router, the bridge is transport-only,
and the conversations map + ChannelSetup.conversations +
ChannelAdapter.updateConversations are all gone.
Key shifts:
1. Subscribe moves from bridge to router.
Bridge used to call `thread.subscribe()` from its onNewMention /
onDirectMessage handlers based on a coarse "any mention-sticky wiring
exists on this channel" check. That forced the decision before the
router could apply per-wiring engage logic, and it relied on the
conversations map being current (staleness risk).
ChannelAdapter gains `subscribe?(platformId, threadId)`. The Chat
SDK bridge implements it via SqliteStateAdapter.subscribe(threadId)
(idempotent — a repeat call on an already-subscribed thread is a
no-op). The router's fan-out loop calls it once per message when
the first mention-sticky wiring actually engages. Precise, not
coarse.
2. Short-circuit the drop path with one combined query.
New `getMessagingGroupWithAgentCount(channelType, platformId)` does
the messaging_groups lookup AND counts wirings in a single SELECT,
using the existing UNIQUE(channel_type, platform_id) index on
messaging_groups and UNIQUE(messaging_group_id, agent_group_id) on
messaging_group_agents for the JOIN. No new indexes needed.
routeInbound now short-circuits:
- No messaging_groups row AND not addressed (no mention/DM)
→ return silently. One DB read, nothing written. This is the
Discord-bot-in-a-big-guild case; we no longer auto-create rows
for every plain message in every channel the bot can see.
- Messaging group exists but no wirings AND not addressed
→ return silently. One DB read.
- Otherwise fall through to sender resolution + fan-out as before.
Behavioral change: plain chatter on unwired channels no longer gets
dropped_messages audit rows, which used to bloat the table. Audit
still fires on addressed-to-bot drops where the admin cares
("someone @-mentioned us but nobody's wired").
3. Bridge is now purely transport.
Deleted entirely: ConversationConfig, ChannelSetup.conversations,
ChannelAdapter.updateConversations?, bridge's `conversations` map,
buildConversationMap, shouldEngage, EngageSource, engageDecision,
bridge.updateConversations method, src/index.ts
buildConversationConfigs. Four handlers reduce to "resolve channel
id, build InboundMessage with isMention, call onInbound". Net
~130 LOC deleted from the bridge.
Collateral: the conversations-map staleness problem is gone. The
upcoming channel-registration feature doesn't need any map-refresh
plumbing — when an approval creates a new wiring, the next message
hits the DB fresh and just works.
Bridge tests prune to the narrow platform-adjacent surface (openDM
delegation, subscribe presence). Host-core test that asserted the
old "auto-create on every unknown message" behavior updates to
reflect the new escalation-gated semantics: plain messages on
unknown channels don't auto-create, mentions do.
159 tests pass (was 172 — net -13, almost entirely from
bridge-engage-mode tests that covered logic now owned by the router
and exercised through host-core.test.ts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,26 +244,42 @@ describe('router', () => {
|
||||
expect(wakeContainer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should auto-create messaging group for unknown platform', async () => {
|
||||
it('auto-creates messaging group only when the bot is addressed (mention/DM)', async () => {
|
||||
// The router's no-mg branch is escalation-gated: plain chatter on an
|
||||
// unknown channel stays silent (no DB writes) so a bot that sits in
|
||||
// many unwired channels doesn't bloat messaging_groups. Only explicit
|
||||
// mentions and DMs trigger auto-create.
|
||||
const { routeInbound } = await import('./router.js');
|
||||
const { getMessagingGroupByPlatform } = await import('./db/messaging-groups.js');
|
||||
|
||||
const event: InboundEvent = {
|
||||
// Plain message on unknown channel — should NOT auto-create.
|
||||
await routeInbound({
|
||||
channelType: 'slack',
|
||||
platformId: 'C-NEW-CHANNEL',
|
||||
platformId: 'C-PLAIN',
|
||||
threadId: null,
|
||||
message: {
|
||||
id: 'msg-2',
|
||||
id: 'msg-plain',
|
||||
kind: 'chat',
|
||||
content: JSON.stringify({ sender: 'User', text: 'Hi' }),
|
||||
timestamp: now(),
|
||||
},
|
||||
};
|
||||
});
|
||||
expect(getMessagingGroupByPlatform('slack', 'C-PLAIN')).toBeUndefined();
|
||||
|
||||
await routeInbound(event);
|
||||
|
||||
const { getMessagingGroupByPlatform } = await import('./db/messaging-groups.js');
|
||||
const mg = getMessagingGroupByPlatform('slack', 'C-NEW-CHANNEL');
|
||||
expect(mg).toBeDefined();
|
||||
// Mention on unknown channel — SHOULD auto-create (next step: channel-registration flow).
|
||||
await routeInbound({
|
||||
channelType: 'slack',
|
||||
platformId: 'C-MENTIONED',
|
||||
threadId: null,
|
||||
message: {
|
||||
id: 'msg-mentioned',
|
||||
kind: 'chat',
|
||||
content: JSON.stringify({ sender: 'User', text: '@bot hi' }),
|
||||
timestamp: now(),
|
||||
isMention: true,
|
||||
},
|
||||
});
|
||||
expect(getMessagingGroupByPlatform('slack', 'C-MENTIONED')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should route multiple messages to the same session', async () => {
|
||||
|
||||
Reference in New Issue
Block a user