@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.47",
3
+ "version": "0.9.48",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -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
- * Recall relevant memories for a project.
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 (from /forget command) */
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
- /** Get summary of all active memories for a project */
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## Cross-Session Memory\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 &lt;number&gt;");
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 memories (with cross-project detection)
497
- const memories = this.memory.recallWithCrossProject(
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 memory + core directives
504
- const coreDirective = "IMPORTANT: Do NOT write files, save to MEMORY.md, or manage your own memory/identity files. Your memory is managed externally by PPMBot. Just respond naturally.";
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
- // ── Memory Save / Session Rotate ────────────────────────────────
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. Memories saved.</i>",
562
+ "<i>Context window near limit — starting fresh session.</i>",
626
563
  );
627
564
  }
628
565