@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.
- package/CHANGELOG.md +3 -50
- package/dist/web/assets/browser-tab--V6I70pH.js +1 -0
- package/dist/web/assets/chat-tab-CrkhvVjF.js +10 -0
- package/dist/web/assets/code-editor-BfMyExLp.js +2 -0
- package/dist/web/assets/{database-viewer-TjRo2b8_.js → database-viewer-CeRUrZKj.js} +1 -1
- package/dist/web/assets/{diff-viewer-BMhCz0xk.js → diff-viewer-D2p3WTMS.js} +1 -1
- package/dist/web/assets/{extension-webview-DiVdlE2r.js → extension-webview-DQWAHMlR.js} +1 -1
- package/dist/web/assets/git-graph-BWRMlCdK.js +1 -0
- package/dist/web/assets/index-C7esr4gM.css +2 -0
- package/dist/web/assets/index-DU6UVgQY.js +30 -0
- package/dist/web/assets/keybindings-store-BE2T8jM9.js +1 -0
- package/dist/web/assets/{markdown-renderer-IyEzLrC6.js → markdown-renderer-C7lKs47M.js} +4 -4
- package/dist/web/assets/{postgres-viewer-CSynGGkJ.js → postgres-viewer-Cr9jpBNd.js} +1 -1
- package/dist/web/assets/{settings-tab-BdI4HhRa.js → settings-tab-DKy-YDg2.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-C5mviyU5.js → sqlite-viewer-9AmeF-Zs.js} +1 -1
- package/dist/web/assets/square-oPKIkJiw.js +1 -0
- package/dist/web/assets/{terminal-tab-CDyC1grg.js → terminal-tab-DFhB4Rxh.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DcVicB_i.js → use-monaco-theme-B7XLw-OX.js} +1 -1
- package/dist/web/index.html +2 -3
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +3 -33
- package/docs/project-changelog.md +0 -47
- package/docs/project-roadmap.md +7 -14
- package/docs/system-architecture.md +2 -65
- package/package.json +1 -1
- package/src/server/index.ts +0 -7
- package/src/server/routes/settings.ts +1 -72
- package/src/services/config.service.ts +1 -1
- package/src/services/db.service.ts +1 -279
- package/src/services/git.service.ts +2 -2
- package/src/types/config.ts +0 -26
- package/src/web/components/browser/browser-tab.tsx +128 -97
- package/src/web/components/chat/chat-history-bar.tsx +3 -8
- package/src/web/components/layout/command-palette.tsx +1 -1
- package/src/web/components/settings/settings-tab.tsx +1 -4
- package/src/web/hooks/use-url-sync.ts +1 -1
- package/dist/web/assets/browser-tab-DnIsHiCc.js +0 -1
- package/dist/web/assets/chat-tab-il6D4jql.js +0 -10
- package/dist/web/assets/code-editor-BUc1jBqm.js +0 -2
- package/dist/web/assets/git-graph-4eGJ8B1A.js +0 -1
- package/dist/web/assets/index-BmcV1di6.js +0 -30
- package/dist/web/assets/index-CcFDEPCo.css +0 -2
- package/dist/web/assets/keybindings-store--5T5hsAj.js +0 -1
- package/dist/web/assets/tab-store-BXMIUvsE.js +0 -1
- package/docs/streaming-input-guide.md +0 -267
- package/snapshot-state.md +0 -1526
- package/src/services/ppmbot/ppmbot-formatter.ts +0 -88
- package/src/services/ppmbot/ppmbot-memory.ts +0 -333
- package/src/services/ppmbot/ppmbot-service.ts +0 -545
- package/src/services/ppmbot/ppmbot-session.ts +0 -199
- package/src/services/ppmbot/ppmbot-streamer.ts +0 -288
- package/src/services/ppmbot/ppmbot-telegram.ts +0 -279
- package/src/types/ppmbot.ts +0 -103
- package/src/web/components/settings/ppmbot-settings-section.tsx +0 -270
- package/test-session-ops.mjs +0 -444
- 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 <name>";
|
|
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 <name>`,
|
|
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 <number>";
|
|
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 <number>");
|
|
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 <topic>");
|
|
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 <fact>");
|
|
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 <name> — Switch project
|
|
346
|
-
/new — Fresh session (current project)
|
|
347
|
-
/sessions — List recent sessions
|
|
348
|
-
/resume <n> — Resume session #n
|
|
349
|
-
/status — Current project/session info
|
|
350
|
-
/stop — End current session
|
|
351
|
-
/memory — Show project memories
|
|
352
|
-
/forget <topic> — Remove matching memories
|
|
353
|
-
/remember <fact> — 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();
|