@hienlh/ppm 0.9.39 → 0.9.41

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +3 -50
  2. package/dist/web/assets/browser-tab--V6I70pH.js +1 -0
  3. package/dist/web/assets/chat-tab-CrkhvVjF.js +10 -0
  4. package/dist/web/assets/code-editor-BfMyExLp.js +2 -0
  5. package/dist/web/assets/{database-viewer-TjRo2b8_.js → database-viewer-CeRUrZKj.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-BMhCz0xk.js → diff-viewer-D2p3WTMS.js} +1 -1
  7. package/dist/web/assets/{extension-webview-DiVdlE2r.js → extension-webview-DQWAHMlR.js} +1 -1
  8. package/dist/web/assets/git-graph-BWRMlCdK.js +1 -0
  9. package/dist/web/assets/index-C7esr4gM.css +2 -0
  10. package/dist/web/assets/index-DU6UVgQY.js +30 -0
  11. package/dist/web/assets/keybindings-store-BE2T8jM9.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-IyEzLrC6.js → markdown-renderer-C7lKs47M.js} +4 -4
  13. package/dist/web/assets/{postgres-viewer-CSynGGkJ.js → postgres-viewer-Cr9jpBNd.js} +1 -1
  14. package/dist/web/assets/{settings-tab-BdI4HhRa.js → settings-tab-DKy-YDg2.js} +1 -1
  15. package/dist/web/assets/{sqlite-viewer-C5mviyU5.js → sqlite-viewer-9AmeF-Zs.js} +1 -1
  16. package/dist/web/assets/square-oPKIkJiw.js +1 -0
  17. package/dist/web/assets/{terminal-tab-CDyC1grg.js → terminal-tab-DFhB4Rxh.js} +1 -1
  18. package/dist/web/assets/{use-monaco-theme-DcVicB_i.js → use-monaco-theme-B7XLw-OX.js} +1 -1
  19. package/dist/web/index.html +2 -3
  20. package/dist/web/sw.js +1 -1
  21. package/docs/codebase-summary.md +3 -33
  22. package/docs/project-changelog.md +0 -47
  23. package/docs/project-roadmap.md +7 -14
  24. package/docs/system-architecture.md +2 -65
  25. package/package.json +1 -1
  26. package/src/server/index.ts +0 -7
  27. package/src/server/routes/settings.ts +1 -72
  28. package/src/services/config.service.ts +1 -1
  29. package/src/services/db.service.ts +1 -279
  30. package/src/services/git.service.ts +2 -2
  31. package/src/types/config.ts +0 -26
  32. package/src/web/components/browser/browser-tab.tsx +128 -97
  33. package/src/web/components/chat/chat-history-bar.tsx +3 -8
  34. package/src/web/components/layout/command-palette.tsx +1 -1
  35. package/src/web/components/settings/settings-tab.tsx +1 -4
  36. package/src/web/hooks/use-url-sync.ts +1 -1
  37. package/dist/web/assets/browser-tab-DnIsHiCc.js +0 -1
  38. package/dist/web/assets/chat-tab-il6D4jql.js +0 -10
  39. package/dist/web/assets/code-editor-BUc1jBqm.js +0 -2
  40. package/dist/web/assets/git-graph-4eGJ8B1A.js +0 -1
  41. package/dist/web/assets/index-BmcV1di6.js +0 -30
  42. package/dist/web/assets/index-CcFDEPCo.css +0 -2
  43. package/dist/web/assets/keybindings-store--5T5hsAj.js +0 -1
  44. package/dist/web/assets/tab-store-BXMIUvsE.js +0 -1
  45. package/docs/streaming-input-guide.md +0 -267
  46. package/snapshot-state.md +0 -1526
  47. package/src/services/ppmbot/ppmbot-formatter.ts +0 -88
  48. package/src/services/ppmbot/ppmbot-memory.ts +0 -333
  49. package/src/services/ppmbot/ppmbot-service.ts +0 -545
  50. package/src/services/ppmbot/ppmbot-session.ts +0 -199
  51. package/src/services/ppmbot/ppmbot-streamer.ts +0 -288
  52. package/src/services/ppmbot/ppmbot-telegram.ts +0 -279
  53. package/src/types/ppmbot.ts +0 -103
  54. package/src/web/components/settings/ppmbot-settings-section.tsx +0 -270
  55. package/test-session-ops.mjs +0 -444
  56. package/test-tokens.mjs +0 -212
