@tritard/waterbrother 0.16.127 → 0.16.129
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/package.json +1 -1
- package/src/discord.js +382 -12
package/package.json
CHANGED
package/src/discord.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState } from "./self-awareness.js";
|
|
5
|
+
import { createSession, listSessions, loadSession, saveSession } from "./session-store.js";
|
|
6
|
+
import { loadGatewayBridge, loadGatewayState, saveGatewayBridge, saveGatewayState } from "./gateway-state.js";
|
|
5
7
|
|
|
6
8
|
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
7
9
|
const STATUS_PATH = path.join(os.homedir(), ".waterbrother", "discord-status.json");
|
|
8
10
|
const DISCORD_BRIDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9
11
|
const DISCORD_BRIDGE_POLL_MS = 250;
|
|
12
|
+
const DISCORD_CONTINUATION_TTL_MS = 15 * 60 * 1000;
|
|
10
13
|
|
|
11
14
|
const INTENT_BITS = {
|
|
12
15
|
GUILDS: 1 << 0,
|
|
@@ -120,15 +123,95 @@ function buildReply(message, botUserId) {
|
|
|
120
123
|
if (!content || content === "ping") {
|
|
121
124
|
return "pong";
|
|
122
125
|
}
|
|
123
|
-
if (normalized === "status" || normalized === "online" || normalized === "are you online") {
|
|
124
|
-
return "Discord gateway is online. I can see messages and basic mentions. Full conversational Discord runtime is the next slice.";
|
|
125
|
-
}
|
|
126
126
|
if (["hello", "hi", "hey"].includes(normalized)) {
|
|
127
127
|
return "Waterbrother is here. Discord setup is live at the gateway level; deeper room workflows come next.";
|
|
128
128
|
}
|
|
129
129
|
return null;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function buildDiscordHelp() {
|
|
133
|
+
return [
|
|
134
|
+
"Waterbrother Discord control",
|
|
135
|
+
"",
|
|
136
|
+
"Basic commands",
|
|
137
|
+
"/help show this help",
|
|
138
|
+
"/about show Waterbrother identity and capabilities",
|
|
139
|
+
"/state show current Waterbrother self-awareness state",
|
|
140
|
+
"/status show Discord gateway and linked remote session status",
|
|
141
|
+
"",
|
|
142
|
+
"Remote session commands",
|
|
143
|
+
"/new start a fresh remote session",
|
|
144
|
+
"/sessions list recent linked remote sessions",
|
|
145
|
+
"/resume <session-id> switch the linked remote session",
|
|
146
|
+
"/clear clear the current remote conversation history",
|
|
147
|
+
"",
|
|
148
|
+
"Use DMs or mention @Waterbrother in a server channel to run work through the live TUI."
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function continuationKey(channelId, userId) {
|
|
153
|
+
return `${String(channelId || "").trim()}:${String(userId || "").trim()}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function upsertSessionHistory(existing, sessionId) {
|
|
157
|
+
const normalized = String(sessionId || "").trim();
|
|
158
|
+
if (!normalized) {
|
|
159
|
+
return Array.isArray(existing) ? existing.map((value) => String(value || "").trim()).filter(Boolean).slice(0, 12) : [];
|
|
160
|
+
}
|
|
161
|
+
const list = Array.isArray(existing) ? existing.map((value) => String(value || "").trim()).filter(Boolean) : [];
|
|
162
|
+
return [normalized, ...list.filter((value) => value !== normalized)].slice(0, 12);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildContinuationPrompt(text = "", continuation = null) {
|
|
166
|
+
const body = String(text || "").trim();
|
|
167
|
+
if (!continuation?.lastPrompt) return body;
|
|
168
|
+
return [
|
|
169
|
+
`Your last question/request to this user was: ${continuation.lastPrompt}`,
|
|
170
|
+
"",
|
|
171
|
+
`Their follow-up reply is: ${body}`
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatSessionList(currentSessionId, sessions = []) {
|
|
176
|
+
if (!sessions.length) {
|
|
177
|
+
return "Remote sessions\nNo linked remote sessions yet.";
|
|
178
|
+
}
|
|
179
|
+
const lines = ["Remote sessions"];
|
|
180
|
+
for (const session of sessions) {
|
|
181
|
+
const id = String(session?.id || "").trim();
|
|
182
|
+
if (!id) continue;
|
|
183
|
+
const current = id === String(currentSessionId || "").trim() ? " (current)" : "";
|
|
184
|
+
const preview = String(session?.lastUserPreview || "").trim();
|
|
185
|
+
const updatedAt = String(session?.updatedAt || "").trim();
|
|
186
|
+
lines.push(`- ${id}${current}`);
|
|
187
|
+
if (updatedAt) lines.push(` updated: ${updatedAt}`);
|
|
188
|
+
if (preview) lines.push(` last: ${preview.slice(0, 120)}`);
|
|
189
|
+
}
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseDiscordSessionCommand(text = "") {
|
|
194
|
+
const value = String(text || "").trim();
|
|
195
|
+
if (!value) return null;
|
|
196
|
+
const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
197
|
+
if (normalized === "/new" || normalized === "new session" || normalized === "start a new session" || normalized === "fresh session") {
|
|
198
|
+
return { type: "new" };
|
|
199
|
+
}
|
|
200
|
+
if (normalized === "/clear" || normalized === "clear session" || normalized === "clear conversation" || normalized === "clear history") {
|
|
201
|
+
return { type: "clear" };
|
|
202
|
+
}
|
|
203
|
+
if (normalized === "/sessions" || normalized === "list sessions" || normalized === "show sessions") {
|
|
204
|
+
return { type: "sessions" };
|
|
205
|
+
}
|
|
206
|
+
if (normalized.startsWith("/resume ")) {
|
|
207
|
+
return { type: "resume", sessionId: value.slice("/resume ".length).trim() };
|
|
208
|
+
}
|
|
209
|
+
if (normalized.startsWith("resume ")) {
|
|
210
|
+
return { type: "resume", sessionId: value.slice(value.toLowerCase().indexOf("resume ") + "resume ".length).trim() };
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
132
215
|
function createBridgeRequestId() {
|
|
133
216
|
return `dc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
134
217
|
}
|
|
@@ -141,10 +224,269 @@ function describeDiscordUser(message = {}) {
|
|
|
141
224
|
return {
|
|
142
225
|
userId: String(author.id || "").trim(),
|
|
143
226
|
username,
|
|
227
|
+
usernameHandle: username ? `@${username}` : "",
|
|
144
228
|
displayName
|
|
145
229
|
};
|
|
146
230
|
}
|
|
147
231
|
|
|
232
|
+
async function loadDiscordGatewayState() {
|
|
233
|
+
return loadGatewayState("discord");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function persistDiscordGatewayState(state) {
|
|
237
|
+
return saveGatewayState("discord", state);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getPeerState(state, userId) {
|
|
241
|
+
return state?.peers?.[String(userId || "").trim()] || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getPendingContinuation(state, message) {
|
|
245
|
+
const key = continuationKey(message?.channel_id, message?.author?.id);
|
|
246
|
+
const item = state?.continuations?.[key];
|
|
247
|
+
if (!item) return null;
|
|
248
|
+
const expiresAtMs = Date.parse(String(item.expiresAt || ""));
|
|
249
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) return null;
|
|
250
|
+
return item;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function clearContinuation(state, message) {
|
|
254
|
+
const key = continuationKey(message?.channel_id, message?.author?.id);
|
|
255
|
+
if (state?.continuations?.[key]) {
|
|
256
|
+
delete state.continuations[key];
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function rememberContinuation(state, message, text = "") {
|
|
263
|
+
const body = String(text || "").trim();
|
|
264
|
+
if (!body) return false;
|
|
265
|
+
const asksFollowUp =
|
|
266
|
+
/\?\s*$/.test(body)
|
|
267
|
+
|| /\b(would you like|do you want|which\b|what\b.*\?|how\b.*\?|who\b.*\?|where\b.*\?)\b/i.test(body);
|
|
268
|
+
if (!asksFollowUp) return false;
|
|
269
|
+
const key = continuationKey(message?.channel_id, message?.author?.id);
|
|
270
|
+
state.continuations[key] = {
|
|
271
|
+
chatId: String(message?.channel_id || "").trim(),
|
|
272
|
+
userId: String(message?.author?.id || "").trim(),
|
|
273
|
+
lastPrompt: body.slice(0, 400),
|
|
274
|
+
kind: "follow-up",
|
|
275
|
+
source: "assistant-question",
|
|
276
|
+
context: {},
|
|
277
|
+
expiresAt: new Date(Date.now() + DISCORD_CONTINUATION_TTL_MS).toISOString()
|
|
278
|
+
};
|
|
279
|
+
await persistDiscordGatewayState(state);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function ensurePeerSession(runtime, state, message) {
|
|
284
|
+
const actor = describeDiscordUser(message);
|
|
285
|
+
const existing = getPeerState(state, actor.userId);
|
|
286
|
+
if (existing?.sessionId) {
|
|
287
|
+
state.peers[actor.userId] = {
|
|
288
|
+
...existing,
|
|
289
|
+
chatId: String(message.channel_id || "").trim(),
|
|
290
|
+
username: actor.displayName,
|
|
291
|
+
usernameHandle: actor.usernameHandle,
|
|
292
|
+
displayName: actor.displayName,
|
|
293
|
+
sessions: upsertSessionHistory(existing.sessions, existing.sessionId),
|
|
294
|
+
lastSeenAt: new Date().toISOString(),
|
|
295
|
+
lastMessageId: String(message.id || "").trim()
|
|
296
|
+
};
|
|
297
|
+
await persistDiscordGatewayState(state);
|
|
298
|
+
return existing.sessionId;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const session = await createSession({
|
|
302
|
+
cwd: process.cwd(),
|
|
303
|
+
model: runtime.model,
|
|
304
|
+
agentProfile: runtime.agentProfile || "coder"
|
|
305
|
+
});
|
|
306
|
+
session.provider = runtime.provider;
|
|
307
|
+
session.baseUrl = runtime.baseUrl;
|
|
308
|
+
session.designModel = runtime.designModel;
|
|
309
|
+
session.runtimeProfile = runtime?.channels?.discord?.defaultRuntimeProfile || runtime?.gateway?.defaultRuntimeProfile || null;
|
|
310
|
+
await saveSession(session);
|
|
311
|
+
|
|
312
|
+
state.peers[actor.userId] = {
|
|
313
|
+
sessionId: session.id,
|
|
314
|
+
chatId: String(message.channel_id || "").trim(),
|
|
315
|
+
username: actor.displayName,
|
|
316
|
+
usernameHandle: actor.usernameHandle,
|
|
317
|
+
displayName: actor.displayName,
|
|
318
|
+
sessions: upsertSessionHistory([], session.id),
|
|
319
|
+
linkedAt: new Date().toISOString(),
|
|
320
|
+
lastSeenAt: new Date().toISOString(),
|
|
321
|
+
lastMessageId: String(message.id || "").trim()
|
|
322
|
+
};
|
|
323
|
+
await persistDiscordGatewayState(state);
|
|
324
|
+
return session.id;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function startFreshSession(runtime, state, message) {
|
|
328
|
+
const actor = describeDiscordUser(message);
|
|
329
|
+
const session = await createSession({
|
|
330
|
+
cwd: process.cwd(),
|
|
331
|
+
model: runtime.model,
|
|
332
|
+
agentProfile: runtime.agentProfile || "coder"
|
|
333
|
+
});
|
|
334
|
+
session.provider = runtime.provider;
|
|
335
|
+
session.baseUrl = runtime.baseUrl;
|
|
336
|
+
session.designModel = runtime.designModel;
|
|
337
|
+
session.runtimeProfile = runtime?.channels?.discord?.defaultRuntimeProfile || runtime?.gateway?.defaultRuntimeProfile || null;
|
|
338
|
+
await saveSession(session);
|
|
339
|
+
|
|
340
|
+
const previous = getPeerState(state, actor.userId) || {};
|
|
341
|
+
state.peers[actor.userId] = {
|
|
342
|
+
...previous,
|
|
343
|
+
sessionId: session.id,
|
|
344
|
+
chatId: String(message.channel_id || "").trim(),
|
|
345
|
+
username: actor.displayName,
|
|
346
|
+
usernameHandle: actor.usernameHandle,
|
|
347
|
+
displayName: actor.displayName,
|
|
348
|
+
sessions: upsertSessionHistory(previous.sessions, session.id),
|
|
349
|
+
linkedAt: new Date().toISOString(),
|
|
350
|
+
lastSeenAt: new Date().toISOString(),
|
|
351
|
+
lastMessageId: String(message.id || "").trim()
|
|
352
|
+
};
|
|
353
|
+
clearContinuation(state, message);
|
|
354
|
+
await persistDiscordGatewayState(state);
|
|
355
|
+
return session.id;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function listPeerSessions(state, userId) {
|
|
359
|
+
const peer = getPeerState(state, userId);
|
|
360
|
+
const ids = Array.isArray(peer?.sessions) ? peer.sessions.map((value) => String(value || "").trim()).filter(Boolean) : [];
|
|
361
|
+
if (!ids.length && peer?.sessionId) ids.push(String(peer.sessionId));
|
|
362
|
+
if (!ids.length) return [];
|
|
363
|
+
const all = await listSessions(Math.max(20, ids.length + 4));
|
|
364
|
+
const byId = new Map(all.map((session) => [session.id, session]));
|
|
365
|
+
return ids.map((id) => byId.get(id) || { id }).filter(Boolean);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function resumePeerSession(state, message, sessionId) {
|
|
369
|
+
const actor = describeDiscordUser(message);
|
|
370
|
+
const normalizedId = String(sessionId || "").trim();
|
|
371
|
+
if (!normalizedId) throw new Error("Usage: /resume <session-id>");
|
|
372
|
+
const session = await loadSession(normalizedId);
|
|
373
|
+
const previous = getPeerState(state, actor.userId) || {};
|
|
374
|
+
state.peers[actor.userId] = {
|
|
375
|
+
...previous,
|
|
376
|
+
sessionId: session.id,
|
|
377
|
+
chatId: String(message.channel_id || "").trim(),
|
|
378
|
+
username: actor.displayName,
|
|
379
|
+
usernameHandle: actor.usernameHandle,
|
|
380
|
+
displayName: actor.displayName,
|
|
381
|
+
sessions: upsertSessionHistory(previous.sessions, session.id),
|
|
382
|
+
linkedAt: previous.linkedAt || new Date().toISOString(),
|
|
383
|
+
lastSeenAt: new Date().toISOString(),
|
|
384
|
+
lastMessageId: String(message.id || "").trim()
|
|
385
|
+
};
|
|
386
|
+
clearContinuation(state, message);
|
|
387
|
+
await persistDiscordGatewayState(state);
|
|
388
|
+
return session.id;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function clearRemoteConversation(sessionId) {
|
|
392
|
+
const session = await loadSession(sessionId);
|
|
393
|
+
session.messages = [];
|
|
394
|
+
session.runState = {
|
|
395
|
+
state: "done",
|
|
396
|
+
detail: "",
|
|
397
|
+
updatedAt: new Date().toISOString()
|
|
398
|
+
};
|
|
399
|
+
await saveSession(session);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function handleDiscordSessionCommand(runtime, state, message, command) {
|
|
403
|
+
const actor = describeDiscordUser(message);
|
|
404
|
+
if (command.type === "new") {
|
|
405
|
+
const sessionId = await startFreshSession(runtime, state, message);
|
|
406
|
+
return `Started new remote session: ${sessionId}`;
|
|
407
|
+
}
|
|
408
|
+
if (command.type === "sessions") {
|
|
409
|
+
const sessions = await listPeerSessions(state, actor.userId);
|
|
410
|
+
const currentSessionId = getPeerState(state, actor.userId)?.sessionId || "";
|
|
411
|
+
return formatSessionList(currentSessionId, sessions);
|
|
412
|
+
}
|
|
413
|
+
if (command.type === "resume") {
|
|
414
|
+
if (!command.sessionId) {
|
|
415
|
+
return "Usage: /resume <session-id>\nUse /sessions to list recent remote session ids.";
|
|
416
|
+
}
|
|
417
|
+
const sessionId = await resumePeerSession(state, message, command.sessionId);
|
|
418
|
+
return `Linked remote session: ${sessionId}`;
|
|
419
|
+
}
|
|
420
|
+
if (command.type === "clear") {
|
|
421
|
+
const sessionId = getPeerState(state, actor.userId)?.sessionId || await ensurePeerSession(runtime, state, message);
|
|
422
|
+
await clearRemoteConversation(sessionId);
|
|
423
|
+
clearContinuation(state, message);
|
|
424
|
+
await persistDiscordGatewayState(state);
|
|
425
|
+
return `Cleared conversation history for ${sessionId}.`;
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function buildDiscordAbout(runtime, state, message) {
|
|
431
|
+
const actor = describeDiscordUser(message);
|
|
432
|
+
const sessionId = getPeerState(state, actor.userId)?.sessionId || "";
|
|
433
|
+
const currentSession = sessionId ? await loadSession(sessionId).catch(() => null) : null;
|
|
434
|
+
const manifest = await buildSelfAwarenessManifest({
|
|
435
|
+
cwd: currentSession?.cwd || process.cwd(),
|
|
436
|
+
runtime,
|
|
437
|
+
currentSession
|
|
438
|
+
});
|
|
439
|
+
return formatAboutWaterbrother(manifest);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function buildDiscordState(runtime, state, message) {
|
|
443
|
+
const actor = describeDiscordUser(message);
|
|
444
|
+
const sessionId = getPeerState(state, actor.userId)?.sessionId || "";
|
|
445
|
+
const currentSession = sessionId ? await loadSession(sessionId).catch(() => null) : null;
|
|
446
|
+
const manifest = await buildSelfAwarenessManifest({
|
|
447
|
+
cwd: currentSession?.cwd || process.cwd(),
|
|
448
|
+
runtime,
|
|
449
|
+
currentSession
|
|
450
|
+
});
|
|
451
|
+
return formatSelfState(manifest);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function buildDiscordStatusMessage(runtime, state, message) {
|
|
455
|
+
const discord = normalizeDiscordRuntime(runtime);
|
|
456
|
+
const actor = describeDiscordUser(message);
|
|
457
|
+
const peer = getPeerState(state, actor.userId);
|
|
458
|
+
const sessionId = String(peer?.sessionId || "").trim();
|
|
459
|
+
const session = sessionId ? await loadSession(sessionId).catch(() => null) : null;
|
|
460
|
+
const liveHost = await getLiveBridgeHost({ cwd: session?.cwd || "" });
|
|
461
|
+
return [
|
|
462
|
+
"Discord status",
|
|
463
|
+
`gateway: ${discord.enabled && discord.token && discord.applicationId ? "configured" : "incomplete"}`,
|
|
464
|
+
`application id: ${discord.applicationId || "missing"}`,
|
|
465
|
+
`linked session: ${sessionId || "none"}`,
|
|
466
|
+
session?.cwd ? `cwd: ${session.cwd}` : "",
|
|
467
|
+
session?.runtimeProfile ? `runtime profile: ${session.runtimeProfile}` : "",
|
|
468
|
+
liveHost ? `live terminal: ${String(liveHost.label || liveHost.ownerName || liveHost.sessionId || "connected").trim()}` : "live terminal: not connected"
|
|
469
|
+
].filter(Boolean).join("\n");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function handleDiscordControlCommand(runtime, state, message, rawText) {
|
|
473
|
+
const normalized = String(rawText || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
474
|
+
if (!normalized) return null;
|
|
475
|
+
if (normalized === "/help" || normalized === "help") {
|
|
476
|
+
return buildDiscordHelp();
|
|
477
|
+
}
|
|
478
|
+
if (normalized === "/about" || normalized === "about") {
|
|
479
|
+
return buildDiscordAbout(runtime, state, message);
|
|
480
|
+
}
|
|
481
|
+
if (normalized === "/state" || normalized === "state") {
|
|
482
|
+
return buildDiscordState(runtime, state, message);
|
|
483
|
+
}
|
|
484
|
+
if (normalized === "/status" || normalized === "status" || normalized === "online" || normalized === "are you online") {
|
|
485
|
+
return buildDiscordStatusMessage(runtime, state, message);
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
148
490
|
async function getLiveBridgeHost({ cwd = "" } = {}) {
|
|
149
491
|
const bridge = await loadGatewayBridge("discord");
|
|
150
492
|
const hosts = Array.isArray(bridge.hosts) ? bridge.hosts : [];
|
|
@@ -178,7 +520,7 @@ async function getLiveBridgeHost({ cwd = "" } = {}) {
|
|
|
178
520
|
return bridge.activeHost?.pid ? bridge.activeHost : (nextHosts[0] || null);
|
|
179
521
|
}
|
|
180
522
|
|
|
181
|
-
async function runPromptViaBridge(runtime, message, promptText) {
|
|
523
|
+
async function runPromptViaBridge(runtime, message, promptText, options = {}) {
|
|
182
524
|
const host = await getLiveBridgeHost();
|
|
183
525
|
if (!host) {
|
|
184
526
|
return { error: "No live Waterbrother TUI is connected. Start Waterbrother in the terminal first, then retry." };
|
|
@@ -195,7 +537,7 @@ async function runPromptViaBridge(runtime, message, promptText) {
|
|
|
195
537
|
username: actor.username,
|
|
196
538
|
usernameHandle: actor.username ? `@${actor.username}` : "",
|
|
197
539
|
displayName: actor.displayName,
|
|
198
|
-
sessionId: "",
|
|
540
|
+
sessionId: String(options.sessionId || "").trim(),
|
|
199
541
|
text: String(promptText || "").trim(),
|
|
200
542
|
requestKind: "prompt",
|
|
201
543
|
explicitExecution: true,
|
|
@@ -226,7 +568,11 @@ async function runPromptViaBridge(runtime, message, promptText) {
|
|
|
226
568
|
if (reply.error) {
|
|
227
569
|
return { error: String(reply.error || "").trim() || "Discord bridge execution failed." };
|
|
228
570
|
}
|
|
229
|
-
return {
|
|
571
|
+
return {
|
|
572
|
+
content: String(reply.content || "").trim() || "(no content)",
|
|
573
|
+
sessionId: String(reply.sessionId || "").trim(),
|
|
574
|
+
meta: reply.meta && typeof reply.meta === "object" ? { ...reply.meta } : {}
|
|
575
|
+
};
|
|
230
576
|
}
|
|
231
577
|
}
|
|
232
578
|
|
|
@@ -372,19 +718,43 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
|
|
|
372
718
|
if (!msg || msg.author?.bot) return;
|
|
373
719
|
const scope = msg.guild_id ? `guild:${msg.guild_id}` : "dm";
|
|
374
720
|
log(`discord: ${scope} #${msg.channel_id} ${msg.author?.username || "unknown"} -> ${String(msg.content || "").trim()}`);
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
721
|
+
const shouldReply = shouldReplyToMessage(msg, botUser?.id);
|
|
722
|
+
const rawText = extractMentionContent(String(msg.content || "").trim(), botUser?.id);
|
|
723
|
+
const reply = shouldReply ? buildReply(msg, botUser?.id) : null;
|
|
378
724
|
try {
|
|
379
725
|
if (reply) {
|
|
380
726
|
await sendChannelMessage(discord, msg.channel_id, reply);
|
|
381
727
|
log(`discord: replied in ${msg.channel_id}`);
|
|
382
728
|
return;
|
|
383
729
|
}
|
|
384
|
-
if (
|
|
385
|
-
const
|
|
730
|
+
if (shouldReply) {
|
|
731
|
+
const state = await loadDiscordGatewayState();
|
|
732
|
+
const controlReply = await handleDiscordControlCommand(runtime, state, msg, rawText);
|
|
733
|
+
if (controlReply) {
|
|
734
|
+
await sendChannelMessage(discord, msg.channel_id, controlReply);
|
|
735
|
+
log(`discord: control reply in ${msg.channel_id}`);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const command = parseDiscordSessionCommand(rawText);
|
|
739
|
+
if (command) {
|
|
740
|
+
const commandReply = await handleDiscordSessionCommand(runtime, state, msg, command);
|
|
741
|
+
if (commandReply) {
|
|
742
|
+
await sendChannelMessage(discord, msg.channel_id, commandReply);
|
|
743
|
+
log(`discord: session reply in ${msg.channel_id}`);
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const continuation = getPendingContinuation(state, msg);
|
|
748
|
+
const stateChanged = clearContinuation(state, msg);
|
|
749
|
+
if (stateChanged) {
|
|
750
|
+
await persistDiscordGatewayState(state);
|
|
751
|
+
}
|
|
752
|
+
const sessionId = await ensurePeerSession(runtime, state, msg);
|
|
753
|
+
const promptText = continuation ? buildContinuationPrompt(rawText, continuation) : rawText;
|
|
754
|
+
const bridged = await runPromptViaBridge(runtime, msg, promptText, { sessionId });
|
|
386
755
|
if (bridged?.content) {
|
|
387
756
|
await sendChannelMessage(discord, msg.channel_id, bridged.content);
|
|
757
|
+
await rememberContinuation(state, msg, bridged.content);
|
|
388
758
|
log(`discord: bridged reply in ${msg.channel_id}`);
|
|
389
759
|
} else if (bridged?.error) {
|
|
390
760
|
await sendChannelMessage(discord, msg.channel_id, bridged.error);
|