@hienlh/ppm 0.9.47 → 0.9.48
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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.48] - 2026-04-06
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Memory → Identity layer**: Stripped custom memory extraction, decay, periodic AI extraction prompts. PPMBot now stores only identity/preferences + explicit `/remember` facts. Contextual memory delegated to provider's native system (Claude Code MEMORY.md). `ppmbot-memory.ts` 333→111 LOC.
|
|
7
|
+
- **Removed "don't write memory" directive**: AI provider manages its own contextual memory naturally.
|
|
8
|
+
|
|
3
9
|
## [0.9.47] - 2026-04-06
|
|
4
10
|
|
|
5
11
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1097,19 +1097,6 @@ export function deletePPMBotMemoriesByTopic(
|
|
|
1097
1097
|
return matches.length;
|
|
1098
1098
|
}
|
|
1099
1099
|
|
|
1100
|
-
export function decayPPMBotMemories(): void {
|
|
1101
|
-
getDb().query(
|
|
1102
|
-
`UPDATE clawbot_memories
|
|
1103
|
-
SET importance = importance * 0.95,
|
|
1104
|
-
updated_at = unixepoch()
|
|
1105
|
-
WHERE superseded_by IS NULL
|
|
1106
|
-
AND category NOT IN ('preference', 'architecture')
|
|
1107
|
-
AND updated_at < unixepoch() - 604800`,
|
|
1108
|
-
).run();
|
|
1109
|
-
getDb().query(
|
|
1110
|
-
`DELETE FROM clawbot_memories WHERE importance < 0.1 AND superseded_by IS NULL`,
|
|
1111
|
-
).run();
|
|
1112
|
-
}
|
|
1113
1100
|
|
|
1114
1101
|
// ---------------------------------------------------------------------------
|
|
1115
1102
|
// PPMBot pairing helpers
|
|
@@ -4,51 +4,22 @@ import {
|
|
|
4
4
|
getPPMBotMemories,
|
|
5
5
|
supersedePPMBotMemory,
|
|
6
6
|
deletePPMBotMemoriesByTopic,
|
|
7
|
-
decayPPMBotMemories,
|
|
8
|
-
getDb,
|
|
9
7
|
} from "../db.service.ts";
|
|
10
|
-
import { configService } from "../config.service.ts";
|
|
11
8
|
import type {
|
|
12
9
|
PPMBotMemoryCategory,
|
|
13
10
|
MemoryRecallResult,
|
|
14
11
|
} from "../../types/ppmbot.ts";
|
|
15
|
-
import type { ProjectConfig } from "../../types/config.ts";
|
|
16
|
-
|
|
17
|
-
/** Max memories per project before pruning */
|
|
18
|
-
const MAX_MEMORIES_PER_PROJECT = 500;
|
|
19
|
-
|
|
20
|
-
/** Fact extracted from AI response */
|
|
21
|
-
interface ExtractedFact {
|
|
22
|
-
content: string;
|
|
23
|
-
category: PPMBotMemoryCategory;
|
|
24
|
-
importance?: number;
|
|
25
|
-
}
|
|
26
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Lightweight memory layer for PPMBot.
|
|
15
|
+
*
|
|
16
|
+
* Stores identity, preferences, and explicit user facts (/remember).
|
|
17
|
+
* Contextual memory (decisions, architecture, etc.) is left to the
|
|
18
|
+
* AI provider's native memory system (e.g. Claude Code MEMORY.md).
|
|
19
|
+
*/
|
|
27
20
|
export class PPMBotMemory {
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
* If query provided, use FTS5 search. Otherwise return top by importance.
|
|
31
|
-
*/
|
|
32
|
-
recall(project: string, query?: string, limit = 20): MemoryRecallResult[] {
|
|
33
|
-
if (query) {
|
|
34
|
-
const sanitized = this.sanitizeFtsQuery(query);
|
|
35
|
-
if (sanitized) {
|
|
36
|
-
try {
|
|
37
|
-
const results = searchPPMBotMemories(project, sanitized, limit);
|
|
38
|
-
return results.map((r) => ({
|
|
39
|
-
id: r.id,
|
|
40
|
-
content: r.content,
|
|
41
|
-
category: r.category,
|
|
42
|
-
importance: r.importance,
|
|
43
|
-
project: r.project,
|
|
44
|
-
rank: r.rank,
|
|
45
|
-
}));
|
|
46
|
-
} catch {
|
|
47
|
-
// FTS query syntax error — fallback to importance-based
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
21
|
+
/** Get all active memories for a project (+ _global) */
|
|
22
|
+
getSummary(project: string, limit = 30): MemoryRecallResult[] {
|
|
52
23
|
const rows = getPPMBotMemories(project, limit);
|
|
53
24
|
return rows.map((r) => ({
|
|
54
25
|
id: r.id,
|
|
@@ -59,61 +30,7 @@ export class PPMBotMemory {
|
|
|
59
30
|
}));
|
|
60
31
|
}
|
|
61
32
|
|
|
62
|
-
/**
|
|
63
|
-
* Enhanced recall: include memories from mentioned projects.
|
|
64
|
-
* Detects project names in message and fetches their memories too.
|
|
65
|
-
*/
|
|
66
|
-
recallWithCrossProject(
|
|
67
|
-
currentProject: string,
|
|
68
|
-
query: string | undefined,
|
|
69
|
-
message: string,
|
|
70
|
-
limit = 20,
|
|
71
|
-
): MemoryRecallResult[] {
|
|
72
|
-
const mainMemories = this.recall(currentProject, query, limit);
|
|
73
|
-
const mentioned = this.detectMentionedProjects(message, currentProject);
|
|
74
|
-
|
|
75
|
-
if (mentioned.length === 0) return mainMemories;
|
|
76
|
-
|
|
77
|
-
const crossMemories: MemoryRecallResult[] = [];
|
|
78
|
-
for (const proj of mentioned.slice(0, 3)) {
|
|
79
|
-
const projMems = this.recall(proj, query, 5);
|
|
80
|
-
crossMemories.push(...projMems.map((m) => ({ ...m, project: proj })));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return [...mainMemories, ...crossMemories].slice(0, limit);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Save multiple extracted facts. Checks for duplicates via FTS
|
|
88
|
-
* and supersedes old facts when new ones are similar.
|
|
89
|
-
*/
|
|
90
|
-
save(project: string, facts: ExtractedFact[], sessionId?: string): number {
|
|
91
|
-
let inserted = 0;
|
|
92
|
-
for (const fact of facts) {
|
|
93
|
-
if (!fact.content?.trim()) continue;
|
|
94
|
-
|
|
95
|
-
const existingId = this.findSimilar(project, fact.content);
|
|
96
|
-
|
|
97
|
-
const newId = insertPPMBotMemory(
|
|
98
|
-
project,
|
|
99
|
-
fact.content.trim(),
|
|
100
|
-
fact.category || "fact",
|
|
101
|
-
fact.importance ?? 1.0,
|
|
102
|
-
sessionId,
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
if (existingId) {
|
|
106
|
-
supersedePPMBotMemory(existingId, newId);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
inserted++;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
this.pruneExcess(project);
|
|
113
|
-
return inserted;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Save a single fact immediately (from /remember command) */
|
|
33
|
+
/** Save a single fact (identity, /remember) */
|
|
117
34
|
saveOne(
|
|
118
35
|
project: string,
|
|
119
36
|
content: string,
|
|
@@ -128,26 +45,14 @@ export class PPMBotMemory {
|
|
|
128
45
|
return newId;
|
|
129
46
|
}
|
|
130
47
|
|
|
131
|
-
/** Delete memories matching a topic (
|
|
48
|
+
/** Delete memories matching a topic (/forget) */
|
|
132
49
|
forget(project: string, topic: string): number {
|
|
133
50
|
const sanitized = this.sanitizeFtsQuery(topic);
|
|
134
51
|
if (!sanitized) return 0;
|
|
135
52
|
return deletePPMBotMemoriesByTopic(project, sanitized);
|
|
136
53
|
}
|
|
137
54
|
|
|
138
|
-
/**
|
|
139
|
-
getSummary(project: string, limit = 30): MemoryRecallResult[] {
|
|
140
|
-
const rows = getPPMBotMemories(project, limit);
|
|
141
|
-
return rows.map((r) => ({
|
|
142
|
-
id: r.id,
|
|
143
|
-
content: r.content,
|
|
144
|
-
category: r.category,
|
|
145
|
-
importance: r.importance,
|
|
146
|
-
project: r.project,
|
|
147
|
-
}));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Build system prompt section with recalled memories */
|
|
55
|
+
/** Build system prompt section with identity/preferences */
|
|
151
56
|
buildRecallPrompt(memories: MemoryRecallResult[]): string {
|
|
152
57
|
if (memories.length === 0) return "";
|
|
153
58
|
|
|
@@ -158,9 +63,7 @@ export class PPMBotMemory {
|
|
|
158
63
|
grouped.get(cat)!.push(mem.content);
|
|
159
64
|
}
|
|
160
65
|
|
|
161
|
-
let prompt = "\n\n##
|
|
162
|
-
prompt += "The following facts are recalled from previous sessions:\n\n";
|
|
163
|
-
|
|
66
|
+
let prompt = "\n\n## User Identity & Preferences\n";
|
|
164
67
|
for (const [category, facts] of grouped) {
|
|
165
68
|
prompt += `### ${category.charAt(0).toUpperCase() + category.slice(1)}s\n`;
|
|
166
69
|
for (const fact of facts) {
|
|
@@ -168,126 +71,11 @@ export class PPMBotMemory {
|
|
|
168
71
|
}
|
|
169
72
|
prompt += "\n";
|
|
170
73
|
}
|
|
171
|
-
|
|
172
|
-
prompt += "Use these as context. Correct any that seem outdated.\n";
|
|
173
74
|
return prompt;
|
|
174
75
|
}
|
|
175
76
|
|
|
176
|
-
/** Build the extraction prompt sent at session end */
|
|
177
|
-
buildExtractionPrompt(): string {
|
|
178
|
-
return `Summarize the key facts, decisions, and preferences from this conversation as a JSON array. Each entry:
|
|
179
|
-
{"content": "the fact", "category": "fact|decision|preference|architecture|issue", "importance": 0.5-2.0}
|
|
180
|
-
|
|
181
|
-
Rules:
|
|
182
|
-
- Only include facts worth remembering across sessions
|
|
183
|
-
- Skip ephemeral details (file line numbers, temp debug info)
|
|
184
|
-
- Prefer concise, self-contained statements
|
|
185
|
-
- Max 10 entries
|
|
186
|
-
- Return ONLY the JSON array, no markdown fencing
|
|
187
|
-
|
|
188
|
-
If nothing worth remembering, return []`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Parse the AI's extraction response into structured facts.
|
|
193
|
-
* Handles: raw JSON array, markdown-fenced JSON, or graceful failure.
|
|
194
|
-
*/
|
|
195
|
-
parseExtractionResponse(text: string): ExtractedFact[] {
|
|
196
|
-
let cleaned = text.trim();
|
|
197
|
-
if (cleaned.startsWith("```")) {
|
|
198
|
-
cleaned = cleaned.replace(/^```\w*\n?/, "").replace(/\n?```$/, "");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
const parsed = JSON.parse(cleaned);
|
|
203
|
-
if (!Array.isArray(parsed)) return [];
|
|
204
|
-
|
|
205
|
-
return parsed
|
|
206
|
-
.filter(
|
|
207
|
-
(item: unknown): item is Record<string, unknown> =>
|
|
208
|
-
typeof item === "object" && item !== null && "content" in item,
|
|
209
|
-
)
|
|
210
|
-
.map((item) => ({
|
|
211
|
-
content: String(item.content ?? ""),
|
|
212
|
-
category: this.validateCategory(String(item.category ?? "fact")),
|
|
213
|
-
importance: Math.max(0, Math.min(2, Number(item.importance ?? 1))),
|
|
214
|
-
}))
|
|
215
|
-
.filter((f) => f.content.length > 0);
|
|
216
|
-
} catch {
|
|
217
|
-
console.warn("[ppmbot-memory] Failed to parse extraction response");
|
|
218
|
-
return [];
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Regex-based fallback for memory extraction.
|
|
224
|
-
* Used when AI extraction returns empty or fails.
|
|
225
|
-
*/
|
|
226
|
-
extractiveMemoryFallback(conversationText: string): ExtractedFact[] {
|
|
227
|
-
const facts: ExtractedFact[] = [];
|
|
228
|
-
const patterns: Array<{ re: RegExp; category: PPMBotMemoryCategory }> = [
|
|
229
|
-
{ re: /(?:decided|chose|went with|picked|selected)\s+(.{10,100})/gi, category: "decision" },
|
|
230
|
-
{ re: /(?:prefer|always use|like to|rather)\s+(.{10,80})/gi, category: "preference" },
|
|
231
|
-
{ re: /(?:uses?|built with|stack is|powered by|database is)\s+(.{5,80})/gi, category: "architecture" },
|
|
232
|
-
{ re: /(?:bug|issue|problem|broken|fails?|error)\s+(?:with|in|when)\s+(.{10,100})/gi, category: "issue" },
|
|
233
|
-
];
|
|
234
|
-
for (const { re, category } of patterns) {
|
|
235
|
-
let match: RegExpExecArray | null;
|
|
236
|
-
while ((match = re.exec(conversationText)) !== null) {
|
|
237
|
-
const content = (match[1] || match[0]).trim();
|
|
238
|
-
if (content.length > 10) {
|
|
239
|
-
facts.push({ content, category, importance: 0.7 });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return facts.slice(0, 10);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Run importance decay on old memories */
|
|
247
|
-
runDecay(): void {
|
|
248
|
-
try {
|
|
249
|
-
decayPPMBotMemories();
|
|
250
|
-
} catch (err) {
|
|
251
|
-
console.error("[ppmbot-memory] Decay error:", (err as Error).message);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Remove excess memories beyond the per-project cap */
|
|
256
|
-
pruneExcess(project: string, maxCount = MAX_MEMORIES_PER_PROJECT): void {
|
|
257
|
-
try {
|
|
258
|
-
const count = (getDb().query(
|
|
259
|
-
`SELECT COUNT(*) as cnt FROM clawbot_memories
|
|
260
|
-
WHERE project = ? AND superseded_by IS NULL`,
|
|
261
|
-
).get(project) as { cnt: number })?.cnt ?? 0;
|
|
262
|
-
|
|
263
|
-
if (count <= maxCount) return;
|
|
264
|
-
|
|
265
|
-
const excess = count - maxCount;
|
|
266
|
-
getDb().query(
|
|
267
|
-
`DELETE FROM clawbot_memories WHERE id IN (
|
|
268
|
-
SELECT id FROM clawbot_memories
|
|
269
|
-
WHERE project = ? AND superseded_by IS NULL
|
|
270
|
-
ORDER BY importance ASC, updated_at ASC
|
|
271
|
-
LIMIT ?
|
|
272
|
-
)`,
|
|
273
|
-
).run(project, excess);
|
|
274
|
-
} catch (err) {
|
|
275
|
-
console.error("[ppmbot-memory] Prune error:", (err as Error).message);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
77
|
// ── Private ─────────────────────────────────────────────────────
|
|
280
78
|
|
|
281
|
-
/** Detect project names mentioned in user message */
|
|
282
|
-
private detectMentionedProjects(message: string, currentProject: string): string[] {
|
|
283
|
-
const allProjects = configService.get("projects") as ProjectConfig[];
|
|
284
|
-
if (!allProjects?.length) return [];
|
|
285
|
-
return allProjects
|
|
286
|
-
.filter((p) => p.name !== currentProject)
|
|
287
|
-
.filter((p) => message.toLowerCase().includes(p.name.toLowerCase()))
|
|
288
|
-
.map((p) => p.name);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
79
|
/** Find an existing memory similar to the given content */
|
|
292
80
|
private findSimilar(project: string, content: string): number | null {
|
|
293
81
|
const words = content
|
|
@@ -320,14 +108,4 @@ If nothing worth remembering, return []`;
|
|
|
320
108
|
.replace(/\s+/g, " ")
|
|
321
109
|
.trim();
|
|
322
110
|
}
|
|
323
|
-
|
|
324
|
-
/** Validate category string against known values */
|
|
325
|
-
private validateCategory(cat: string): PPMBotMemoryCategory {
|
|
326
|
-
const valid: PPMBotMemoryCategory[] = [
|
|
327
|
-
"fact", "decision", "preference", "architecture", "issue",
|
|
328
|
-
];
|
|
329
|
-
return valid.includes(cat as PPMBotMemoryCategory)
|
|
330
|
-
? (cat as PPMBotMemoryCategory)
|
|
331
|
-
: "fact";
|
|
332
|
-
}
|
|
333
111
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
isPairedChat,
|
|
5
5
|
getPairingByChatId,
|
|
6
6
|
createPairingRequest,
|
|
7
|
-
approvePairing,
|
|
8
7
|
getSessionTitles,
|
|
9
8
|
getApprovedPairedChats,
|
|
10
9
|
} from "../db.service.ts";
|
|
@@ -41,12 +40,6 @@ class PPMBotService {
|
|
|
41
40
|
/** Chat IDs that just received identity onboarding prompt */
|
|
42
41
|
private identityPending = new Set<string>();
|
|
43
42
|
|
|
44
|
-
/** Message count per session for periodic memory save */
|
|
45
|
-
private messageCount = new Map<string, number>();
|
|
46
|
-
|
|
47
|
-
/** Interval (messages) between automatic memory saves */
|
|
48
|
-
private readonly MEMORY_SAVE_INTERVAL = 5;
|
|
49
|
-
|
|
50
43
|
// ── Lifecycle ─────────────────────────────────────────────────
|
|
51
44
|
|
|
52
45
|
async start(): Promise<void> {
|
|
@@ -66,9 +59,6 @@ class PPMBotService {
|
|
|
66
59
|
this.telegram = new PPMBotTelegram(telegramConfig.bot_token);
|
|
67
60
|
this.running = true;
|
|
68
61
|
|
|
69
|
-
// Run memory decay on startup
|
|
70
|
-
this.memory.runDecay();
|
|
71
|
-
|
|
72
62
|
// Start polling (non-blocking)
|
|
73
63
|
this.telegram.startPolling((update) => this.handleUpdate(update));
|
|
74
64
|
|
|
@@ -92,7 +82,6 @@ class PPMBotService {
|
|
|
92
82
|
this.processing.clear();
|
|
93
83
|
this.messageQueue.clear();
|
|
94
84
|
this.identityPending.clear();
|
|
95
|
-
this.messageCount.clear();
|
|
96
85
|
|
|
97
86
|
console.log("[ppmbot] Stopped");
|
|
98
87
|
}
|
|
@@ -247,7 +236,6 @@ class PPMBotService {
|
|
|
247
236
|
return;
|
|
248
237
|
}
|
|
249
238
|
|
|
250
|
-
await this.saveSessionMemory(chatId);
|
|
251
239
|
const session = await this.sessions.switchProject(chatId, args);
|
|
252
240
|
await this.telegram!.sendMessage(
|
|
253
241
|
Number(chatId),
|
|
@@ -256,7 +244,6 @@ class PPMBotService {
|
|
|
256
244
|
}
|
|
257
245
|
|
|
258
246
|
private async cmdNew(chatId: string): Promise<void> {
|
|
259
|
-
await this.saveSessionMemory(chatId);
|
|
260
247
|
const active = this.sessions.getActiveSession(chatId);
|
|
261
248
|
const projectName = active?.projectName;
|
|
262
249
|
await this.sessions.closeSession(chatId);
|
|
@@ -299,7 +286,6 @@ class PPMBotService {
|
|
|
299
286
|
await this.telegram!.sendMessage(Number(chatId), "Usage: /resume <number>");
|
|
300
287
|
return;
|
|
301
288
|
}
|
|
302
|
-
await this.saveSessionMemory(chatId);
|
|
303
289
|
const session = await this.sessions.resumeSessionById(chatId, index);
|
|
304
290
|
if (!session) {
|
|
305
291
|
await this.telegram!.sendMessage(Number(chatId), "Session not found.");
|
|
@@ -325,7 +311,6 @@ class PPMBotService {
|
|
|
325
311
|
}
|
|
326
312
|
|
|
327
313
|
private async cmdStop(chatId: string): Promise<void> {
|
|
328
|
-
await this.saveSessionMemory(chatId);
|
|
329
314
|
await this.sessions.closeSession(chatId);
|
|
330
315
|
await this.telegram!.sendMessage(Number(chatId), "Session ended ✓");
|
|
331
316
|
}
|
|
@@ -493,16 +478,11 @@ class PPMBotService {
|
|
|
493
478
|
this.titledSessions.add(session.sessionId);
|
|
494
479
|
}
|
|
495
480
|
|
|
496
|
-
// Recall
|
|
497
|
-
const memories = this.memory.
|
|
498
|
-
session.projectName,
|
|
499
|
-
text,
|
|
500
|
-
text,
|
|
501
|
-
);
|
|
481
|
+
// Recall identity & preferences (global + project)
|
|
482
|
+
const memories = this.memory.getSummary(session.projectName);
|
|
502
483
|
|
|
503
|
-
// Build system prompt with
|
|
504
|
-
|
|
505
|
-
let systemPrompt = coreDirective + "\n\n" + (config?.system_prompt ?? "");
|
|
484
|
+
// Build system prompt with identity/preferences
|
|
485
|
+
let systemPrompt = config?.system_prompt ?? "";
|
|
506
486
|
const memorySection = this.memory.buildRecallPrompt(memories);
|
|
507
487
|
if (memorySection) {
|
|
508
488
|
systemPrompt += memorySection;
|
|
@@ -546,15 +526,6 @@ class PPMBotService {
|
|
|
546
526
|
},
|
|
547
527
|
);
|
|
548
528
|
|
|
549
|
-
// Periodic memory extraction — fire-and-forget every N messages
|
|
550
|
-
const count = (this.messageCount.get(session.sessionId) ?? 0) + 1;
|
|
551
|
-
this.messageCount.set(session.sessionId, count);
|
|
552
|
-
if (count % this.MEMORY_SAVE_INTERVAL === 0) {
|
|
553
|
-
this.saveSessionMemory(chatId).catch((err) =>
|
|
554
|
-
console.warn("[ppmbot] Periodic memory save failed:", (err as Error).message),
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
529
|
// Check context window — auto-rotate if near limit
|
|
559
530
|
if (
|
|
560
531
|
result.contextWindowPct != null &&
|
|
@@ -581,48 +552,14 @@ class PPMBotService {
|
|
|
581
552
|
}
|
|
582
553
|
}
|
|
583
554
|
|
|
584
|
-
// ──
|
|
585
|
-
|
|
586
|
-
private async saveSessionMemory(chatId: string): Promise<void> {
|
|
587
|
-
const session = this.sessions.getActiveSession(chatId);
|
|
588
|
-
if (!session) return;
|
|
589
|
-
|
|
590
|
-
try {
|
|
591
|
-
const extractionPrompt = this.memory.buildExtractionPrompt();
|
|
592
|
-
const events = chatService.sendMessage(
|
|
593
|
-
session.providerId,
|
|
594
|
-
session.sessionId,
|
|
595
|
-
extractionPrompt,
|
|
596
|
-
{ permissionMode: "bypassPermissions" },
|
|
597
|
-
);
|
|
598
|
-
|
|
599
|
-
let responseText = "";
|
|
600
|
-
for await (const event of events) {
|
|
601
|
-
if (event.type === "text") responseText += event.content;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const facts = this.memory.parseExtractionResponse(responseText);
|
|
605
|
-
if (facts.length > 0) {
|
|
606
|
-
const count = this.memory.save(session.projectName, facts, session.sessionId);
|
|
607
|
-
console.log(`[ppmbot] Saved ${count} memories for ${session.projectName}`);
|
|
608
|
-
} else {
|
|
609
|
-
// Fallback: regex-based extraction
|
|
610
|
-
// Note: we don't have conversation history text here easily,
|
|
611
|
-
// so regex fallback only triggers when AI extraction fails
|
|
612
|
-
console.log("[ppmbot] No memories extracted via AI");
|
|
613
|
-
}
|
|
614
|
-
} catch (err) {
|
|
615
|
-
console.warn("[ppmbot] Memory save failed:", (err as Error).message);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
555
|
+
// ── Session Rotate ──────────────────────────────────────────────
|
|
618
556
|
|
|
619
557
|
private async rotateSession(chatId: string, projectName: string): Promise<void> {
|
|
620
|
-
await this.saveSessionMemory(chatId);
|
|
621
558
|
await this.sessions.closeSession(chatId);
|
|
622
559
|
await this.sessions.getOrCreateSession(chatId, projectName);
|
|
623
560
|
await this.telegram?.sendMessage(
|
|
624
561
|
Number(chatId),
|
|
625
|
-
"<i>Context window near limit — starting fresh session
|
|
562
|
+
"<i>Context window near limit — starting fresh session.</i>",
|
|
626
563
|
);
|
|
627
564
|
}
|
|
628
565
|
|