true-mem 1.0.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/dist/adapters/opencode/index.d.ts +11 -0
  4. package/dist/adapters/opencode/index.d.ts.map +1 -0
  5. package/dist/adapters/opencode/index.js +676 -0
  6. package/dist/adapters/opencode/index.js.map +1 -0
  7. package/dist/adapters/opencode/injection.d.ts +52 -0
  8. package/dist/adapters/opencode/injection.d.ts.map +1 -0
  9. package/dist/adapters/opencode/injection.js +93 -0
  10. package/dist/adapters/opencode/injection.js.map +1 -0
  11. package/dist/config.d.ts +9 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +61 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/extraction/queue.d.ts +35 -0
  16. package/dist/extraction/queue.d.ts.map +1 -0
  17. package/dist/extraction/queue.js +75 -0
  18. package/dist/extraction/queue.js.map +1 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +2900 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/logger.d.ts +6 -0
  24. package/dist/logger.d.ts.map +1 -0
  25. package/dist/logger.js +30 -0
  26. package/dist/logger.js.map +1 -0
  27. package/dist/memory/classifier.d.ts +78 -0
  28. package/dist/memory/classifier.d.ts.map +1 -0
  29. package/dist/memory/classifier.js +363 -0
  30. package/dist/memory/classifier.js.map +1 -0
  31. package/dist/memory/embeddings.d.ts +41 -0
  32. package/dist/memory/embeddings.d.ts.map +1 -0
  33. package/dist/memory/embeddings.js +98 -0
  34. package/dist/memory/embeddings.js.map +1 -0
  35. package/dist/memory/negative-patterns.d.ts +32 -0
  36. package/dist/memory/negative-patterns.d.ts.map +1 -0
  37. package/dist/memory/negative-patterns.js +71 -0
  38. package/dist/memory/negative-patterns.js.map +1 -0
  39. package/dist/memory/patterns.d.ts +110 -0
  40. package/dist/memory/patterns.d.ts.map +1 -0
  41. package/dist/memory/patterns.js +623 -0
  42. package/dist/memory/patterns.js.map +1 -0
  43. package/dist/memory/reconsolidate.d.ts +57 -0
  44. package/dist/memory/reconsolidate.d.ts.map +1 -0
  45. package/dist/memory/reconsolidate.js +96 -0
  46. package/dist/memory/reconsolidate.js.map +1 -0
  47. package/dist/memory/role-patterns.d.ts +50 -0
  48. package/dist/memory/role-patterns.d.ts.map +1 -0
  49. package/dist/memory/role-patterns.js +188 -0
  50. package/dist/memory/role-patterns.js.map +1 -0
  51. package/dist/shutdown.d.ts +31 -0
  52. package/dist/shutdown.d.ts.map +1 -0
  53. package/dist/shutdown.js +120 -0
  54. package/dist/shutdown.js.map +1 -0
  55. package/dist/storage/database.d.ts +77 -0
  56. package/dist/storage/database.d.ts.map +1 -0
  57. package/dist/storage/database.js +590 -0
  58. package/dist/storage/database.js.map +1 -0
  59. package/dist/storage/sqlite-adapter.d.ts +35 -0
  60. package/dist/storage/sqlite-adapter.d.ts.map +1 -0
  61. package/dist/storage/sqlite-adapter.js +99 -0
  62. package/dist/storage/sqlite-adapter.js.map +1 -0
  63. package/dist/types.d.ts +266 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +63 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +54 -0
