@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 +9 -1
- package/dist/bot.js +54 -4
- package/dist/index.js +4 -0
- package/dist/notifier.d.ts +11 -2
- package/dist/notifier.js +90 -41
- package/package.json +2 -2
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
|
-
|
|
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); };
|
package/dist/notifier.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
|
|
251
|
-
const
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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 =
|
|
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",
|
|
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,
|
|
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(
|
|
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(
|
|
311
|
-
log("info", `cca:chat:incoming: routed to meta-agent for 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",
|
|
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",
|
|
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.
|
|
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.
|
|
21
|
+
"@gonzih/cc-wire": "^0.1.6",
|
|
22
22
|
"discord.js": "^14.0.0",
|
|
23
23
|
"ioredis": "^5.0.0"
|
|
24
24
|
},
|