@p-sw/brainbox 0.1.0 → 0.1.2-alpha.0
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/index.js +5319 -0
- package/package.json +10 -4
- package/scripts/smoke_providers.ts +0 -176
- package/src/brain/index.ts +0 -936
- package/src/brain/manager.ts +0 -144
- package/src/brain/memory.ts +0 -99
- package/src/brain/messageHistory.ts +0 -26
- package/src/brain/schedule.ts +0 -11
- package/src/brain/types.ts +0 -26
- package/src/channel/base.ts +0 -527
- package/src/channel/discord.ts +0 -227
- package/src/channel/telegram.ts +0 -150
- package/src/commands/auth.tsx +0 -306
- package/src/commands/brain.ts +0 -119
- package/src/commands/daemon/commands.ts +0 -56
- package/src/commands/daemon/pairingCommand.ts +0 -15
- package/src/commands/daemon/restartCommand.ts +0 -17
- package/src/commands/daemon.ts +0 -168
- package/src/commands/index.ts +0 -16
- package/src/commands/model.tsx +0 -138
- package/src/commands/onboard.tsx +0 -473
- package/src/commands/pairing.ts +0 -32
- package/src/commands/restart.ts +0 -24
- package/src/config/file/auth.ts +0 -105
- package/src/config/file/root.ts +0 -37
- package/src/config/index.ts +0 -21
- package/src/config/loader.ts +0 -115
- package/src/index.ts +0 -61
- package/src/provider/index.ts +0 -122
- package/src/provider/llm.ts +0 -191
- package/src/provider/promptLoader.ts +0 -36
- package/src/provider/providers/302ai.ts +0 -17
- package/src/provider/providers/MiniMax.ts +0 -17
- package/src/provider/providers/anthropic.ts +0 -257
- package/src/provider/providers/azure_cognitive.ts +0 -40
- package/src/provider/providers/azure_openai.ts +0 -49
- package/src/provider/providers/baseten.ts +0 -17
- package/src/provider/providers/bedrock.ts +0 -312
- package/src/provider/providers/cerebras.ts +0 -17
- package/src/provider/providers/cloudflare_gateway.ts +0 -34
- package/src/provider/providers/cloudflare_workers.ts +0 -178
- package/src/provider/providers/copilot.ts +0 -22
- package/src/provider/providers/cortecs.ts +0 -17
- package/src/provider/providers/deepinfra.ts +0 -17
- package/src/provider/providers/deepseek.ts +0 -17
- package/src/provider/providers/digitalocean.ts +0 -17
- package/src/provider/providers/fireworks.ts +0 -17
- package/src/provider/providers/gitlab_duo.ts +0 -184
- package/src/provider/providers/gmi.ts +0 -17
- package/src/provider/providers/groq.ts +0 -17
- package/src/provider/providers/helicone.ts +0 -17
- package/src/provider/providers/huggingface.ts +0 -17
- package/src/provider/providers/ionet.ts +0 -17
- package/src/provider/providers/llamacpp.ts +0 -17
- package/src/provider/providers/llmgateway.ts +0 -17
- package/src/provider/providers/lmstudio.ts +0 -17
- package/src/provider/providers/mistral.ts +0 -17
- package/src/provider/providers/moonshot.ts +0 -17
- package/src/provider/providers/nebius.ts +0 -17
- package/src/provider/providers/nvidia.ts +0 -17
- package/src/provider/providers/ollama.ts +0 -17
- package/src/provider/providers/ollama_cloud.ts +0 -17
- package/src/provider/providers/openai.ts +0 -17
- package/src/provider/providers/openai_compatible.ts +0 -293
- package/src/provider/providers/openrouter.ts +0 -175
- package/src/provider/providers/ovhcloud.ts +0 -17
- package/src/provider/providers/sap_aicore.ts +0 -22
- package/src/provider/providers/scaleway.ts +0 -17
- package/src/provider/providers/snowflake_cortex.ts +0 -207
- package/src/provider/providers/stackit.ts +0 -17
- package/src/provider/providers/together.ts +0 -17
- package/src/provider/providers/venice.ts +0 -17
- package/src/provider/providers/vercel.ts +0 -17
- package/src/provider/providers/vertex.ts +0 -229
- package/src/provider/providers/xai.ts +0 -17
- package/src/provider/providers/zai.ts +0 -17
- package/src/provider/providers/zenmux.ts +0 -17
- package/src/provider/schema.ts +0 -143
- package/src/ui/TextInput.tsx +0 -114
- package/src/utils/daemonClient.ts +0 -85
- package/src/utils/logger.ts +0 -204
- package/tsconfig.json +0 -34
- /package/{prompts → dist/prompts}/daily_schedule.md +0 -0
- /package/{prompts → dist/prompts}/memoir.md +0 -0
- /package/{prompts → dist/prompts}/monthly_schedule.md +0 -0
- /package/{prompts → dist/prompts}/objectifier.md +0 -0
- /package/{prompts → dist/prompts}/persona_base_system_prompt.md +0 -0
- /package/{prompts → dist/prompts}/persona_base_system_prompt_fixed.md +0 -0
- /package/{prompts → dist/prompts}/persona_init.md +0 -0
- /package/{prompts → dist/prompts}/schedule_availability.md +0 -0
- /package/{prompts → dist/prompts}/send_message.md +0 -0
- /package/{prompts → dist/prompts}/start_conversation.md +0 -0
package/src/channel/base.ts
DELETED
|
@@ -1,527 +0,0 @@
|
|
|
1
|
-
import { Brain } from "@/brain";
|
|
2
|
-
import type { BrainItemWithChannel } from "@/brain/manager";
|
|
3
|
-
import type { MessageHistoryEntry } from "@/brain/messageHistory";
|
|
4
|
-
import type { AvailabilityStatus } from "@/provider/schema";
|
|
5
|
-
import { logger } from "@/utils/logger";
|
|
6
|
-
import { formatDateKey } from "@/brain/schedule";
|
|
7
|
-
import { Cron, scheduledJobs, type CronCallback } from "croner";
|
|
8
|
-
|
|
9
|
-
const MESSAGE_DEBOUNCE_MS = 1500;
|
|
10
|
-
const IS_CHATTING_DEBOUNCE_MS = 1000 * 60 * 3; // 3m
|
|
11
|
-
const DEFERRED_QUEUE_CAP = 1000;
|
|
12
|
-
const AVAILABILITY_WATCHER_KEY = "__availability-watcher__";
|
|
13
|
-
const AVAILABILITY_WATCHER_PATTERN = "*/5 * * * *";
|
|
14
|
-
const SLEEP_MEMORY_CRON_KEY = "__sleep-memory__";
|
|
15
|
-
const SLEEP_MEMORY_CRON_PATTERN = "0 * * * *"; // every 1 hour
|
|
16
|
-
const START_CONVERSATION_CRON_KEY = "__start-conversation__";
|
|
17
|
-
const START_CONVERSATION_CRON_PATTERN = "*/10 * * * *"; // every 10 min
|
|
18
|
-
const DAILY_SCHEDULE_CRON_KEY = "__daily-schedule__";
|
|
19
|
-
const DAILY_SCHEDULE_CRON_PATTERN = "0 0 * * *"; // every day at 00:00
|
|
20
|
-
const DAILY_SCHEDULE_NOON_CRON_KEY = "__daily-schedule-noon__";
|
|
21
|
-
const DAILY_SCHEDULE_NOON_CRON_PATTERN = "0 12 * * *"; // every day at 12:00 (backup tick)
|
|
22
|
-
|
|
23
|
-
export interface PairingInbound {
|
|
24
|
-
content: string;
|
|
25
|
-
time: Date;
|
|
26
|
-
replyTo?: string;
|
|
27
|
-
channelId?: string;
|
|
28
|
-
chatId?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface PairingEntry {
|
|
32
|
-
brainId: string;
|
|
33
|
-
channelId?: string;
|
|
34
|
-
chatId?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface PairingCompletionResult {
|
|
38
|
-
ok: boolean;
|
|
39
|
-
error?: string;
|
|
40
|
-
brainId?: string;
|
|
41
|
-
displayName?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export abstract class BaseChannel<
|
|
45
|
-
BB extends BrainItemWithChannel = BrainItemWithChannel,
|
|
46
|
-
> {
|
|
47
|
-
private messageInQueue: MessageHistoryEntry[] = [];
|
|
48
|
-
private messageDebounce: NodeJS.Timeout | null = null;
|
|
49
|
-
private isChatting: boolean = false; // Is brain ready to reply to chat?
|
|
50
|
-
private isChattingDebounce: NodeJS.Timeout | null = null;
|
|
51
|
-
private isSending: boolean = false; // Is brain generating messages to send?
|
|
52
|
-
private isSendingQueue: MessageHistoryEntry[] = []; // Messages received while isSending = true
|
|
53
|
-
private deferredQueue: MessageHistoryEntry[] = [];
|
|
54
|
-
private previousAvailability: AvailabilityStatus | null = null;
|
|
55
|
-
private startConversationCounters: Map<string, number> = new Map();
|
|
56
|
-
protected isReady: boolean = false;
|
|
57
|
-
protected pairingMode: boolean = false;
|
|
58
|
-
private startConversationTimeout: boolean = false;
|
|
59
|
-
|
|
60
|
-
private static pairingRegistry = new Map<string, PairingEntry>();
|
|
61
|
-
private static activeChannels = new Map<string, BaseChannel>();
|
|
62
|
-
|
|
63
|
-
constructor(protected readonly brain: Brain<BB>) {
|
|
64
|
-
this.registerCron(
|
|
65
|
-
SLEEP_MEMORY_CRON_KEY,
|
|
66
|
-
SLEEP_MEMORY_CRON_PATTERN,
|
|
67
|
-
async () => {
|
|
68
|
-
const dateKey = formatDateKey(new Date());
|
|
69
|
-
const availability = await this.brain.getAvailability();
|
|
70
|
-
if (availability.status !== "offline") {
|
|
71
|
-
logger.debug(
|
|
72
|
-
`sleepMemory cron: skip — availability=${availability.status}`,
|
|
73
|
-
);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const existing = await this.brain.memory.get(
|
|
77
|
-
`daily-journal:${dateKey}`,
|
|
78
|
-
);
|
|
79
|
-
if (existing) {
|
|
80
|
-
logger.debug(
|
|
81
|
-
`sleepMemory cron: skip — journal for ${dateKey} exists`,
|
|
82
|
-
);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const history = await this.getMessageHistoryBetween(
|
|
86
|
-
new Date(Date.now() - 24 * 60 * 60 * 1000),
|
|
87
|
-
new Date(),
|
|
88
|
-
);
|
|
89
|
-
await this.brain.sleepMemory(new Date(), history);
|
|
90
|
-
},
|
|
91
|
-
);
|
|
92
|
-
this.registerCron(
|
|
93
|
-
DAILY_SCHEDULE_CRON_KEY,
|
|
94
|
-
DAILY_SCHEDULE_CRON_PATTERN,
|
|
95
|
-
() => this.regenerateSchedules(),
|
|
96
|
-
);
|
|
97
|
-
this.registerCron(
|
|
98
|
-
DAILY_SCHEDULE_NOON_CRON_KEY,
|
|
99
|
-
DAILY_SCHEDULE_NOON_CRON_PATTERN,
|
|
100
|
-
() => this.regenerateSchedules(),
|
|
101
|
-
);
|
|
102
|
-
this.registerCron(
|
|
103
|
-
START_CONVERSATION_CRON_KEY,
|
|
104
|
-
START_CONVERSATION_CRON_PATTERN,
|
|
105
|
-
() => this.runStartConversation(),
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private async regenerateSchedules(): Promise<void> {
|
|
110
|
-
logger.debug(
|
|
111
|
-
`regenerateSchedules: tick for ${this.brain.brainbase.displayName}`,
|
|
112
|
-
);
|
|
113
|
-
await this.brain.regenerateSchedules();
|
|
114
|
-
logger.debug(`regenerateSchedules: done`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private async runStartConversation(): Promise<void> {
|
|
118
|
-
if (!this.isReady) {
|
|
119
|
-
logger.debug("startConversation: skip — not ready");
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (this.isChatting || this.startConversationTimeout) {
|
|
123
|
-
logger.debug("startConversation: skip — chat in progress");
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const availability = await this.brain.getAvailability();
|
|
127
|
-
if (availability.status !== "online") {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const now = new Date();
|
|
131
|
-
const dateKey = formatDateKey(now);
|
|
132
|
-
const count = this.startConversationCounters.get(dateKey) ?? 0;
|
|
133
|
-
const countThreshold = this.brain.brainbase.startConversationCountThreshold;
|
|
134
|
-
if (count >= countThreshold) return;
|
|
135
|
-
const nowMs = now.getTime();
|
|
136
|
-
const history = await this.getMessageHistoryBetween(
|
|
137
|
-
new Date(nowMs - 24 * 60 * 60 * 1000),
|
|
138
|
-
now,
|
|
139
|
-
);
|
|
140
|
-
try {
|
|
141
|
-
const replies = await this.brain.sendMessage(history, [], {
|
|
142
|
-
initiate: true,
|
|
143
|
-
send: this.send.bind(this),
|
|
144
|
-
});
|
|
145
|
-
if (replies.length === 0) return;
|
|
146
|
-
this.startConversationCounters.set(dateKey, count + 1);
|
|
147
|
-
this.startConversationTimeout = true;
|
|
148
|
-
setTimeout(
|
|
149
|
-
() => {
|
|
150
|
-
this.startConversationTimeout = false;
|
|
151
|
-
},
|
|
152
|
-
this.brain.brainbase.startConversationTimeThreshold * 60 * 1000,
|
|
153
|
-
);
|
|
154
|
-
} catch (e) {
|
|
155
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
156
|
-
logger.error(`startConversation sendMessage failed: ${reason}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
protected resolveCronPrefix() {
|
|
161
|
-
return `${this.brain.brainbase.brainId}_`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
protected resolveCronName(key: string) {
|
|
165
|
-
return this.resolveCronPrefix() + key;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
protected registerCron<T = undefined>(
|
|
169
|
-
key: string,
|
|
170
|
-
pattern: string,
|
|
171
|
-
callback: CronCallback<T>,
|
|
172
|
-
) {
|
|
173
|
-
const name = this.resolveCronName(key);
|
|
174
|
-
logger.debug(`registerCron: ${name} (${pattern})`);
|
|
175
|
-
new Cron(
|
|
176
|
-
pattern,
|
|
177
|
-
{
|
|
178
|
-
name,
|
|
179
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
180
|
-
catch: (e) => {
|
|
181
|
-
logger.error(`Error while running cron ${name}: ${e}`);
|
|
182
|
-
logger.debug(
|
|
183
|
-
`Cron ${name} stack: ${e instanceof Error ? e.stack : "(no stack)"}`,
|
|
184
|
-
);
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
callback,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
protected getRegisteredCrons() {
|
|
192
|
-
return scheduledJobs
|
|
193
|
-
.filter((c) => c.name && c.name.startsWith(this.resolveCronPrefix()))
|
|
194
|
-
.map((c) => c.name as string);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
protected pauseCron(key: string) {
|
|
198
|
-
const job = scheduledJobs.find((c) => c.name === this.resolveCronName(key));
|
|
199
|
-
if (!job) return false;
|
|
200
|
-
job.pause();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
protected resumeCron(key: string) {
|
|
204
|
-
const job = scheduledJobs.find((c) => c.name === this.resolveCronName(key));
|
|
205
|
-
if (!job) return false;
|
|
206
|
-
job.resume();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
protected removeCron(key: string) {
|
|
210
|
-
const job = scheduledJobs.find((c) => c.name === this.resolveCronName(key));
|
|
211
|
-
if (!job) return false;
|
|
212
|
-
job.stop();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
protected isCronStarted(key: string) {
|
|
216
|
-
const job = scheduledJobs.find((c) => c.name === this.resolveCronName(key));
|
|
217
|
-
if (!job) return false;
|
|
218
|
-
return job.isRunning();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
protected isCronBusy(key: string) {
|
|
222
|
-
const job = scheduledJobs.find((c) => c.name === this.resolveCronName(key));
|
|
223
|
-
if (!job) return false;
|
|
224
|
-
return job.isBusy();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async onMessage(message: MessageHistoryEntry) {
|
|
228
|
-
this.ensureAvailabilityWatcher();
|
|
229
|
-
logger.debug(
|
|
230
|
-
`onMessage: received (${message.content.length} chars, sender=${message.sender})`,
|
|
231
|
-
);
|
|
232
|
-
const availability = await this.brain.getAvailability();
|
|
233
|
-
if (availability.status === "offline") {
|
|
234
|
-
this.deferMessage(message, "offline");
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (
|
|
238
|
-
!this.isChatting &&
|
|
239
|
-
availability.status === "do-not-disturb" &&
|
|
240
|
-
Math.random() > this.brain.brainbase.dndReplyProbability
|
|
241
|
-
) {
|
|
242
|
-
this.deferMessage(message, "dnd");
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
logger.debug(
|
|
246
|
-
`onMessage: passing through (availability=${availability.status})`,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
if (!this.isSending) {
|
|
250
|
-
this.messageInQueue.push(message);
|
|
251
|
-
logger.debug(
|
|
252
|
-
`onMessage: queued (queueSize=${this.messageInQueue.length})`,
|
|
253
|
-
);
|
|
254
|
-
if (this.messageDebounce) clearTimeout(this.messageDebounce);
|
|
255
|
-
this.messageDebounce = setTimeout(async () => {
|
|
256
|
-
const newUserMessages = this.messageInQueue.splice(
|
|
257
|
-
0,
|
|
258
|
-
this.messageInQueue.length,
|
|
259
|
-
);
|
|
260
|
-
this.messageDebounce = null;
|
|
261
|
-
logger.debug(
|
|
262
|
-
`onMessage: debounce fired, dispatching ${newUserMessages.length} message(s)`,
|
|
263
|
-
);
|
|
264
|
-
const now = new Date();
|
|
265
|
-
const twoDaysAgo = new Date(now);
|
|
266
|
-
twoDaysAgo.setDate(now.getDate() - 2);
|
|
267
|
-
this.isSending = true;
|
|
268
|
-
try {
|
|
269
|
-
await this.brain.sendMessage(
|
|
270
|
-
await this.getMessageHistoryBetween(twoDaysAgo, now),
|
|
271
|
-
newUserMessages,
|
|
272
|
-
{ send: this.send.bind(this) },
|
|
273
|
-
);
|
|
274
|
-
} catch (e) {
|
|
275
|
-
logger.error(`Error while sending message: ${e}`);
|
|
276
|
-
logger.debug(
|
|
277
|
-
`onMessage: sendMessage threw — ${e instanceof Error ? e.stack : String(e)}`,
|
|
278
|
-
);
|
|
279
|
-
} finally {
|
|
280
|
-
this.isSending = false;
|
|
281
|
-
|
|
282
|
-
if (this.isSendingQueue.length > 0) {
|
|
283
|
-
const queueMessages = this.isSendingQueue.splice(
|
|
284
|
-
0,
|
|
285
|
-
this.isSendingQueue.length,
|
|
286
|
-
);
|
|
287
|
-
logger.debug(
|
|
288
|
-
`onMessage: draining ${queueMessages.length} queued message(s) from isSendingQueue`,
|
|
289
|
-
);
|
|
290
|
-
let lastMessage: MessageHistoryEntry | undefined = undefined;
|
|
291
|
-
while (!lastMessage && queueMessages.length > 0) {
|
|
292
|
-
lastMessage = queueMessages.splice(-1, 1)[0];
|
|
293
|
-
}
|
|
294
|
-
if (lastMessage) {
|
|
295
|
-
this.messageInQueue.push(...queueMessages);
|
|
296
|
-
void this.onMessage(lastMessage);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}, MESSAGE_DEBOUNCE_MS);
|
|
301
|
-
} else {
|
|
302
|
-
logger.debug(
|
|
303
|
-
`onMessage: isSending — buffering into isSendingQueue (size=${this.isSendingQueue.length + 1})`,
|
|
304
|
-
);
|
|
305
|
-
this.isSendingQueue.push(message);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
this.isChatting = true;
|
|
309
|
-
if (this.isChattingDebounce) clearTimeout(this.isChattingDebounce);
|
|
310
|
-
this.isChattingDebounce = setTimeout(() => {
|
|
311
|
-
this.isChatting = false;
|
|
312
|
-
this.isChattingDebounce = null;
|
|
313
|
-
}, IS_CHATTING_DEBOUNCE_MS);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private ensureAvailabilityWatcher(): void {
|
|
317
|
-
if (this.isCronStarted(AVAILABILITY_WATCHER_KEY)) return;
|
|
318
|
-
logger.debug(
|
|
319
|
-
`ensureAvailabilityWatcher: starting ${AVAILABILITY_WATCHER_PATTERN} watcher`,
|
|
320
|
-
);
|
|
321
|
-
this.registerCron(
|
|
322
|
-
AVAILABILITY_WATCHER_KEY,
|
|
323
|
-
AVAILABILITY_WATCHER_PATTERN,
|
|
324
|
-
async () => {
|
|
325
|
-
const current = await this.brain.getAvailability();
|
|
326
|
-
const prev = this.previousAvailability;
|
|
327
|
-
this.previousAvailability = current.status;
|
|
328
|
-
logger.debug(
|
|
329
|
-
`availabilityWatcher: ${prev ?? "(initial)"} → ${current.status}`,
|
|
330
|
-
);
|
|
331
|
-
if (prev !== null && prev !== "online" && current.status === "online") {
|
|
332
|
-
await this.flushDeferred();
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private async flushDeferred(): Promise<void> {
|
|
339
|
-
if (this.deferredQueue.length === 0) {
|
|
340
|
-
logger.debug(`flushDeferred: nothing to flush`);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const current = await this.brain.getAvailability();
|
|
344
|
-
if (current.status !== "online") {
|
|
345
|
-
logger.debug(
|
|
346
|
-
`flushDeferred: skip — still ${current.status} (deferred size=${this.deferredQueue.length})`,
|
|
347
|
-
);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const drained = this.deferredQueue.splice(0, this.deferredQueue.length);
|
|
351
|
-
logger.debug(`flushDeferred: replaying ${drained.length} message(s)`);
|
|
352
|
-
for (const msg of drained) {
|
|
353
|
-
void this.onMessage(msg);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private deferMessage(message: MessageHistoryEntry, reason: string): void {
|
|
358
|
-
this.deferredQueue.push(message);
|
|
359
|
-
if (this.deferredQueue.length > DEFERRED_QUEUE_CAP) {
|
|
360
|
-
this.deferredQueue.shift();
|
|
361
|
-
logger.warn(
|
|
362
|
-
`Deferred queue over cap (${DEFERRED_QUEUE_CAP}); dropped oldest`,
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
logger.debug(
|
|
366
|
-
`Deferred message (reason=${reason}); queue size=${this.deferredQueue.length}`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// ---------------------------------------------------------------------------
|
|
371
|
-
// Pairing
|
|
372
|
-
//
|
|
373
|
-
// When a brain has a channel token but no channel/chat id, the channel sits
|
|
374
|
-
// in `pairingMode` until the user confirms via the CLI. Every inbound
|
|
375
|
-
// message during pairing triggers `onPairing`, which replies with a fresh
|
|
376
|
-
// code; the CLI consumes that code, persists the inbound's channel/chat id
|
|
377
|
-
// onto the brain, and flips `isReady = true`.
|
|
378
|
-
// ---------------------------------------------------------------------------
|
|
379
|
-
|
|
380
|
-
static generatePairingCode(): string {
|
|
381
|
-
const hex = "0123456789ABCDEF";
|
|
382
|
-
let out = "";
|
|
383
|
-
for (let i = 0; i < 8; i++) {
|
|
384
|
-
if (i === 4) out += "-";
|
|
385
|
-
out += hex[Math.floor(Math.random() * 16)];
|
|
386
|
-
}
|
|
387
|
-
return out;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
protected registerActive(): void {
|
|
391
|
-
BaseChannel.activeChannels.set(this.brain.brainbase.brainId, this);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
protected unregisterActive(): void {
|
|
395
|
-
BaseChannel.activeChannels.delete(this.brain.brainbase.brainId);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
static all(): readonly BaseChannel[] {
|
|
399
|
-
return Array.from(BaseChannel.activeChannels.values());
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
static async shutdownAll(): Promise<void> {
|
|
403
|
-
await Promise.all(BaseChannel.all().map((c) => c.shutdown()));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Tear down this channel. Stops the crons we own, clears pending timers,
|
|
408
|
-
* removes us from the active-channels registry, and finally delegates
|
|
409
|
-
* client/bot teardown to the subclass via {@link teardownClient}.
|
|
410
|
-
*/
|
|
411
|
-
async shutdown(): Promise<void> {
|
|
412
|
-
logger.debug(`shutdown: tearing down ${this.brain.brainbase.displayName}`);
|
|
413
|
-
this.stopOwnCrons();
|
|
414
|
-
this.clearTimers();
|
|
415
|
-
this.unregisterActive();
|
|
416
|
-
await this.teardownClient();
|
|
417
|
-
logger.debug(`shutdown: done`);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
protected stopOwnCrons(): void {
|
|
421
|
-
for (const key of this.getRegisteredCrons()) {
|
|
422
|
-
this.removeCron(key);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
protected clearTimers(): void {
|
|
427
|
-
if (this.messageDebounce) {
|
|
428
|
-
clearTimeout(this.messageDebounce);
|
|
429
|
-
this.messageDebounce = null;
|
|
430
|
-
}
|
|
431
|
-
if (this.isChattingDebounce) {
|
|
432
|
-
clearTimeout(this.isChattingDebounce);
|
|
433
|
-
this.isChattingDebounce = null;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
protected abstract teardownClient(): Promise<void>;
|
|
438
|
-
|
|
439
|
-
protected engagePairing(): void {
|
|
440
|
-
this.pairingMode = true;
|
|
441
|
-
this.isReady = false;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
protected abstract sendPairingReply(
|
|
445
|
-
text: string,
|
|
446
|
-
inbound: PairingInbound,
|
|
447
|
-
): Promise<void>;
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Handle an inbound message while in pairing mode: register a fresh code,
|
|
451
|
-
* then reply with start message + code + help. `onPairing` is wired as the
|
|
452
|
-
* message event listener from each channel subclass.
|
|
453
|
-
*/
|
|
454
|
-
protected async onPairing(inbound: PairingInbound): Promise<void> {
|
|
455
|
-
const code = BaseChannel.generatePairingCode();
|
|
456
|
-
BaseChannel.pairingRegistry.set(code, {
|
|
457
|
-
brainId: this.brain.brainbase.brainId,
|
|
458
|
-
channelId: inbound.channelId,
|
|
459
|
-
chatId: inbound.chatId,
|
|
460
|
-
});
|
|
461
|
-
const displayName = this.brain.brainbase.displayName;
|
|
462
|
-
const text = [
|
|
463
|
-
`🔗 Pairing started for "${displayName}".`,
|
|
464
|
-
``,
|
|
465
|
-
`Your pairing code: ${code}`,
|
|
466
|
-
``,
|
|
467
|
-
`To finish pairing, run this on the host running the daemon:`,
|
|
468
|
-
` brainbox pairing ${code}`,
|
|
469
|
-
``,
|
|
470
|
-
`The code is single-use. Send another message here if you need a new one.`,
|
|
471
|
-
].join("\n");
|
|
472
|
-
logger.info(
|
|
473
|
-
`Pairing code issued for "${displayName}": ${code} (channel=${
|
|
474
|
-
inbound.channelId ?? `chat:${inbound.chatId}`
|
|
475
|
-
})`,
|
|
476
|
-
);
|
|
477
|
-
await this.sendPairingReply(text, inbound);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Finalize pairing by persisting the bound channel/chat id onto the
|
|
482
|
-
* subclass's config. Subclasses override to write the id and refresh
|
|
483
|
-
* their send target before calling super.
|
|
484
|
-
*/
|
|
485
|
-
protected async completePairing(entry: PairingEntry): Promise<void> {
|
|
486
|
-
this.pairingMode = false;
|
|
487
|
-
this.isReady = true;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
static async completePairingByCode(
|
|
491
|
-
code: string,
|
|
492
|
-
): Promise<PairingCompletionResult> {
|
|
493
|
-
const normalized = code.trim().toUpperCase();
|
|
494
|
-
const entry = BaseChannel.pairingRegistry.get(normalized);
|
|
495
|
-
if (!entry) {
|
|
496
|
-
return { ok: false, error: "invalid or expired pairing code" };
|
|
497
|
-
}
|
|
498
|
-
const channel = BaseChannel.activeChannels.get(entry.brainId);
|
|
499
|
-
if (!channel) {
|
|
500
|
-
return {
|
|
501
|
-
ok: false,
|
|
502
|
-
error: "no active channel for that brain (is the daemon running?)",
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
await channel.completePairing(entry);
|
|
507
|
-
} catch (error) {
|
|
508
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
509
|
-
return { ok: false, error: `failed to persist pairing: ${reason}` };
|
|
510
|
-
}
|
|
511
|
-
BaseChannel.pairingRegistry.delete(normalized);
|
|
512
|
-
return {
|
|
513
|
-
ok: true,
|
|
514
|
-
brainId: entry.brainId,
|
|
515
|
-
displayName: channel.brain.brainbase.displayName,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
abstract init(): Promise<void>;
|
|
520
|
-
|
|
521
|
-
abstract getMessageHistoryBetween(
|
|
522
|
-
start: Date,
|
|
523
|
-
end: Date,
|
|
524
|
-
): Promise<ReadonlyArray<MessageHistoryEntry>>;
|
|
525
|
-
abstract send(text: string, opts?: { replyTo?: string }): Promise<void>;
|
|
526
|
-
abstract setAvailability(status: AvailabilityStatus): Promise<void>;
|
|
527
|
-
}
|