@@ -0,0 +1,676 @@
1
+ /**
2
+ * True-Memory OpenCode Adapter
3
+ */
4
+ const BUILD_TIME = "2026-02-23T09:45:00.000Z";
5
+ import { DEFAULT_CONFIG } from '../../config.js';
6
+ import { createMemoryDatabase } from '../../storage/database.js';
7
+ import { log } from '../../logger.js';
8
+ import { shouldStoreMemory, classifyWithRoleAwareness, calculateRoleWeightedScore, } from '../../memory/classifier.js';
9
+ import { matchAllPatterns } from '../../memory/patterns.js';
10
+ import { getExtractionQueue } from '../../extraction/queue.js';
11
+ import { registerShutdownHandler, executeShutdown } from '../../shutdown.js';
12
+ import { getAtomicMemories, wrapMemories } from './injection.js';
13
+ // Debounce state for message.updated events
14
+ let messageDebounceTimer = null;
15
+ let pendingMessageEvent = null;
16
+ // Session ID extraction helper
17
+ function getSessionIdFromEvent(properties) {
18
+ if (!properties)
19
+ return undefined;
20
+ const info = properties.info;
21
+ if (info && typeof info.id === 'string')
22
+ return info.id;
23
+ if (typeof properties.sessionID === 'string')
24
+ return properties.sessionID;
25
+ if (typeof properties.id === 'string')
26
+ return properties.id;
27
+ return undefined;
28
+ }
29
+ // Sub-agent detection helper
30
+ function isSubAgentSession(sessionId) {
31
+ // Heuristic: sub-agent sessions typically contain "-task-" in the ID
32
+ return sessionId.includes('-task-');
33
+ }
34
+ // Debounce helper for message.updated events
35
+ function debounceMessageUpdate(state, eventProps, handler) {
36
+ pendingMessageEvent = { properties: eventProps };
37
+ if (messageDebounceTimer) {
38
+ clearTimeout(messageDebounceTimer);
39
+ }
40
+ messageDebounceTimer = setTimeout(() => {
41
+ if (pendingMessageEvent) {
42
+ handler(state, pendingMessageEvent.properties)
43
+ .catch(err => log(`Message processing error: ${err}`));
44
+ }
45
+ pendingMessageEvent = null;
46
+ messageDebounceTimer = null;
47
+ }, 500); // 500ms debounce
48
+ }
49
+ /**
50
+ * Create OpenCode plugin hooks
51
+ */
52
+ export async function createTrueMemoryPlugin(ctx, configOverrides = {}) {
53
+ log('createTrueMemoryPlugin called');
54
+ const config = {
55
+ ...DEFAULT_CONFIG,
56
+ ...configOverrides,
57
+ };
58
+ // Initialize database
59
+ const db = await createMemoryDatabase(config);
60
+ log('Database initialized');
61
+ // Register shutdown handler for database
62
+ registerShutdownHandler('database', () => db.close());
63
+ // Resolve project root
64
+ const worktree = (!ctx.worktree || ctx.worktree === '/' || ctx.worktree === '\\')
65
+ ? ctx.directory
66
+ : ctx.worktree;
67
+ const state = {
68
+ db,
69
+ config,
70
+ currentSessionId: null,
71
+ injectedSessions: new Set(),
72
+ worktree,
73
+ client: ctx.client,
74
+ };
75
+ log(`True-Memory initialized — worktree=${worktree}`);
76
+ // Extract project name and create professional startup message
77
+ const projectName = worktree.split(/[/\\]/).pop() || 'Unknown';
78
+ const startupMessage = `🧠 True-Memory: Plugin loaded successfully | v2.0.1 [${BUILD_TIME}] | Mode: Jaccard Similarity | Project: ${projectName}`;
79
+ // Log to file-based logger only (to avoid overwriting OpenCode TUI during lazy initialization)
80
+ log(startupMessage);
81
+ return {
82
+ event: async ({ event }) => {
83
+ // Skip noisy events
84
+ const silentEvents = new Set(['message.part.delta', 'message.part.updated', 'session.diff']);
85
+ if (silentEvents.has(event.type))
86
+ return;
87
+ log(`Event: ${event.type}`);
88
+ const sessionId = getSessionIdFromEvent(event.properties);
89
+ switch (event.type) {
90
+ case 'session.created':
91
+ await handleSessionCreated(state, sessionId);
92
+ break;
93
+ case 'session.idle':
94
+ // Add extraction job to queue for sequential processing
95
+ queueExtractionJob(state, sessionId);
96
+ break;
97
+ case 'session.deleted':
98
+ case 'session.error':
99
+ await handleSessionEnd(state, event.type, sessionId);
100
+ break;
101
+ case 'server.instance.disposed':
102
+ // Server is shutting down, execute synchronous shutdown
103
+ // CRITICAL: Don't await - Bun handles cleanup automatically
104
+ log('Server instance disposed, triggering sync shutdown');
105
+ executeShutdown('server.instance.disposed');
106
+ break;
107
+ case 'message.updated':
108
+ if (state.config.opencode.extractOnMessage) {
109
+ // Debounce message updates to avoid blocking UI
110
+ debounceMessageUpdate(state, event.properties, handleMessageUpdated);
111
+ }
112
+ break;
113
+ }
114
+ },
115
+ 'tool.execute.before': async (input, output) => {
116
+ const toolInput = input;
117
+ const toolName = toolInput.tool;
118
+ log(`tool.execute.before: ${toolName}`);
119
+ // Only inject for task and background_task tools
120
+ if (toolName !== 'task' && toolName !== 'background_task') {
121
+ return;
122
+ }
123
+ // Extract prompt from output args
124
+ const outputWithArgs = output;
125
+ const originalPrompt = outputWithArgs.args?.prompt;
126
+ if (!originalPrompt) {
127
+ return;
128
+ }
129
+ // Retrieve relevant memories using atomic injection
130
+ try {
131
+ const injectionState = {
132
+ db: state.db,
133
+ worktree: state.worktree,
134
+ };
135
+ const memories = await getAtomicMemories(injectionState, originalPrompt, 10);
136
+ if (memories.length > 0) {
137
+ const wrappedContext = wrapMemories(memories, state.worktree, 'project');
138
+ // Update the prompt in output args
139
+ outputWithArgs.args.prompt = `${wrappedContext}\n\n${originalPrompt}`;
140
+ log(`Atomic injection: ${memories.length} memories injected for ${toolName}`);
141
+ }
142
+ }
143
+ catch (error) {
144
+ log(`Atomic injection failed for ${toolName}: ${error}`);
145
+ // Continue without injection on error
146
+ }
147
+ },
148
+ 'tool.execute.after': async (input, output) => {
149
+ log(`tool.execute.after: ${input.tool}`);
150
+ if (!state.currentSessionId && input.sessionID) {
151
+ state.currentSessionId = input.sessionID;
152
+ }
153
+ if (!state.currentSessionId)
154
+ return;
155
+ await handlePostToolUse(state, input, output);
156
+ },
157
+ 'experimental.chat.system.transform': async (input, output) => {
158
+ log('experimental.chat.system.transform: Injecting global memories');
159
+ try {
160
+ const injectionState = {
161
+ db: state.db,
162
+ worktree: state.worktree,
163
+ };
164
+ // Retrieve global memories (user-level: constraints, preferences, learning, procedural)
165
+ const userLevelClassifications = ['constraint', 'preference', 'learning', 'procedural'];
166
+ const allMemories = injectionState.db.getMemoriesByScope(undefined, 20);
167
+ // Filter only user-level memories for global context
168
+ const globalMemories = allMemories.filter(m => userLevelClassifications.includes(m.classification));
169
+ if (globalMemories.length > 0) {
170
+ const wrappedContext = wrapMemories(globalMemories, state.worktree, 'global');
171
+ // Handle system as string[] - append to the last element
172
+ const systemArray = Array.isArray(output.system) ? output.system : [output.system];
173
+ const lastElement = systemArray[systemArray.length - 1] || '';
174
+ systemArray[systemArray.length - 1] = `${lastElement}\n\n${wrappedContext}`;
175
+ output.system = systemArray;
176
+ log(`Global injection: ${globalMemories.length} memories injected into system prompt`);
177
+ }
178
+ }
179
+ catch (error) {
180
+ log(`Global injection failed: ${error}`);
181
+ // Continue without injection on error
182
+ }
183
+ },
184
+ 'experimental.session.compacting': async (input, output) => {
185
+ log('Compaction hook triggered');
186
+ const sessionId = input.sessionID ?? state.currentSessionId;
187
+ if (state.config.opencode.injectOnCompaction) {
188
+ const memories = await getRelevantMemories(state, state.config.opencode.maxCompactionMemories);
189
+ if (memories.length > 0) {
190
+ const memoryContext = formatMemoriesForInjection(memories, state.worktree);
191
+ output.prompt = buildCompactionPrompt(memoryContext);
192
+ log(`Injected ${memories.length} memories into compaction`);
193
+ }
194
+ else {
195
+ output.prompt = buildCompactionPrompt(null);
196
+ }
197
+ }
198
+ },
199
+ };
200
+ }
201
+ // Queue helper for session idle processing
202
+ function queueExtractionJob(state, sessionId) {
203
+ const queue = getExtractionQueue();
204
+ queue.add({
205
+ description: `session:${sessionId ?? state.currentSessionId}`,
206
+ execute: async () => {
207
+ await processSessionIdle(state, sessionId);
208
+ },
209
+ });
210
+ }
211
+ // Session handlers
212
+ async function handleSessionCreated(state, sessionId) {
213
+ if (!sessionId)
214
+ return;
215
+ state.currentSessionId = sessionId;
216
+ log(`Session created: ${sessionId}`);
217
+ // ✅ Sola creazione sessione - nessun maintenance bloccante
218
+ state.db.createSession(sessionId, state.worktree, { agentType: 'opencode' });
219
+ }
220
+ async function processSessionIdle(state, sessionId) {
221
+ const effectiveSessionId = sessionId ?? state.currentSessionId;
222
+ if (!effectiveSessionId)
223
+ return;
224
+ if (sessionId && !state.currentSessionId) {
225
+ state.currentSessionId = sessionId;
226
+ }
227
+ // Skip extraction for sub-agent sessions to avoid duplicate extraction
228
+ if (isSubAgentSession(effectiveSessionId)) {
229
+ log(`Skipping extraction: sub-agent session detected (${effectiveSessionId})`);
230
+ return;
231
+ }
232
+ const watermark = state.db.getMessageWatermark(effectiveSessionId);
233
+ let messages;
234
+ try {
235
+ const response = await state.client.session.messages({ path: { id: effectiveSessionId } });
236
+ if (response.error) {
237
+ log(`Failed to fetch messages: ${response.error}`);
238
+ return;
239
+ }
240
+ messages = response.data ?? [];
241
+ }
242
+ catch (error) {
243
+ log(`Failed to fetch messages: ${error}`);
244
+ return;
245
+ }
246
+ if (!messages || messages.length <= watermark)
247
+ return;
248
+ const newMessages = messages.slice(watermark);
249
+ const { text: conversationText, lines: roleLines } = extractConversationTextWithRoles(newMessages);
250
+ log('Debug: Clean conversation text (start):', conversationText.slice(0, 200));
251
+ log('Debug: Role-aware lines extracted:', roleLines.length);
252
+ if (!conversationText.trim()) {
253
+ state.db.updateMessageWatermark(effectiveSessionId, messages.length);
254
+ return;
255
+ }
256
+ // Check for injection markers as a final safety net before processing
257
+ const injectionMarkers = [
258
+ /## Relevant Memories from Previous Sessions/i,
259
+ /### User Preferences & Constraints/i,
260
+ /### .* Context/i,
261
+ /## Compaction Instructions/i,
262
+ /\[LTM\]/i,
263
+ /\[STM\]/i,
264
+ ];
265
+ const hasInjectedContent = injectionMarkers.some(marker => marker.test(conversationText));
266
+ if (hasInjectedContent) {
267
+ log(`WARNING: Conversation contains injection markers (safety check), extractConversationText should have filtered them out`);
268
+ // Don't skip extraction - let the filtered conversationText be processed
269
+ }
270
+ // Extract memories using role-aware classifier
271
+ log(`Processing ${newMessages.length} new messages, ${roleLines.length} lines with role info`);
272
+ // Get signals from patterns (applied to full conversation)
273
+ const signals = matchAllPatterns(conversationText);
274
+ log('Debug: Detected signals:', JSON.stringify(signals));
275
+ let extractionAttempted = false;
276
+ let extractionSucceeded = false;
277
+ if (signals.length > 0) {
278
+ extractionAttempted = true;
279
+ try {
280
+ // Process each Human message with role-aware classification
281
+ const humanMessages = roleLines.filter(line => line.role === 'user');
282
+ log(`Debug: Processing ${humanMessages.length} Human messages for memory extraction`);
283
+ for (const humanMsg of humanMessages) {
284
+ const { text, role } = humanMsg;
285
+ // Get signals specific to this message
286
+ const msgSignals = matchAllPatterns(text);
287
+ if (msgSignals.length === 0) {
288
+ continue; // No signals in this message, skip
289
+ }
290
+ log(`Debug: Processing Human message (${msgSignals.length} signals):`, text.slice(0, 100));
291
+ // Calculate base signal score (average weight of matched signals)
292
+ const baseSignalScore = msgSignals.reduce((sum, s) => sum + s.weight, 0) / msgSignals.length;
293
+ // Apply role weighting (10x for Human messages)
294
+ const roleWeightedScore = calculateRoleWeightedScore(baseSignalScore, role, text);
295
+ log(`Debug: Role-weighted score: ${roleWeightedScore.toFixed(2)} (base: ${baseSignalScore.toFixed(2)})`);
296
+ // Build role-aware context
297
+ const roleAwareContext = {
298
+ primaryRole: role,
299
+ roleWeightedScore,
300
+ hasAssistantContext: roleLines.some(line => line.role === 'assistant'),
301
+ fullConversation: conversationText,
302
+ };
303
+ // Classify with role-awareness
304
+ const { classification, confidence, isolatedContent, roleValidated, validationReason } = classifyWithRoleAwareness(text, msgSignals, roleAwareContext);
305
+ log(`Debug: Classification result: ${classification}, confidence: ${confidence.toFixed(2)}, roleValidated: ${roleValidated}, reason: ${validationReason}`);
306
+ if (classification && roleValidated) {
307
+ // Apply three-layer defense
308
+ const result = shouldStoreMemory(isolatedContent, classification, baseSignalScore);
309
+ if (result.store) {
310
+ // Determine scope
311
+ // - User-level classifications: global scope (all projects)
312
+ // - Explicit intent semantic: also global (user said "remember this")
313
+ const userLevelClassifications = ['constraint', 'preference', 'learning', 'procedural'];
314
+ const isExplicitIntentSemantic = classification === 'semantic' && confidence >= 0.85;
315
+ const isUserLevel = userLevelClassifications.includes(classification) || isExplicitIntentSemantic;
316
+ const scope = isUserLevel ? undefined : state.worktree;
317
+ // Determine store: STM vs LTM
318
+ // - Explicit intent (confidence >= 0.85) → LTM (user explicitly said "remember this")
319
+ // - Auto-promote classifications → LTM (bugfix, learning, decision)
320
+ // - Everything else → STM
321
+ const autoPromoteClassifications = ['bugfix', 'learning', 'decision'];
322
+ const isExplicitIntent = confidence >= 0.85;
323
+ const shouldPromoteToLtm = isExplicitIntent || autoPromoteClassifications.includes(classification);
324
+ const store = shouldPromoteToLtm ? 'ltm' : 'stm';
325
+ // Store memory (no embeddings - using Jaccard similarity)
326
+ await state.db.createMemory(store, classification, extractCleanSummary(isolatedContent), // Clean summary without prefixes
327
+ [], {
328
+ sessionId: effectiveSessionId,
329
+ projectScope: scope,
330
+ importance: confidence, // Use confidence from classifyWithRoleAwareness
331
+ confidence: confidence,
332
+ });
333
+ log(`Stored ${classification} memory in ${store.toUpperCase()} (confidence: ${confidence.toFixed(2)}, role: ${role}, reason: ${result.reason})`);
334
+ }
335
+ else {
336
+ log(`Skipped ${classification} memory: ${result.reason}`);
337
+ }
338
+ }
339
+ else if (classification && !roleValidated) {
340
+ log(`Skipped ${classification} memory: ${validationReason}`);
341
+ }
342
+ }
343
+ extractionSucceeded = true;
344
+ }
345
+ catch (error) {
346
+ log(`Extraction failed with critical error: ${error}`);
347
+ // Don't update watermark if extraction failed with critical error
348
+ return;
349
+ }
350
+ }
351
+ // Only update watermark if extraction was attempted and succeeded, or if no extraction was needed
352
+ if (!extractionAttempted || extractionSucceeded) {
353
+ state.db.updateMessageWatermark(effectiveSessionId, messages.length);
354
+ }
355
+ }
356
+ async function handleSessionEnd(state, eventType, sessionId) {
357
+ const effectiveSessionId = sessionId ?? state.currentSessionId;
358
+ if (!effectiveSessionId)
359
+ return;
360
+ // ✅ ESEGUI maintenance alla fine della sessione (non blocca startup)
361
+ try {
362
+ const decayed = state.db.applyDecay();
363
+ const promoted = state.db.runConsolidation();
364
+ if (decayed > 0 || promoted > 0) {
365
+ log(`Maintenance: decayed ${decayed} memories, promoted ${promoted} to LTM`);
366
+ }
367
+ }
368
+ catch (err) {
369
+ log(`Maintenance error: ${err}`);
370
+ }
371
+ const reason = eventType === 'session.error' ? 'abandoned' : 'normal';
372
+ state.db.endSession(effectiveSessionId, reason === 'abandoned' ? 'abandoned' : 'completed');
373
+ state.currentSessionId = null;
374
+ log(`Session ended: ${effectiveSessionId} (${reason})`);
375
+ }
376
+ async function handleMessageUpdated(state, eventProps) {
377
+ const info = eventProps?.info;
378
+ const sessionId = info?.sessionID ?? eventProps?.sessionID ?? state.currentSessionId;
379
+ if (!sessionId)
380
+ return;
381
+ if (!state.currentSessionId && sessionId) {
382
+ state.currentSessionId = sessionId;
383
+ }
384
+ // Lazy injection disabled - using atomic injection via tool.execute.before and experimental.chat.system.transform
385
+ // This avoids duplicate injections and provides more context-aware memory retrieval
386
+ // const role = info?.role ?? (eventProps?.role as string | undefined);
387
+ // if (role === 'user' && !state.injectedSessions.has(sessionId)) {
388
+ // state.injectedSessions.add(sessionId);
389
+ // log(`Lazy injection for session ${sessionId}`);
390
+ //
391
+ // // Extract user's message content for contextual retrieval
392
+ // let userQuery: string | undefined;
393
+ // const parts = info?.parts ?? (eventProps?.parts as Part[] | undefined);
394
+ // if (parts && parts.length > 0) {
395
+ // for (const part of parts) {
396
+ // if (part.type === 'text' && 'text' in part) {
397
+ // userQuery = (part as { text: string }).text;
398
+ // break;
399
+ // }
400
+ // }
401
+ // }
402
+ //
403
+ // const memories = await getRelevantMemories(state, state.config.opencode.maxSessionStartMemories, userQuery);
404
+ // if (memories.length > 0) {
405
+ // const memoryContext = formatMemoriesForInjection(memories, state.worktree);
406
+ // await injectContext(state, sessionId, memoryContext);
407
+ // log(`Lazy injection: ${memories.length} memories`);
408
+ // }
409
+ // }
410
+ }
411
+ async function handlePostToolUse(state, input, output) {
412
+ const sessionId = state.currentSessionId;
413
+ if (!sessionId)
414
+ return;
415
+ const toolOutput = output.output && output.output.length > 2000
416
+ ? output.output.slice(0, 2000) + '...[truncated]'
417
+ : (output.output ?? '');
418
+ state.db.createEvent(sessionId, 'PostToolUse', '', {
419
+ toolName: input.tool,
420
+ toolInput: JSON.stringify(input.args),
421
+ toolOutput,
422
+ });
423
+ }
424
+ // Helpers
425
+ /**
426
+ * Extract a clean summary from conversation text.
427
+ * Removes "Human:" / "Assistant:" prefixes and trims to reasonable length.
428
+ */
429
+ function extractCleanSummary(conversationText, maxLength = 500) {
430
+ // Remove role prefixes
431
+ let cleaned = conversationText
432
+ .replace(/^(Human|Assistant):\s*/gm, '')
433
+ .replace(/\n+/g, ' ')
434
+ .trim();
435
+ // Remove any remaining injection markers (second line of defense)
436
+ const injectionMarkers = [
437
+ /## Relevant Memories from Previous Sessions/gi,
438
+ /### User Preferences & Constraints/gi,
439
+ /### .* Context/gi,
440
+ /## Compaction Instructions/gi,
441
+ /\[LTM\]/gi,
442
+ /\[STM\]/gi,
443
+ ];
444
+ for (const marker of injectionMarkers) {
445
+ cleaned = cleaned.replace(marker, '');
446
+ }
447
+ // Normalize whitespace after marker removal
448
+ cleaned = cleaned.replace(/\s+/g, ' ').trim();
449
+ // Truncate if necessary, try to break at word boundaries
450
+ if (cleaned.length <= maxLength) {
451
+ return cleaned;
452
+ }
453
+ // Find last complete word within limit
454
+ const truncated = cleaned.slice(0, maxLength);
455
+ const lastSpace = truncated.lastIndexOf(' ');
456
+ if (lastSpace > maxLength * 0.8) {
457
+ return truncated.slice(0, lastSpace) + '...';
458
+ }
459
+ return truncated + '...';
460
+ }
461
+ function extractConversationText(messages) {
462
+ const lines = [];
463
+ // Regex patterns that indicate injected content (case-insensitive, should be filtered out)
464
+ const injectionMarkers = [
465
+ /## Relevant Memories from Previous Sessions/i,
466
+ /### User Preferences & Constraints/i,
467
+ /### .* Context/i, // Matches "### ProjectName Context" pattern
468
+ /## Compaction Instructions/i,
469
+ /\[LTM\]/i,
470
+ /\[STM\]/i,
471
+ ];
472
+ // Regex patterns that indicate tool execution or results (should be filtered out)
473
+ const toolMarkers = [
474
+ /\[Tool:\s*\w+\]/i,
475
+ /^Tool Result:/i,
476
+ /^Tool Error:/i,
477
+ /<tool_use>[\s\S]*?<\/tool_use>/gi, // Strip <tool_use> blocks
478
+ /<tool_result>[\s\S]*?<\/tool_result>/gi, // Strip <tool_result> blocks
479
+ /```json[\s\S]*?"tool"[\s\S]*?```/gi, // Strip JSON blobs with tool
480
+ ];
481
+ for (const msg of messages) {
482
+ const role = msg.info.role === 'user' ? 'Human' : 'Assistant';
483
+ for (const part of msg.parts) {
484
+ if (part.type === 'text' && 'text' in part) {
485
+ const text = part.text;
486
+ // Skip parts that contain any injection marker (prevents re-extracting injected content)
487
+ const hasInjectionMarker = injectionMarkers.some(marker => marker.test(text));
488
+ if (hasInjectionMarker) {
489
+ continue; // Skip this part entirely
490
+ }
491
+ // Skip parts that look like tool execution or results
492
+ const hasToolMarker = toolMarkers.some(marker => marker.test(text));
493
+ if (hasToolMarker) {
494
+ continue; // Skip this part entirely
495
+ }
496
+ lines.push(`${role}: ${text}`);
497
+ }
498
+ else if (part.type === 'tool') {
499
+ const toolPart = part;
500
+ if (toolPart.state?.status === 'completed' || toolPart.state?.status === 'error') {
501
+ lines.push(`Assistant: [Tool: ${toolPart.tool}]`);
502
+ if (toolPart.state.output)
503
+ lines.push(`Tool Result: ${toolPart.state.output.slice(0, 2000)}`);
504
+ if (toolPart.state.error)
505
+ lines.push(`Tool Error: ${toolPart.state.error}`);
506
+ }
507
+ }
508
+ }
509
+ }
510
+ return lines.join('\n');
511
+ }
512
+ /**
513
+ * Extract conversation text with role information
514
+ * Returns both the text and role-aware line information
515
+ */
516
+ function extractConversationTextWithRoles(messages) {
517
+ const textLines = [];
518
+ const roleLines = [];
519
+ // Regex patterns that indicate injected content (case-insensitive, should be filtered out)
520
+ const injectionMarkers = [
521
+ /## Relevant Memories from Previous Sessions/i,
522
+ /### User Preferences & Constraints/i,
523
+ /### .* Context/i, // Matches "### ProjectName Context" pattern
524
+ /## Compaction Instructions/i,
525
+ /\[LTM\]/i,
526
+ /\[STM\]/i,
527
+ ];
528
+ // Regex patterns that indicate tool execution or results (should be filtered out)
529
+ const toolMarkers = [
530
+ /\[Tool:\s*\w+\]/i,
531
+ /^Tool Result:/i,
532
+ /^Tool Error:/i,
533
+ /<tool_use>[\s\S]*?<\/tool_use>/gi, // Strip <tool_use> blocks
534
+ /<tool_result>[\s\S]*?<\/tool_result>/gi, // Strip <tool_result> blocks
535
+ /```json[\s\S]*?"tool"[\s\S]*?```/gi, // Strip JSON blobs with tool
536
+ ];
537
+ for (const msg of messages) {
538
+ const role = msg.info.role === 'user' ? 'user' : 'assistant';
539
+ const roleLabel = role === 'user' ? 'Human' : 'Assistant';
540
+ for (const part of msg.parts) {
541
+ if (part.type === 'text' && 'text' in part) {
542
+ const text = part.text;
543
+ // Skip parts that contain any injection marker (prevents re-extracting injected content)
544
+ const hasInjectionMarker = injectionMarkers.some(marker => marker.test(text));
545
+ if (hasInjectionMarker) {
546
+ continue; // Skip this part entirely
547
+ }
548
+ // Skip parts that look like tool execution or results
549
+ const hasToolMarker = toolMarkers.some(marker => marker.test(text));
550
+ if (hasToolMarker) {
551
+ continue; // Skip this part entirely
552
+ }
553
+ textLines.push(`${roleLabel}: ${text}`);
554
+ roleLines.push({
555
+ text,
556
+ role,
557
+ lineNumber: textLines.length - 1,
558
+ });
559
+ }
560
+ else if (part.type === 'tool') {
561
+ const toolPart = part;
562
+ if (toolPart.state?.status === 'completed' || toolPart.state?.status === 'error') {
563
+ const toolText = `Assistant: [Tool: ${toolPart.tool}]`;
564
+ textLines.push(toolText);
565
+ roleLines.push({
566
+ text: toolText,
567
+ role: 'assistant',
568
+ lineNumber: textLines.length - 1,
569
+ });
570
+ if (toolPart.state.output) {
571
+ const outputText = `Tool Result: ${toolPart.state.output.slice(0, 2000)}`;
572
+ textLines.push(outputText);
573
+ roleLines.push({
574
+ text: outputText,
575
+ role: 'assistant',
576
+ lineNumber: textLines.length - 1,
577
+ });
578
+ }
579
+ if (toolPart.state.error) {
580
+ const errorText = `Tool Error: ${toolPart.state.error}`;
581
+ textLines.push(errorText);
582
+ roleLines.push({
583
+ text: errorText,
584
+ role: 'assistant',
585
+ lineNumber: textLines.length - 1,
586
+ });
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ return {
593
+ text: textLines.join('\n'),
594
+ lines: roleLines,
595
+ };
596
+ }
597
+ async function getRelevantMemories(state, limit, query) {
598
+ if (query) {
599
+ // Use Jaccard similarity search (text-based, no embeddings)
600
+ return state.db.vectorSearch(query, state.worktree, limit);
601
+ }
602
+ else {
603
+ // Fall back to scope-based retrieval
604
+ return state.db.getMemoriesByScope(state.worktree, limit);
605
+ }
606
+ }
607
+ function formatMemoriesForInjection(memories, currentProject) {
608
+ const lines = ['## Relevant Memories from Previous Sessions', ''];
609
+ const userLevelClassifications = ['constraint', 'preference', 'learning', 'procedural'];
610
+ const userLevel = memories.filter(m => userLevelClassifications.includes(m.classification));
611
+ const projectLevel = memories.filter(m => !userLevelClassifications.includes(m.classification));
612
+ if (userLevel.length > 0) {
613
+ lines.push('### User Preferences & Constraints');
614
+ lines.push('_These apply across all projects_');
615
+ lines.push('');
616
+ for (const mem of userLevel) {
617
+ const storeLabel = mem.store === 'ltm' ? '[LTM]' : '[STM]';
618
+ lines.push(`- ${storeLabel} [${mem.classification}] ${mem.summary}`);
619
+ }
620
+ lines.push('');
621
+ }
622
+ if (projectLevel.length > 0) {
623
+ const projectName = currentProject ? currentProject.split(/[/\\]/).pop() : 'Current Project';
624
+ lines.push(`### ${projectName} Context`);
625
+ lines.push('');
626
+ for (const mem of projectLevel) {
627
+ const storeLabel = mem.store === 'ltm' ? '[LTM]' : '[STM]';
628
+ lines.push(`- ${storeLabel} [${mem.classification}] ${mem.summary}`);
629
+ }
630
+ lines.push('');
631
+ }
632
+ return lines.join('\n');
633
+ }
634
+ async function injectContext(state, sessionId, context) {
635
+ try {
636
+ await state.client.session.prompt({
637
+ path: { id: sessionId },
638
+ body: {
639
+ noReply: true,
640
+ parts: [{ type: 'text', text: context }],
641
+ },
642
+ });
643
+ }
644
+ catch (error) {
645
+ log(`Failed to inject context: ${error}`);
646
+ }
647
+ }
648
+ function buildCompactionPrompt(memoriesMarkdown) {
649
+ const sections = [];
650
+ if (memoriesMarkdown) {
651
+ sections.push(memoriesMarkdown);
652
+ }
653
+ sections.push(`## Compaction Instructions
654
+
655
+ You are compacting a conversation. Preserve:
656
+
657
+ ### MUST PRESERVE
658
+ - Current task/goal
659
+ - User constraints, preferences, requirements
660
+ - Decisions and rationale
661
+ - Errors and solutions
662
+ - Files modified and why
663
+ - Current state of in-progress work
664
+
665
+ ### CAN DISCARD
666
+ - Verbose tool outputs (summarize)
667
+ - Intermediate reasoning
668
+ - Exploratory discussions
669
+ - Repetitive information
670
+
671
+ ### OUTPUT FORMAT
672
+ Write a structured summary: task, accomplishments, remaining work, critical context.`);
673
+ return sections.join('\n\n');
674
+ }
675
+ export default createTrueMemoryPlugin;
676
+ //# sourceMappingURL=index.js.map