@@ -1,545 +0,0 @@
1
- import { configService } from "../config.service.ts";
2
- import { chatService } from "../chat.service.ts";
3
- import {
4
- isPairedChat,
5
- getPairingByChatId,
6
- createPairingRequest,
7
- approvePairing,
8
- } from "../db.service.ts";
9
- import { PPMBotTelegram } from "./ppmbot-telegram.ts";
10
- import { PPMBotSessionManager } from "./ppmbot-session.ts";
11
- import { PPMBotMemory } from "./ppmbot-memory.ts";
12
- import { streamToTelegram } from "./ppmbot-streamer.ts";
13
- import { escapeHtml } from "./ppmbot-formatter.ts";
14
- import type { TelegramUpdate, PPMBotCommand } from "../../types/ppmbot.ts";
15
- import type { PPMBotConfig, TelegramConfig, PermissionMode } from "../../types/config.ts";
16
- import type { SendMessageOpts } from "../../types/chat.ts";
17
-
18
- const CONTEXT_WINDOW_THRESHOLD = 80;
19
-
20
- class PPMBotService {
21
- private telegram: PPMBotTelegram | null = null;
22
- private sessions = new PPMBotSessionManager();
23
- private memory = new PPMBotMemory();
24
- private running = false;
25
-
26
- /** Debounce timers per chatId */
27
- private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
28
- private debouncedTexts = new Map<string, string>();
29
-
30
- /** Processing lock per chatId */
31
- private processing = new Set<string>();
32
-
33
- /** Message queue per chatId for concurrent messages */
34
- private messageQueue = new Map<string, string[]>();
35
-
36
- /** Sessions that already had their title set */
37
- private titledSessions = new Set<string>();
38
-
39
- /** Chat IDs that just received identity onboarding prompt */
40
- private identityPending = new Set<string>();
41
-
42
- /** Message count per session for periodic memory save */
43
- private messageCount = new Map<string, number>();
44
-
45
- /** Interval (messages) between automatic memory saves */
46
- private readonly MEMORY_SAVE_INTERVAL = 5;
47
-
48
- // ── Lifecycle ─────────────────────────────────────────────────
49
-
50
- async start(): Promise<void> {
51
- const ppmbotConfig = this.getConfig();
52
- if (!ppmbotConfig?.enabled) {
53
- console.log("[ppmbot] Disabled in config");
54
- return;
55
- }
56
-
57
- const telegramConfig = configService.get("telegram") as TelegramConfig | undefined;
58
- if (!telegramConfig?.bot_token) {
59
- console.log("[ppmbot] No bot token configured");
60
- return;
61
- }
62
-
63
- try {
64
- this.telegram = new PPMBotTelegram(telegramConfig.bot_token);
65
- this.running = true;
66
-
67
- // Run memory decay on startup
68
- this.memory.runDecay();
69
-
70
- // Start polling (non-blocking)
71
- this.telegram.startPolling((update) => this.handleUpdate(update));
72
-
73
- console.log("[ppmbot] Started");
74
- } catch (err) {
75
- console.error("[ppmbot] Start failed:", (err as Error).message);
76
- }
77
- }
78
-
79
- stop(): void {
80
- this.running = false;
81
- this.telegram?.stop();
82
- this.telegram = null;
83
-
84
- for (const timer of this.debounceTimers.values()) clearTimeout(timer);
85
- this.debounceTimers.clear();
86
- this.debouncedTexts.clear();
87
- this.processing.clear();
88
- this.messageQueue.clear();
89
- this.identityPending.clear();
90
- this.messageCount.clear();
91
-
92
- console.log("[ppmbot] Stopped");
93
- }
94
-
95
- get isRunning(): boolean {
96
- return this.running;
97
- }
98
-
99
- /** Notify user on Telegram that their pairing was approved */
100
- async notifyPairingApproved(chatId: string): Promise<void> {
101
- await this.telegram?.sendMessage(
102
- Number(chatId),
103
- "✅ Pairing approved! You can now chat with PPMBot.\n\nSend /start to begin.",
104
- );
105
- }
106
-
107
- // ── Update Routing ──────────────────────────────────────────────
108
-
109
- private async handleUpdate(update: TelegramUpdate): Promise<void> {
110
- if (!this.telegram) return;
111
- const message = update.message;
112
- if (!message?.chat?.id) return;
113
-
114
- const chatId = String(message.chat.id);
115
- const userId = message.from?.id ?? 0;
116
- const displayName = message.from?.first_name ?? message.from?.username ?? "Unknown";
117
-
118
- // Pairing-based access control
119
- if (!isPairedChat(chatId)) {
120
- const pairing = getPairingByChatId(chatId);
121
- if (!pairing) {
122
- // First-time user — generate pairing code
123
- const code = this.generatePairingCode();
124
- createPairingRequest(chatId, String(userId), displayName, code);
125
- await this.telegram!.sendMessage(
126
- Number(chatId),
127
- `🔐 Pairing required.\n\nYour pairing code: <code>${code}</code>\n\nEnter this code in PPM Settings → PPMBot → Pair Device to approve access.`,
128
- );
129
- return;
130
- }
131
- if (pairing.status === "pending") {
132
- await this.telegram!.sendMessage(
133
- Number(chatId),
134
- `⏳ Pairing pending approval.\n\nCode: <code>${pairing.pairing_code}</code>\nAsk the PPM owner to approve in Settings → PPMBot.`,
135
- );
136
- return;
137
- }
138
- if (pairing.status === "revoked") {
139
- return; // Silently ignore
140
- }
141
- }
142
-
143
- // Try parsing as command
144
- const command = PPMBotTelegram.parseCommand(message);
145
- if (command) {
146
- await this.handleCommand(command);
147
- return;
148
- }
149
-
150
- // Regular message
151
- const text = message.text ?? message.caption ?? "";
152
- if (!text.trim()) return;
153
-
154
- await this.handleMessage(chatId, text);
155
- }
156
-
157
- // ── Command Handlers ────────────────────────────────────────────
158
-
159
- private async handleCommand(cmd: PPMBotCommand): Promise<void> {
160
- const chatId = String(cmd.chatId);
161
- const tg = this.telegram!;
162
-
163
- try {
164
- switch (cmd.command) {
165
- case "start": await this.cmdStart(chatId); break;
166
- case "project": await this.cmdProject(chatId, cmd.args); break;
167
- case "new": await this.cmdNew(chatId); break;
168
- case "sessions": await this.cmdSessions(chatId); break;
169
- case "resume": await this.cmdResume(chatId, cmd.args); break;
170
- case "status": await this.cmdStatus(chatId); break;
171
- case "stop": await this.cmdStop(chatId); break;
172
- case "memory": await this.cmdMemory(chatId); break;
173
- case "forget": await this.cmdForget(chatId, cmd.args); break;
174
- case "remember": await this.cmdRemember(chatId, cmd.args); break;
175
- case "help": await this.cmdHelp(chatId); break;
176
- default: await tg.sendMessage(Number(chatId), `Unknown command: /${cmd.command}`);
177
- }
178
- } catch (err) {
179
- await tg.sendMessage(
180
- Number(chatId),
181
- `❌ Command error: ${escapeHtml((err as Error).message)}`,
182
- );
183
- }
184
- }
185
-
186
- private async cmdStart(chatId: string): Promise<void> {
187
- const projects = this.sessions.getProjectNames();
188
- let text = "<b>🤖 PPMBot</b>\n\n";
189
- text += "Hey! I'm your AI coding assistant, right here in Telegram.\n";
190
- text += "Ask me anything — code questions, debugging, project tasks.\n\n";
191
- if (projects.length) {
192
- text += "<b>Your projects:</b>\n";
193
- for (const name of projects) {
194
- text += ` • <code>${escapeHtml(name)}</code>\n`;
195
- }
196
- text += "\nSwitch: /project &lt;name&gt;";
197
- } else {
198
- text += "No projects configured — I'll use a default workspace.";
199
- }
200
- text += "\n\nJust send a message to start chatting, or /help for commands.";
201
- await this.telegram!.sendMessage(Number(chatId), text);
202
-
203
- // Identity onboarding: if no identity memories exist, ask user
204
- const identityMemories = this.memory.recall("_global", "user identity name role");
205
- if (identityMemories.length === 0) {
206
- this.identityPending.add(chatId);
207
- await this.telegram!.sendMessage(
208
- Number(chatId),
209
- "📝 <b>Quick intro?</b>\n\n" +
210
- "I don't know much about you yet! Tell me:\n" +
211
- "• Your name\n" +
212
- "• What you work on (language, stack, role)\n" +
213
- "• Preferred response language (English, Vietnamese, etc.)\n\n" +
214
- "I'll remember your preferences for future chats.\n" +
215
- "Or skip this and just start chatting!",
216
- );
217
- }
218
- }
219
-
220
- private async cmdProject(chatId: string, args: string): Promise<void> {
221
- if (!args) {
222
- const active = this.sessions.getActiveSession(chatId);
223
- const current = active?.projectName ?? "(none)";
224
- await this.telegram!.sendMessage(
225
- Number(chatId),
226
- `Current project: <b>${escapeHtml(current)}</b>\n\nUsage: /project &lt;name&gt;`,
227
- );
228
- return;
229
- }
230
-
231
- await this.saveSessionMemory(chatId);
232
- const session = await this.sessions.switchProject(chatId, args);
233
- await this.telegram!.sendMessage(
234
- Number(chatId),
235
- `Switched to <b>${escapeHtml(session.projectName)}</b> ✓`,
236
- );
237
- }
238
-
239
- private async cmdNew(chatId: string): Promise<void> {
240
- await this.saveSessionMemory(chatId);
241
- const active = this.sessions.getActiveSession(chatId);
242
- const projectName = active?.projectName;
243
- await this.sessions.closeSession(chatId);
244
- const session = await this.sessions.getOrCreateSession(chatId, projectName ?? undefined);
245
- await this.telegram!.sendMessage(
246
- Number(chatId),
247
- `New session for <b>${escapeHtml(session.projectName)}</b> ✓`,
248
- );
249
- }
250
-
251
- private async cmdSessions(chatId: string): Promise<void> {
252
- const sessions = this.sessions.listRecentSessions(chatId, 10);
253
- if (sessions.length === 0) {
254
- await this.telegram!.sendMessage(Number(chatId), "No recent sessions.");
255
- return;
256
- }
257
- let text = "<b>Recent Sessions</b>\n\n";
258
- sessions.forEach((s, i) => {
259
- const active = s.is_active ? " ⬤" : "";
260
- const date = new Date(s.last_message_at * 1000).toLocaleDateString();
261
- text += `${i + 1}. <code>${escapeHtml(s.project_name)}</code> — ${date}${active}\n`;
262
- });
263
- text += "\nResume: /resume &lt;number&gt;";
264
- await this.telegram!.sendMessage(Number(chatId), text);
265
- }
266
-
267
- private async cmdResume(chatId: string, args: string): Promise<void> {
268
- const index = parseInt(args, 10);
269
- if (!index || index < 1) {
270
- await this.telegram!.sendMessage(Number(chatId), "Usage: /resume &lt;number&gt;");
271
- return;
272
- }
273
- await this.saveSessionMemory(chatId);
274
- const session = await this.sessions.resumeSessionById(chatId, index);
275
- if (!session) {
276
- await this.telegram!.sendMessage(Number(chatId), "Session not found.");
277
- return;
278
- }
279
- await this.telegram!.sendMessage(
280
- Number(chatId),
281
- `Resumed session for <b>${escapeHtml(session.projectName)}</b> ✓`,
282
- );
283
- }
284
-
285
- private async cmdStatus(chatId: string): Promise<void> {
286
- const active = this.sessions.getActiveSession(chatId);
287
- if (!active) {
288
- await this.telegram!.sendMessage(Number(chatId), "No active session. Send a message to start.");
289
- return;
290
- }
291
- let text = "<b>Status</b>\n\n";
292
- text += `Project: <code>${escapeHtml(active.projectName)}</code>\n`;
293
- text += `Provider: <code>${escapeHtml(active.providerId)}</code>\n`;
294
- text += `Session: <code>${active.sessionId.slice(0, 12)}…</code>\n`;
295
- await this.telegram!.sendMessage(Number(chatId), text);
296
- }
297
-
298
- private async cmdStop(chatId: string): Promise<void> {
299
- await this.saveSessionMemory(chatId);
300
- await this.sessions.closeSession(chatId);
301
- await this.telegram!.sendMessage(Number(chatId), "Session ended ✓");
302
- }
303
-
304
- private async cmdMemory(chatId: string): Promise<void> {
305
- const active = this.sessions.getActiveSession(chatId);
306
- const project = active?.projectName ?? "_global";
307
- const memories = this.memory.getSummary(project);
308
- if (memories.length === 0) {
309
- await this.telegram!.sendMessage(Number(chatId), "No memories stored for this project.");
310
- return;
311
- }
312
- let text = `<b>Memory — ${escapeHtml(project)}</b>\n\n`;
313
- for (const mem of memories) {
314
- text += `• [${mem.category}] ${escapeHtml(mem.content)}\n`;
315
- }
316
- await this.telegram!.sendMessage(Number(chatId), text);
317
- }
318
-
319
- private async cmdForget(chatId: string, args: string): Promise<void> {
320
- if (!args) {
321
- await this.telegram!.sendMessage(Number(chatId), "Usage: /forget &lt;topic&gt;");
322
- return;
323
- }
324
- const active = this.sessions.getActiveSession(chatId);
325
- const project = active?.projectName ?? "_global";
326
- const count = this.memory.forget(project, args);
327
- await this.telegram!.sendMessage(Number(chatId), `Forgot ${count} memor${count === 1 ? "y" : "ies"} ✓`);
328
- }
329
-
330
- private async cmdRemember(chatId: string, args: string): Promise<void> {
331
- if (!args) {
332
- await this.telegram!.sendMessage(Number(chatId), "Usage: /remember &lt;fact&gt;");
333
- return;
334
- }
335
- const active = this.sessions.getActiveSession(chatId);
336
- const project = active?.projectName ?? "_global";
337
- this.memory.saveOne(project, args, "fact", active?.sessionId);
338
- await this.telegram!.sendMessage(Number(chatId), "Remembered ✓");
339
- }
340
-
341
- private async cmdHelp(chatId: string): Promise<void> {
342
- const text = `<b>PPMBot Commands</b>
343
-
344
- /start — Greeting + list projects
345
- /project &lt;name&gt; — Switch project
346
- /new — Fresh session (current project)
347
- /sessions — List recent sessions
348
- /resume &lt;n&gt; — Resume session #n
349
- /status — Current project/session info
350
- /stop — End current session
351
- /memory — Show project memories
352
- /forget &lt;topic&gt; — Remove matching memories
353
- /remember &lt;fact&gt; — Save a fact
354
- /help — This message`;
355
- await this.telegram!.sendMessage(Number(chatId), text);
356
- }
357
-
358
- // ── Chat Message Pipeline ───────────────────────────────────────
359
-
360
- private async handleMessage(chatId: string, text: string): Promise<void> {
361
- const config = this.getConfig();
362
- const debounceMs = config?.debounce_ms ?? 2000;
363
-
364
- const existing = this.debouncedTexts.get(chatId) ?? "";
365
- const merged = existing ? `${existing}\n${text}` : text;
366
- this.debouncedTexts.set(chatId, merged.length > 10000 ? merged.slice(0, 10000) : merged);
367
-
368
- const prevTimer = this.debounceTimers.get(chatId);
369
- if (prevTimer) clearTimeout(prevTimer);
370
-
371
- this.debounceTimers.set(
372
- chatId,
373
- setTimeout(() => this.processMessage(chatId), debounceMs),
374
- );
375
- }
376
-
377
- private async processMessage(chatId: string): Promise<void> {
378
- this.debounceTimers.delete(chatId);
379
- const text = this.debouncedTexts.get(chatId) ?? "";
380
- this.debouncedTexts.delete(chatId);
381
- if (!text.trim()) return;
382
-
383
- // Queue if already processing
384
- if (this.processing.has(chatId)) {
385
- const queue = this.messageQueue.get(chatId) ?? [];
386
- queue.push(text);
387
- this.messageQueue.set(chatId, queue);
388
- return;
389
- }
390
- this.processing.add(chatId);
391
-
392
- try {
393
- const config = this.getConfig();
394
-
395
- const session = await this.sessions.getOrCreateSession(chatId);
396
-
397
- // Update title on first message only
398
- if (!this.titledSessions.has(session.sessionId)) {
399
- this.sessions.updateSessionTitle(session.sessionId, text);
400
- this.titledSessions.add(session.sessionId);
401
- }
402
-
403
- // Recall memories (with cross-project detection)
404
- const memories = this.memory.recallWithCrossProject(
405
- session.projectName,
406
- text,
407
- text,
408
- );
409
-
410
- // Build system prompt with memory
411
- let systemPrompt = config?.system_prompt ?? "";
412
- const memorySection = this.memory.buildRecallPrompt(memories);
413
- if (memorySection) {
414
- systemPrompt += memorySection;
415
- }
416
-
417
- // Send message to AI (prepend system prompt + memory context)
418
- const opts: SendMessageOpts = {
419
- permissionMode: (config?.permission_mode ?? "bypassPermissions") as PermissionMode,
420
- };
421
-
422
- let fullMessage = text;
423
- if (systemPrompt) {
424
- fullMessage = `<system-context>\n${systemPrompt}\n</system-context>\n\n${text}`;
425
- }
426
-
427
- const events = chatService.sendMessage(
428
- session.providerId,
429
- session.sessionId,
430
- fullMessage,
431
- opts,
432
- );
433
-
434
- // Stream response to Telegram
435
- const result = await streamToTelegram(
436
- Number(chatId),
437
- events,
438
- this.telegram!,
439
- {
440
- showToolCalls: config?.show_tool_calls ?? true,
441
- showThinking: config?.show_thinking ?? false,
442
- },
443
- );
444
-
445
- // Capture identity if onboarding was just shown
446
- if (this.identityPending.has(chatId)) {
447
- this.identityPending.delete(chatId);
448
- this.memory.saveOne("_global", `User identity: ${text}`, "preference", session.sessionId);
449
- console.log("[ppmbot] Saved identity memory from onboarding");
450
- }
451
-
452
- // Periodic memory extraction — fire-and-forget every N messages
453
- const count = (this.messageCount.get(session.sessionId) ?? 0) + 1;
454
- this.messageCount.set(session.sessionId, count);
455
- if (count % this.MEMORY_SAVE_INTERVAL === 0) {
456
- this.saveSessionMemory(chatId).catch((err) =>
457
- console.warn("[ppmbot] Periodic memory save failed:", (err as Error).message),
458
- );
459
- }
460
-
461
- // Check context window — auto-rotate if near limit
462
- if (
463
- result.contextWindowPct != null &&
464
- result.contextWindowPct > CONTEXT_WINDOW_THRESHOLD
465
- ) {
466
- await this.rotateSession(chatId, session.projectName);
467
- }
468
- } catch (err) {
469
- console.error(`[ppmbot] processMessage error for ${chatId}:`, (err as Error).message);
470
- await this.telegram?.sendMessage(
471
- Number(chatId),
472
- `❌ ${escapeHtml((err as Error).message)}`,
473
- );
474
- } finally {
475
- this.processing.delete(chatId);
476
-
477
- // Process queued messages
478
- const queued = this.messageQueue.get(chatId);
479
- if (queued && queued.length > 0) {
480
- this.messageQueue.delete(chatId);
481
- const merged = queued.join("\n\n");
482
- this.handleMessage(chatId, merged);
483
- }
484
- }
485
- }
486
-
487
- // ── Memory Save / Session Rotate ────────────────────────────────
488
-
489
- private async saveSessionMemory(chatId: string): Promise<void> {
490
- const session = this.sessions.getActiveSession(chatId);
491
- if (!session) return;
492
-
493
- try {
494
- const extractionPrompt = this.memory.buildExtractionPrompt();
495
- const events = chatService.sendMessage(
496
- session.providerId,
497
- session.sessionId,
498
- extractionPrompt,
499
- { permissionMode: "bypassPermissions" },
500
- );
501
-
502
- let responseText = "";
503
- for await (const event of events) {
504
- if (event.type === "text") responseText += event.content;
505
- }
506
-
507
- const facts = this.memory.parseExtractionResponse(responseText);
508
- if (facts.length > 0) {
509
- const count = this.memory.save(session.projectName, facts, session.sessionId);
510
- console.log(`[ppmbot] Saved ${count} memories for ${session.projectName}`);
511
- } else {
512
- // Fallback: regex-based extraction
513
- // Note: we don't have conversation history text here easily,
514
- // so regex fallback only triggers when AI extraction fails
515
- console.log("[ppmbot] No memories extracted via AI");
516
- }
517
- } catch (err) {
518
- console.warn("[ppmbot] Memory save failed:", (err as Error).message);
519
- }
520
- }
521
-
522
- private async rotateSession(chatId: string, projectName: string): Promise<void> {
523
- await this.saveSessionMemory(chatId);
524
- await this.sessions.closeSession(chatId);
525
- await this.sessions.getOrCreateSession(chatId, projectName);
526
- await this.telegram?.sendMessage(
527
- Number(chatId),
528
- "<i>Context window near limit — starting fresh session. Memories saved.</i>",
529
- );
530
- }
531
-
532
- // ── Helpers ─────────────────────────────────────────────────────
533
-
534
- private generatePairingCode(): string {
535
- const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I,O,0,1 (ambiguous)
536
- const bytes = crypto.getRandomValues(new Uint8Array(6));
537
- return Array.from(bytes, (b) => chars[b % chars.length]).join("");
538
- }
539
-
540
- private getConfig(): PPMBotConfig | undefined {
541
- return configService.get("clawbot") as PPMBotConfig | undefined;
542
- }
543
- }
544
-
545
- export const ppmbotService = new PPMBotService();