@gonzih/cc-discord 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bot.d.ts CHANGED
@@ -37,6 +37,13 @@ export declare class CcDiscordBot {
37
37
  private channelNamespaceMap;
38
38
  private storeSnowflake;
39
39
  reverseSnowflakeLookup(n: number): string | undefined;
40
+ /** Persist a channelId → {namespace, repoUrl} mapping to Redis. */
41
+ private persistChannelMapping;
42
+ /**
43
+ * Load persisted channel→namespace mappings from Redis and repopulate
44
+ * channelNamespaceMap + routedChannelIds. Call once on startup after the notifier is ready.
45
+ */
46
+ loadChannelMappings(): Promise<void>;
40
47
  /** Session key: "channelId" or "channelId:threadId" for threads */
41
48
  private sessionKey;
42
49
  /** Get the channel/thread for sending messages */
@@ -71,7 +78,8 @@ export declare class CcDiscordBot {
71
78
  * and start the meta-agent for `repoUrl`. Fire-and-forget after sending the confirmation message.
72
79
  */
73
80
  private createChannelForRepo;
74
- /** Write a message to the Redis chat log. Fire-and-forget. */
81
+ /** Write a message to the Redis chat log. Fire-and-forget.
82
+ * Pass `ns` to write under a specific namespace; defaults to the bot's primary namespace. */
75
83
  private writeChatMessage;
76
84
  /** Returns the last channelId that sent a message. */
77
85
  getLastActiveChannelId(): string | undefined;
package/dist/bot.js CHANGED
@@ -14,6 +14,10 @@ import { getCurrentToken } from "./tokens.js";
14
14
  import { writeChatLog } from "./notifier.js";
15
15
  import { CronManager } from "./cron.js";
16
16
  import { parseChannelCreateIntent, ensureMetaAgent, routeToMetaAgent } from "./router.js";
17
+ /** Redis key for persisting a Discord channelId → namespace mapping across restarts. */
18
+ function discordChannelKey(channelId) {
19
+ return `cca:discord:channel:${channelId}`;
20
+ }
17
21
  /** Convert a Discord snowflake string to a safe 53-bit integer for CronManager compatibility. */
18
22
  function snowflakeToInt(id) {
19
23
  // Discord snowflakes are up to 2^63, beyond Number.MAX_SAFE_INTEGER.
@@ -165,6 +169,49 @@ export class CcDiscordBot {
165
169
  reverseSnowflakeLookup(n) {
166
170
  return this.snowflakeMap.get(n);
167
171
  }
172
+ /** Persist a channelId → {namespace, repoUrl} mapping to Redis. */
173
+ persistChannelMapping(channelId, namespace, repoUrl) {
174
+ if (!this.redis)
175
+ return;
176
+ const key = discordChannelKey(channelId);
177
+ const value = JSON.stringify({ namespace, repoUrl });
178
+ this.redis.set(key, value).catch((err) => {
179
+ console.warn(`[bot] persistChannelMapping failed for ${channelId}:`, err.message);
180
+ });
181
+ }
182
+ /**
183
+ * Load persisted channel→namespace mappings from Redis and repopulate
184
+ * channelNamespaceMap + routedChannelIds. Call once on startup after the notifier is ready.
185
+ */
186
+ async loadChannelMappings() {
187
+ if (!this.redis)
188
+ return;
189
+ let keys;
190
+ try {
191
+ keys = await this.redis.keys("cca:discord:channel:*");
192
+ }
193
+ catch (err) {
194
+ console.warn("[bot] loadChannelMappings keys scan failed:", err.message);
195
+ return;
196
+ }
197
+ for (const key of keys) {
198
+ try {
199
+ const raw = await this.redis.get(key);
200
+ if (!raw)
201
+ continue;
202
+ const { namespace, repoUrl } = JSON.parse(raw);
203
+ const channelId = key.slice("cca:discord:channel:".length);
204
+ if (!this.channelNamespaceMap.has(channelId)) {
205
+ this.channelNamespaceMap.set(channelId, { namespace, repoUrl });
206
+ this.opts.registerRoutedChannelId?.(namespace, channelId);
207
+ console.log(`[bot] restored channel mapping: ${channelId} → ${namespace}`);
208
+ }
209
+ }
210
+ catch (err) {
211
+ console.warn(`[bot] loadChannelMappings: failed to parse ${key}:`, err.message);
212
+ }
213
+ }
214
+ }
168
215
  /** Session key: "channelId" or "channelId:threadId" for threads */
169
216
  sessionKey(channelId, threadId) {
170
217
  return threadId ? `${channelId}:${threadId}` : channelId;
@@ -284,7 +331,7 @@ export class CcDiscordBot {
284
331
  // Channel registered via createChannelForRepo or /channel — route directly to its meta-agent
285
332
  const mappedNs = this.channelNamespaceMap.get(effectiveChannelId);
286
333
  if (mappedNs && this.redis) {
287
- this.writeChatMessage("user", "discord", text, effectiveChannelId);
334
+ this.writeChatMessage("user", "discord", text, effectiveChannelId, mappedNs.namespace);
288
335
  this.opts.registerRoutedChannelId?.(mappedNs.namespace, effectiveChannelId);
289
336
  const username = msg.member?.displayName ?? msg.author.username;
290
337
  try {
@@ -642,6 +689,7 @@ export class CcDiscordBot {
642
689
  const newChannel = await guild.channels.create({ name: namespace, type: ChannelType.GuildText, parent: resolveCategoryId(guild) });
643
690
  this.channelNamespaceMap.set(newChannel.id, { namespace, repoUrl });
644
691
  this.opts.registerRoutedChannelId?.(namespace, newChannel.id);
692
+ this.persistChannelMapping(newChannel.id, namespace, repoUrl);
645
693
  await interaction.editReply(`Created <#${newChannel.id}> — messages there route to the ${repoUrl} meta-agent`);
646
694
  // Start meta-agent in the background
647
695
  if (this.redis) {
@@ -785,6 +833,7 @@ export class CcDiscordBot {
785
833
  }
786
834
  this.channelNamespaceMap.set(newChannel.id, { namespace, repoUrl });
787
835
  this.opts.registerRoutedChannelId?.(namespace, newChannel.id);
836
+ this.persistChannelMapping(newChannel.id, namespace, repoUrl);
788
837
  await channel.send(`Created <#${newChannel.id}> — messages there route to the ${repoUrl} meta-agent`).catch(() => { });
789
838
  // Start meta-agent in the background after acknowledging the user
790
839
  if (this.redis) {
@@ -795,8 +844,9 @@ export class CcDiscordBot {
795
844
  });
796
845
  }
797
846
  }
798
- /** Write a message to the Redis chat log. Fire-and-forget. */
799
- writeChatMessage(role, source, content, channelId) {
847
+ /** Write a message to the Redis chat log. Fire-and-forget.
848
+ * Pass `ns` to write under a specific namespace; defaults to the bot's primary namespace. */
849
+ writeChatMessage(role, source, content, channelId, ns) {
800
850
  if (!this.redis)
801
851
  return;
802
852
  const msg = {
@@ -807,7 +857,7 @@ export class CcDiscordBot {
807
857
  timestamp: new Date().toISOString(),
808
858
  chatId: snowflakeToInt(channelId),
809
859
  };
810
- writeChatLog(this.redis, this.namespace, msg);
860
+ writeChatLog(this.redis, ns ?? this.namespace, msg);
811
861
  }
812
862
  /** Returns the last channelId that sent a message. */
813
863
  getLastActiveChannelId() {
package/dist/index.js CHANGED
@@ -95,6 +95,10 @@ const bot = new CcDiscordBot({
95
95
  });
96
96
  const notifier = startNotifier(bot, notifyChannelId, namespace, sharedRedis, (channelId, text) => handleUserMessageFn?.(channelId, text), (channelId, text) => forwardNotificationFn?.(channelId, text), () => getLastActiveChannelIdFn(), (n) => bot.reverseSnowflakeLookup(n));
97
97
  console.log(`[notifier] started for namespace=${namespace} notifyChannelId=${notifyChannelId ?? "dynamic"}`);
98
+ // Restore persisted channel→namespace mappings so routing survives restarts
99
+ bot.loadChannelMappings().catch((err) => {
100
+ console.warn("[cc-discord] loadChannelMappings failed:", err.message);
101
+ });
98
102
  // Wire closures now that bot is constructed
99
103
  getLastActiveChannelIdFn = () => bot.getLastActiveChannelId();
100
104
  handleUserMessageFn = (channelId, text) => { void bot.handleUserMessage(channelId, text); };
@@ -26,21 +26,30 @@ export interface ParsedNotification {
26
26
  }
27
27
  /**
28
28
  * Parse a notification payload.
29
- * Returns the display text plus an optional chatId for per-channel routing.
29
+ * Returns the display text plus an optional chatId for per-channel routing,
30
+ * or null when the routing array excludes "discord".
30
31
  * Appends a [driver] or [driver:model] badge when present.
31
32
  * Appends " cost: $X.XXX" if a numeric cost field is present.
32
33
  */
33
- export declare function parseNotification(raw: string): ParsedNotification;
34
+ export declare function parseNotification(raw: string): ParsedNotification | null;
34
35
  /**
35
36
  * Write a message to the chat log in Redis.
36
37
  * Fire-and-forget — errors are logged but not thrown.
37
38
  */
38
39
  export declare function writeChatLog(redis: Redis, namespace: string, msg: ChatMessage): void;
40
+ /**
41
+ * Resolve the target Discord channelId for a notification.
42
+ * When chatId is set and a reverse-lookup function is available, prefer the originating channel.
43
+ * Falls back to notifyChannelId, then getActiveChannelId.
44
+ */
45
+ export declare function resolveNotifyChannel(chatId: number | undefined, notifyChannelId: string | null, getActiveChannelId?: () => string | undefined, reverseSnowflakeLookup?: (n: number) => string | undefined): string | undefined;
39
46
  export interface NotifierHandle {
40
47
  /**
41
48
  * Register the originating Discord channel ID for a routed namespace.
42
49
  * When the meta-agent for `namespace` publishes a response, it will be
43
50
  * forwarded to `channelId`.
51
+ * Also subscribes to notifyChannel(namespace) and chatIncomingChannel(namespace)
52
+ * so notifications and UI messages for that namespace are received.
44
53
  */
45
54
  registerRoutedChannelId: (namespace: string, channelId: string) => void;
46
55
  }
package/dist/notifier.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * cca:chat:log:{namespace} — LPUSH + LTRIM 0 499 (last 500 messages)
11
11
  * cca:chat:outgoing:{namespace} — PUBLISH for web UI to consume
12
12
  */
13
- import { chatLogKey, chatOutgoingChannel, chatIncomingChannel, notifyChannel, metaAgentStatusKey, metaInputKey, } from "@gonzih/cc-wire";
13
+ import { chatLogKey, chatOutgoingChannel, chatIncomingChannel, notifyChannel, notifyListKey, metaAgentStatusKey, metaInputKey, } from "@gonzih/cc-wire";
14
14
  import { splitLongMessage, stripAnsi } from "./formatter.js";
15
15
  function log(level, ...args) {
16
16
  const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
@@ -32,7 +32,8 @@ function shortenModelName(model, driver) {
32
32
  }
33
33
  /**
34
34
  * Parse a notification payload.
35
- * Returns the display text plus an optional chatId for per-channel routing.
35
+ * Returns the display text plus an optional chatId for per-channel routing,
36
+ * or null when the routing array excludes "discord".
36
37
  * Appends a [driver] or [driver:model] badge when present.
37
38
  * Appends " cost: $X.XXX" if a numeric cost field is present.
38
39
  */
@@ -44,6 +45,10 @@ export function parseNotification(raw) {
44
45
  let chatId;
45
46
  try {
46
47
  const parsed = JSON.parse(raw);
48
+ // routing: absent/empty → all transports; non-empty → only listed transports
49
+ if (parsed.routing && parsed.routing.length > 0 && !parsed.routing.includes("discord")) {
50
+ return null;
51
+ }
47
52
  if (parsed.text)
48
53
  text = parsed.text;
49
54
  driver = parsed.driver;
@@ -86,7 +91,7 @@ export function writeChatLog(redis, namespace, msg) {
86
91
  * When chatId is set and a reverse-lookup function is available, prefer the originating channel.
87
92
  * Falls back to notifyChannelId, then getActiveChannelId.
88
93
  */
89
- function resolveNotifyChannel(chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup) {
94
+ export function resolveNotifyChannel(chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup) {
90
95
  if (chatId != null && reverseSnowflakeLookup) {
91
96
  const resolved = reverseSnowflakeLookup(chatId);
92
97
  if (resolved)
@@ -107,8 +112,10 @@ function resolveNotifyChannel(chatId, notifyChannelId, getActiveChannelId, rever
107
112
  * @param reverseSnowflakeLookup - Optional callback to resolve a chatId integer to a Discord channelId
108
113
  */
109
114
  export function startNotifier(bot, notifyChannelId, namespace, redis, handleUserMessage, forwardNotification, getActiveChannelId, reverseSnowflakeLookup) {
110
- // Per-namespace channelId registry
115
+ // Per-namespace channelId registry — maps routed namespace → Discord channelId
111
116
  const routedChannelIds = new Map();
117
+ // Track which namespaces we've already subscribed to (to avoid duplicate subscribe calls)
118
+ const subscribedNamespaces = new Set();
112
119
  const sub = redis.duplicate({
113
120
  retryStrategy: (times) => {
114
121
  const delay = Math.min(1000 * Math.pow(2, times - 1), 30_000);
@@ -122,25 +129,39 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
122
129
  sub.on("close", () => {
123
130
  log("info", "subscriber disconnected, will reconnect with backoff");
124
131
  });
125
- // notifyChannel(namespace) forward job completion notifications to Discord
126
- sub.subscribe(notifyChannel(namespace), (err) => {
127
- if (err) {
128
- log("error", `subscribe ${notifyChannel(namespace)} failed:`, err.message);
129
- }
130
- else {
131
- log("info", `subscribed to ${notifyChannel(namespace)}`);
132
- }
133
- });
134
- // chatIncomingChannel(namespace) — messages from UI
135
- sub.subscribe(chatIncomingChannel(namespace), (err) => {
136
- if (err) {
137
- log("error", `subscribe ${chatIncomingChannel(namespace)} failed:`, err.message);
138
- }
139
- else {
140
- log("info", `subscribed to ${chatIncomingChannel(namespace)}`);
141
- }
142
- });
143
- // chatOutgoingChannel("*") meta-agent stdout lines
132
+ // Reverse map: Redis channel string → namespace (for O(1) lookup in message handler)
133
+ const channelToNamespace = new Map();
134
+ function subscribeNamespace(ns) {
135
+ if (subscribedNamespaces.has(ns))
136
+ return;
137
+ subscribedNamespaces.add(ns);
138
+ const notifyCh = notifyChannel(ns);
139
+ const incomingCh = chatIncomingChannel(ns);
140
+ channelToNamespace.set(notifyCh, ns);
141
+ channelToNamespace.set(incomingCh, ns);
142
+ sub.subscribe(notifyCh, (err) => {
143
+ if (err) {
144
+ log("error", `subscribe ${notifyCh} failed:`, err.message);
145
+ }
146
+ else {
147
+ log("info", `subscribed to ${notifyCh}`);
148
+ }
149
+ });
150
+ sub.subscribe(incomingCh, (err) => {
151
+ if (err) {
152
+ log("error", `subscribe ${incomingCh} failed:`, err.message);
153
+ }
154
+ else {
155
+ log("info", `subscribed to ${incomingCh}`);
156
+ }
157
+ });
158
+ }
159
+ function resolveSubscribedNamespace(channel) {
160
+ return channelToNamespace.get(channel);
161
+ }
162
+ // Subscribe to the primary namespace immediately
163
+ subscribeNamespace(namespace);
164
+ // chatOutgoingChannel("*") — meta-agent stdout lines for ALL namespaces
144
165
  sub.psubscribe(chatOutgoingChannel("*"), (err) => {
145
166
  if (err) {
146
167
  log("error", `psubscribe ${chatOutgoingChannel("*")} failed:`, err.message);
@@ -181,7 +202,12 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
181
202
  const content = parsed.content;
182
203
  if (!content)
183
204
  return;
184
- const targetChannelId = routedChannelIds.get(ns) ?? notifyChannelId ?? getActiveChannelId?.();
205
+ // For the primary namespace, fall back to notifyChannelId / getActiveChannelId.
206
+ // For any other namespace, ONLY use the registered channelId — never fall back to
207
+ // the primary channel, as that would cause cross-namespace leakage.
208
+ const targetChannelId = ns === namespace
209
+ ? (routedChannelIds.get(ns) ?? notifyChannelId ?? getActiveChannelId?.())
210
+ : routedChannelIds.get(ns);
185
211
  if (targetChannelId == null) {
186
212
  log("warn", `meta-agent output: no channelId for namespace=${ns}, dropping line`);
187
213
  return;
@@ -196,8 +222,8 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
196
222
  clearTimeout(buf.timer);
197
223
  buf.timer = setTimeout(() => flushMetaAgentBuffer(ns, targetChannelId), META_AGENT_FLUSH_DELAY_MS);
198
224
  });
199
- // Poll the notifyChannel(namespace) LIST every 5 seconds
200
- const notifyListKey = notifyChannel(namespace);
225
+ // Poll the notifyListKey(namespace) LIST every 5 seconds
226
+ const notifyListRedisKey = notifyListKey(namespace);
201
227
  const MAX_PER_CYCLE = 20;
202
228
  const pollNotifyList = async () => {
203
229
  const targetId = notifyChannelId ?? getActiveChannelId?.();
@@ -206,7 +232,7 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
206
232
  const items = [];
207
233
  try {
208
234
  for (let i = 0; i < MAX_PER_CYCLE; i++) {
209
- const item = await redis.rpop(notifyListKey);
235
+ const item = await redis.rpop(notifyListRedisKey);
210
236
  if (item === null)
211
237
  break;
212
238
  items.push(item);
@@ -221,7 +247,7 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
221
247
  let remaining = 0;
222
248
  if (items.length === MAX_PER_CYCLE) {
223
249
  try {
224
- remaining = await redis.llen(notifyListKey);
250
+ remaining = await redis.llen(notifyListRedisKey);
225
251
  }
226
252
  catch (err) {
227
253
  log("warn", "notify list llen failed:", err.message);
@@ -229,6 +255,8 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
229
255
  }
230
256
  for (const raw of items) {
231
257
  const notification = parseNotification(raw);
258
+ if (notification === null)
259
+ continue; // routing excludes discord
232
260
  const destChannelId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup) ?? targetId;
233
261
  bot.sendToChannelById(destChannelId, notification.text).catch((err) => {
234
262
  log("warn", "notify list send failed:", err.message);
@@ -247,21 +275,37 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
247
275
  void pollNotifyList();
248
276
  }, 5_000);
249
277
  sub.on("message", (channel, message) => {
250
- const notifyCh = notifyChannel(namespace);
251
- const incomingCh = chatIncomingChannel(namespace);
278
+ // Determine which namespace this channel belongs to
279
+ const ns = resolveSubscribedNamespace(channel);
280
+ if (!ns)
281
+ return;
282
+ const isPrimary = ns === namespace;
283
+ const notifyCh = notifyChannel(ns);
284
+ const incomingCh = chatIncomingChannel(ns);
252
285
  if (channel === notifyCh) {
253
286
  const notification = parseNotification(message);
254
- const targetId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup);
287
+ if (notification === null)
288
+ return; // routing excludes discord
289
+ let targetId;
290
+ if (isPrimary) {
291
+ targetId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup);
292
+ }
293
+ else {
294
+ // For routed namespaces, only use the registered channelId — no fallback to primary
295
+ targetId = notification.chatId != null && reverseSnowflakeLookup
296
+ ? (reverseSnowflakeLookup(notification.chatId) ?? routedChannelIds.get(ns))
297
+ : routedChannelIds.get(ns);
298
+ }
255
299
  if (targetId != null) {
256
300
  bot.sendToChannelById(targetId, notification.text).catch((err) => {
257
- log("warn", "notify send failed:", err.message);
301
+ log("warn", `notify send failed (ns=${ns}):`, err.message);
258
302
  });
259
303
  if (forwardNotification) {
260
304
  forwardNotification(targetId, notification.text);
261
305
  }
262
306
  }
263
307
  else {
264
- log("warn", "notify: no channelId available, dropping notification");
308
+ log("warn", `notify: no channelId available for ns=${ns}, dropping notification`);
265
309
  }
266
310
  return;
267
311
  }
@@ -278,11 +322,13 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
278
322
  catch {
279
323
  // raw string message — use as-is
280
324
  }
281
- const targetChannelId = notifyChannelId ?? getActiveChannelId?.();
325
+ const targetChannelId = isPrimary
326
+ ? (notifyChannelId ?? getActiveChannelId?.())
327
+ : routedChannelIds.get(ns);
282
328
  if (targetChannelId !== undefined) {
283
329
  // Echo to Discord so the user sees UI messages
284
330
  bot.sendToChannelById(targetChannelId, `[from UI]: ${content}`).catch((err) => {
285
- log("warn", "sendToChannelById (UI echo) failed:", err.message);
331
+ log("warn", `sendToChannelById (UI echo) failed (ns=${ns}):`, err.message);
286
332
  });
287
333
  // Log the incoming message
288
334
  const inMsg = {
@@ -293,12 +339,12 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
293
339
  timestamp: originalTimestamp ?? new Date().toISOString(),
294
340
  chatId: 0, // no numeric chatId for Discord — stored by channelId string
295
341
  };
296
- writeChatLog(redis, namespace, inMsg);
342
+ writeChatLog(redis, ns, inMsg);
297
343
  // Check if a meta-agent is running; if so, route there instead
298
344
  void (async () => {
299
345
  let routedToMetaAgent = false;
300
346
  try {
301
- const statusRaw = await redis.get(metaAgentStatusKey(namespace));
347
+ const statusRaw = await redis.get(metaAgentStatusKey(ns));
302
348
  if (statusRaw) {
303
349
  const status = JSON.parse(statusRaw);
304
350
  if (status.status === "running") {
@@ -307,14 +353,14 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
307
353
  content,
308
354
  timestamp: new Date().toISOString(),
309
355
  });
310
- await redis.rpush(metaInputKey(namespace), entry);
311
- log("info", `cca:chat:incoming: routed to meta-agent for namespace ${namespace}`);
356
+ await redis.rpush(metaInputKey(ns), entry);
357
+ log("info", `cca:chat:incoming: routed to meta-agent for namespace ${ns}`);
312
358
  routedToMetaAgent = true;
313
359
  }
314
360
  }
315
361
  }
316
362
  catch (err) {
317
- log("warn", "meta-agent status check failed:", err.message);
363
+ log("warn", `meta-agent status check failed (ns=${ns}):`, err.message);
318
364
  }
319
365
  if (!routedToMetaAgent && handleUserMessage) {
320
366
  handleUserMessage(targetChannelId, content);
@@ -322,13 +368,16 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
322
368
  })();
323
369
  }
324
370
  else {
325
- log("warn", "cca:chat:incoming: no active channelId to route message to");
371
+ log("warn", `cca:chat:incoming: no active channelId for ns=${ns}, dropping message`);
326
372
  }
327
373
  }
328
374
  });
329
375
  return {
330
376
  registerRoutedChannelId: (ns, channelId) => {
331
377
  routedChannelIds.set(ns, channelId);
378
+ // Subscribe to this namespace's Redis channels so we receive its notifications
379
+ // and incoming UI messages. No-op if already subscribed.
380
+ subscribeNamespace(ns);
332
381
  },
333
382
  };
334
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Claude Code Discord bot — chat with Claude Code via Discord",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "dist/"
19
19
  ],
20
20
  "dependencies": {
21
- "@gonzih/cc-wire": "^0.1.4",
21
+ "@gonzih/cc-wire": "^0.1.6",
22
22
  "discord.js": "^14.0.0",
23
23
  "ioredis": "^5.0.0"
24
24
  },