nova-terminal-assistant 0.1.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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,657 @@
1
+ // ============================================================================
2
+ // LayeredMemoryManager - L1-L4 layered memory architecture
3
+ // Reference: Letta/MemGPT memory system with context budget management
4
+ // ============================================================================
5
+
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import type { Message, SessionId } from '../types/session.js';
10
+ import { ContextCompressor, type StructuredSummary, type FileChange, type Decision } from './ContextCompressor.js';
11
+
12
+ // --- Memory Layer Types ---
13
+
14
+ export interface MemoryEntry {
15
+ id: string;
16
+ content: string;
17
+ layer: MemoryLayer;
18
+ category: MemoryCategory;
19
+ tags: string[];
20
+ createdAt: number;
21
+ updatedAt: number;
22
+ accessCount: number;
23
+ lastAccessedAt: number;
24
+ /** Approximate token count */
25
+ tokenCount: number;
26
+ /** Optional TTL in ms; entry expires after this */
27
+ ttl?: number;
28
+ /** Relevance score (0-1), used for retrieval ranking */
29
+ relevanceScore?: number;
30
+ }
31
+
32
+ export type MemoryLayer = 'L1_immediate' | 'L2_working' | 'L3_longterm' | 'L4_archival';
33
+
34
+ export type MemoryCategory = 'project' | 'user' | 'convention' | 'decision' | 'pattern' | 'error';
35
+
36
+ export interface MemoryContext {
37
+ /** L1 messages - always injected into the context */
38
+ immediate: Message[];
39
+ /** L2 working summary - session-level persistence */
40
+ workingSummary: StructuredSummary | null;
41
+ /** L3 relevant long-term memory entries */
42
+ longTermEntries: MemoryEntry[];
43
+ /** L4 archival entries (only loaded on demand) */
44
+ archivalEntries: MemoryEntry[];
45
+ /** Total estimated token count of all loaded memory */
46
+ totalTokens: number;
47
+ }
48
+
49
+ export interface MemoryQuery {
50
+ /** The task or query to match against */
51
+ query: string;
52
+ /** Maximum number of entries to retrieve */
53
+ maxEntries?: number;
54
+ /** Minimum relevance score (0-1) */
55
+ minScore?: number;
56
+ /** Which layers to search */
57
+ layers?: MemoryLayer[];
58
+ /** Which categories to search */
59
+ categories?: MemoryCategory[];
60
+ /** Maximum total tokens for returned entries */
61
+ maxTokens?: number;
62
+ }
63
+
64
+ export interface LayeredMemoryOptions {
65
+ /** Session ID for persistence isolation */
66
+ sessionId: SessionId;
67
+ /** Base directory for memory persistence */
68
+ storageDir?: string;
69
+ /** L1: Maximum number of recent messages to keep (default: 10) */
70
+ l1MaxMessages?: number;
71
+ /** L2: Maximum token budget for working summary (default: 2000) */
72
+ l2MaxTokens?: number;
73
+ /** L3: Maximum entries to load into context (default: 20) */
74
+ l3MaxEntries?: number;
75
+ /** L4: Maximum entries to retrieve from archival (default: 5) */
76
+ l4MaxRetrieveEntries?: number;
77
+ /** Total token budget for all memory layers combined (default: 20000) */
78
+ totalTokenBudget?: number;
79
+ }
80
+
81
+ // --- Circular Buffer for L1 ---
82
+
83
+ class CircularBuffer<T> {
84
+ private buffer: T[] = [];
85
+ private maxSize: number;
86
+
87
+ constructor(maxSize: number) {
88
+ this.maxSize = maxSize;
89
+ }
90
+
91
+ push(item: T): void {
92
+ if (this.buffer.length >= this.maxSize) {
93
+ this.buffer.shift();
94
+ }
95
+ this.buffer.push(item);
96
+ }
97
+
98
+ getAll(): T[] {
99
+ return [...this.buffer];
100
+ }
101
+
102
+ get size(): number {
103
+ return this.buffer.length;
104
+ }
105
+
106
+ clear(): void {
107
+ this.buffer = [];
108
+ }
109
+ }
110
+
111
+ // --- LayeredMemoryManager ---
112
+
113
+ export class LayeredMemoryManager {
114
+ private readonly sessionId: SessionId;
115
+ private readonly storageDir: string;
116
+
117
+ // L1: Immediate memory - circular buffer of recent messages
118
+ private immediateBuffer: CircularBuffer<Message>;
119
+
120
+ // L2: Working memory - structured session summary
121
+ private compressor: ContextCompressor;
122
+ private workingSummary: StructuredSummary | null = null;
123
+
124
+ // L3: Long-term memory - in-memory entries with persistence
125
+ private longTermMemory: Map<string, MemoryEntry> = new Map();
126
+
127
+ // L4: Archival memory - persisted to disk, loaded on demand
128
+ private archivalIndex: Map<string, MemoryEntry> = new Map();
129
+
130
+ // Configuration
131
+ private readonly l2MaxTokens: number;
132
+ private readonly l3MaxEntries: number;
133
+ private readonly l4MaxRetrieveEntries: number;
134
+ private readonly totalTokenBudget: number;
135
+
136
+ // Persistence dirty flag
137
+ private dirty = false;
138
+
139
+ constructor(options: LayeredMemoryOptions) {
140
+ this.sessionId = options.sessionId;
141
+ this.storageDir = options.storageDir || path.join(os.homedir(), '.nova', 'memory', this.sessionId.slice(0, 8));
142
+ this.immediateBuffer = new CircularBuffer(options.l1MaxMessages || 10);
143
+ this.compressor = new ContextCompressor(this.sessionId);
144
+ this.l2MaxTokens = options.l2MaxTokens || 2000;
145
+ this.l3MaxEntries = options.l3MaxEntries || 20;
146
+ this.l4MaxRetrieveEntries = options.l4MaxRetrieveEntries || 5;
147
+ this.totalTokenBudget = options.totalTokenBudget || 20000;
148
+
149
+ this.ensureStorageDir();
150
+ }
151
+
152
+ // ========================================================================
153
+ // L1: Immediate Memory
154
+ // ========================================================================
155
+
156
+ /**
157
+ * Add a message to L1 immediate memory.
158
+ * This is the most recent conversation context.
159
+ */
160
+ addImmediateMessage(message: Message): void {
161
+ this.immediateBuffer.push(message);
162
+ this.dirty = true;
163
+ }
164
+
165
+ /**
166
+ * Get all L1 immediate messages.
167
+ */
168
+ getImmediateMessages(): Message[] {
169
+ return this.immediateBuffer.getAll();
170
+ }
171
+
172
+ /**
173
+ * Clear L1 immediate memory (e.g., on session reset).
174
+ */
175
+ clearImmediateMemory(): void {
176
+ this.immediateBuffer.clear();
177
+ this.dirty = true;
178
+ }
179
+
180
+ // ========================================================================
181
+ // L2: Working Memory (Session-level)
182
+ // ========================================================================
183
+
184
+ /**
185
+ * Promote messages from L1 to L2 working memory via compression.
186
+ * Called when L1 is full and messages need to be "graduated" to summary.
187
+ */
188
+ async promoteToWorkingMemory(): Promise<void> {
189
+ const messages = this.immediateBuffer.getAll();
190
+ if (messages.length < 2) return;
191
+
192
+ // Use the compressor to extract structured info
193
+ const newSummary = this.compressor.incrementalCompress(messages, this.workingSummary);
194
+
195
+ // Only update if compressor returned a valid summary
196
+ if (newSummary) {
197
+ this.workingSummary = newSummary;
198
+
199
+ // Ensure summary doesn't exceed token budget
200
+ if (this.workingSummary.tokenCount > this.l2MaxTokens) {
201
+ this.workingSummary = this.trimSummary(this.workingSummary, this.l2MaxTokens);
202
+ }
203
+ }
204
+
205
+ // Clear immediate memory after promotion
206
+ this.immediateBuffer.clear();
207
+ this.dirty = true;
208
+ }
209
+
210
+ /**
211
+ * Get the L2 working memory summary.
212
+ */
213
+ getWorkingSummary(): StructuredSummary | null {
214
+ return this.workingSummary;
215
+ }
216
+
217
+ /**
218
+ * Set the working summary directly (e.g., restored from persistence).
219
+ */
220
+ setWorkingSummary(summary: StructuredSummary): void {
221
+ this.workingSummary = summary;
222
+ this.dirty = true;
223
+ }
224
+
225
+ // ========================================================================
226
+ // L3: Long-term Memory
227
+ // ========================================================================
228
+
229
+ /**
230
+ * Store an entry in L3 long-term memory.
231
+ */
232
+ async storeLongTerm(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt' | 'accessCount' | 'lastAccessedAt' | 'tokenCount'>): Promise<MemoryEntry> {
233
+ const fullEntry: MemoryEntry = {
234
+ ...entry,
235
+ id: `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
236
+ createdAt: Date.now(),
237
+ updatedAt: Date.now(),
238
+ accessCount: 0,
239
+ lastAccessedAt: Date.now(),
240
+ tokenCount: this.estimateTokens(entry.content),
241
+ };
242
+
243
+ this.longTermMemory.set(fullEntry.id, fullEntry);
244
+ this.dirty = true;
245
+
246
+ return fullEntry;
247
+ }
248
+
249
+ /**
250
+ * Retrieve relevant entries from L3 long-term memory based on a query.
251
+ * Uses simple keyword matching (can be upgraded to embeddings later).
252
+ */
253
+ retrieveLongTerm(query: MemoryQuery): MemoryEntry[] {
254
+ const queryLower = query.query.toLowerCase();
255
+ const queryTokens = queryLower.split(/\s+/);
256
+
257
+ let entries = Array.from(this.longTermMemory.values());
258
+
259
+ // Filter by layers and categories if specified
260
+ if (query.layers) {
261
+ entries = entries.filter((e) => query.layers!.includes(e.layer));
262
+ }
263
+ if (query.categories) {
264
+ entries = entries.filter((e) => query.categories!.includes(e.category));
265
+ }
266
+
267
+ // Filter expired entries
268
+ const now = Date.now();
269
+ entries = entries.filter((e) => {
270
+ if (e.ttl && now - e.createdAt > e.ttl) return false;
271
+ return true;
272
+ });
273
+
274
+ // Score and rank by relevance
275
+ const scored = entries.map((entry) => {
276
+ const contentStr = typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content);
277
+ const contentLower = contentStr.toLowerCase();
278
+ const tagsLower = entry.tags.join(' ').toLowerCase();
279
+
280
+ // Simple TF scoring: count matching tokens
281
+ let score = 0;
282
+ for (const token of queryTokens) {
283
+ if (token.length < 2) continue;
284
+ const regex = new RegExp(token, 'gi');
285
+ const contentMatches = contentLower.match(regex);
286
+ const tagMatches = tagsLower.match(regex);
287
+ score += (contentMatches?.length || 0) * 2; // Content matches count more
288
+ score += (tagMatches?.length || 0) * 3; // Tag matches count even more
289
+ }
290
+
291
+ // Boost recently accessed entries slightly
292
+ const recencyBoost = 1 + (entry.accessCount * 0.05);
293
+ score *= recencyBoost;
294
+
295
+ entry.relevanceScore = Math.min(score / 10, 1);
296
+ return { entry, score };
297
+ });
298
+
299
+ // Sort by relevance descending
300
+ scored.sort((a, b) => b.score - a.score);
301
+
302
+ // Apply min score filter
303
+ const filtered = scored.filter(
304
+ (s) => !query.minScore || s.entry.relevanceScore! >= query.minScore
305
+ );
306
+
307
+ // Apply token budget
308
+ let totalTokens = 0;
309
+ const maxTokens = query.maxTokens || this.totalTokenBudget * 0.4;
310
+ const maxEntries = query.maxEntries || this.l3MaxEntries;
311
+ const result: MemoryEntry[] = [];
312
+
313
+ for (const { entry } of filtered) {
314
+ if (result.length >= maxEntries) break;
315
+ if (totalTokens + entry.tokenCount > maxTokens) break;
316
+
317
+ // Update access metadata
318
+ entry.accessCount++;
319
+ entry.lastAccessedAt = Date.now();
320
+ result.push(entry);
321
+ totalTokens += entry.tokenCount;
322
+ }
323
+
324
+ return result;
325
+ }
326
+
327
+ /**
328
+ * Get all L3 entries (for management purposes).
329
+ */
330
+ getAllLongTermEntries(): MemoryEntry[] {
331
+ return Array.from(this.longTermMemory.values());
332
+ }
333
+
334
+ /**
335
+ * Delete an entry from L3.
336
+ */
337
+ deleteLongTerm(id: string): boolean {
338
+ const deleted = this.longTermMemory.delete(id);
339
+ if (deleted) this.dirty = true;
340
+ return deleted;
341
+ }
342
+
343
+ // ========================================================================
344
+ // L4: Archival Memory
345
+ // ========================================================================
346
+
347
+ /**
348
+ * Archive an entry to L4 (persist to disk).
349
+ */
350
+ async archiveEntry(entry: MemoryEntry): Promise<void> {
351
+ this.archivalIndex.set(entry.id, entry);
352
+ await this.persistArchivalEntry(entry);
353
+ this.dirty = true;
354
+ }
355
+
356
+ /**
357
+ * Retrieve relevant entries from L4 archival.
358
+ * Currently uses same keyword matching as L3, but from disk-backed index.
359
+ */
360
+ async retrieveArchival(query: MemoryQuery): Promise<MemoryEntry[]> {
361
+ const queryLower = query.query.toLowerCase();
362
+ const queryTokens = queryLower.split(/\s+/);
363
+
364
+ let entries = Array.from(this.archivalIndex.values());
365
+
366
+ // Filter expired
367
+ const now = Date.now();
368
+ entries = entries.filter((e) => !e.ttl || now - e.createdAt <= e.ttl);
369
+
370
+ // Score relevance
371
+ const scored = entries.map((entry) => {
372
+ const contentStr = typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content);
373
+ const contentLower = contentStr.toLowerCase();
374
+ let score = 0;
375
+ for (const token of queryTokens) {
376
+ if (token.length < 2) continue;
377
+ const regex = new RegExp(token, 'gi');
378
+ score += (contentLower.match(regex)?.length || 0);
379
+ }
380
+ entry.relevanceScore = Math.min(score / 5, 1);
381
+ return { entry, score };
382
+ });
383
+
384
+ scored.sort((a, b) => b.score - a.score);
385
+
386
+ const minScore = query.minScore || 0.3;
387
+ const maxEntries = query.maxEntries || this.l4MaxRetrieveEntries;
388
+ const result: MemoryEntry[] = [];
389
+
390
+ for (const { entry } of scored) {
391
+ if (result.length >= maxEntries) break;
392
+ if (entry.relevanceScore! >= minScore) {
393
+ result.push(entry);
394
+ }
395
+ }
396
+
397
+ return result;
398
+ }
399
+
400
+ // ========================================================================
401
+ // Unified Memory Loading
402
+ // ========================================================================
403
+
404
+ /**
405
+ * Load all relevant memory into a MemoryContext for injection into the LLM.
406
+ * This is the main entry point for the AgentLoop to get context.
407
+ */
408
+ async loadMemory(query?: string): Promise<MemoryContext> {
409
+ let totalTokens = 0;
410
+
411
+ // L1: Immediate messages
412
+ const immediate = this.immediateBuffer.getAll();
413
+ totalTokens += this.estimateMessagesTokens(immediate);
414
+
415
+ // L2: Working summary
416
+ const workingSummary = this.workingSummary;
417
+ if (workingSummary) {
418
+ totalTokens += workingSummary.tokenCount;
419
+ }
420
+
421
+ // L3: Long-term memory (keyword search)
422
+ let longTermEntries: MemoryEntry[] = [];
423
+ if (query) {
424
+ longTermEntries = this.retrieveLongTerm({
425
+ query,
426
+ maxEntries: this.l3MaxEntries,
427
+ maxTokens: this.totalTokenBudget * 0.3,
428
+ });
429
+ }
430
+ totalTokens += longTermEntries.reduce((sum, e) => sum + e.tokenCount, 0);
431
+
432
+ // L4: Archival memory (only if we have budget remaining)
433
+ let archivalEntries: MemoryEntry[] = [];
434
+ if (query && totalTokens < this.totalTokenBudget * 0.8) {
435
+ archivalEntries = await this.retrieveArchival({
436
+ query,
437
+ maxEntries: this.l4MaxRetrieveEntries,
438
+ maxTokens: this.totalTokenBudget - totalTokens,
439
+ minScore: 0.5, // Higher threshold for archival
440
+ });
441
+ totalTokens += archivalEntries.reduce((sum, e) => sum + e.tokenCount, 0);
442
+ }
443
+
444
+ return {
445
+ immediate,
446
+ workingSummary,
447
+ longTermEntries,
448
+ archivalEntries,
449
+ totalTokens,
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Build a system prompt section from loaded memory context.
455
+ */
456
+ buildMemoryPrompt(context: MemoryContext): string {
457
+ const parts: string[] = [];
458
+
459
+ if (context.workingSummary) {
460
+ parts.push(this.compressor.renderSummaryToText(context.workingSummary));
461
+ }
462
+
463
+ if (context.longTermEntries.length > 0) {
464
+ parts.push('<long-term-memory>');
465
+ for (const entry of context.longTermEntries) {
466
+ const tagStr = entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : '';
467
+ parts.push(`- [${entry.category}]${tagStr} ${entry.content}`);
468
+ }
469
+ parts.push('</long-term-memory>');
470
+ }
471
+
472
+ if (context.archivalEntries.length > 0) {
473
+ parts.push('<archival-context>');
474
+ for (const entry of context.archivalEntries) {
475
+ parts.push(`- ${entry.content}`);
476
+ }
477
+ parts.push('</archival-context>');
478
+ }
479
+
480
+ return parts.join('\n\n');
481
+ }
482
+
483
+ // ========================================================================
484
+ // Persistence
485
+ // ========================================================================
486
+
487
+ /**
488
+ * Persist all memory layers to disk.
489
+ */
490
+ async persist(): Promise<void> {
491
+ if (!this.dirty) return;
492
+
493
+ try {
494
+ await fs.mkdir(this.storageDir, { recursive: true });
495
+
496
+ // Persist L2 working summary
497
+ if (this.workingSummary) {
498
+ const summaryPath = path.join(this.storageDir, 'working-summary.json');
499
+ await fs.writeFile(summaryPath, JSON.stringify(this.workingSummary, null, 2), 'utf-8');
500
+ }
501
+
502
+ // Persist L3 long-term memory
503
+ if (this.longTermMemory.size > 0) {
504
+ const ltPath = path.join(this.storageDir, 'long-term.json');
505
+ const entries = Array.from(this.longTermMemory.values());
506
+ await fs.writeFile(ltPath, JSON.stringify(entries, null, 2), 'utf-8');
507
+ }
508
+
509
+ // Persist L4 archival index
510
+ if (this.archivalIndex.size > 0) {
511
+ const archPath = path.join(this.storageDir, 'archival-index.json');
512
+ const entries = Array.from(this.archivalIndex.values());
513
+ await fs.writeFile(archPath, JSON.stringify(entries, null, 2), 'utf-8');
514
+ }
515
+
516
+ this.dirty = false;
517
+ } catch (err) {
518
+ // Memory persistence is best-effort, never block
519
+ console.warn(`Memory persistence warning: ${(err as Error).message}`);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Restore memory from disk.
525
+ */
526
+ async restore(): Promise<void> {
527
+ try {
528
+ // Restore L2 working summary
529
+ const summaryPath = path.join(this.storageDir, 'working-summary.json');
530
+ try {
531
+ const data = await fs.readFile(summaryPath, 'utf-8');
532
+ this.workingSummary = JSON.parse(data);
533
+ } catch {
534
+ // No summary file, that's OK
535
+ }
536
+
537
+ // Restore L3 long-term memory
538
+ const ltPath = path.join(this.storageDir, 'long-term.json');
539
+ try {
540
+ const data = await fs.readFile(ltPath, 'utf-8');
541
+ const entries: MemoryEntry[] = JSON.parse(data);
542
+ for (const entry of entries) {
543
+ this.longTermMemory.set(entry.id, entry);
544
+ }
545
+ } catch {
546
+ // No long-term memory, that's OK
547
+ }
548
+
549
+ // Restore L4 archival index
550
+ const archPath = path.join(this.storageDir, 'archival-index.json');
551
+ try {
552
+ const data = await fs.readFile(archPath, 'utf-8');
553
+ const entries: MemoryEntry[] = JSON.parse(data);
554
+ for (const entry of entries) {
555
+ this.archivalIndex.set(entry.id, entry);
556
+ }
557
+ } catch {
558
+ // No archival memory, that's OK
559
+ }
560
+ } catch (err) {
561
+ console.warn(`Memory restore warning: ${(err as Error).message}`);
562
+ }
563
+ }
564
+
565
+ // ========================================================================
566
+ // Helpers
567
+ // ========================================================================
568
+
569
+ private async ensureStorageDir(): Promise<void> {
570
+ try {
571
+ await fs.mkdir(this.storageDir, { recursive: true });
572
+ } catch {
573
+ // Ignore
574
+ }
575
+ }
576
+
577
+ private async persistArchivalEntry(entry: MemoryEntry): Promise<void> {
578
+ try {
579
+ const entryPath = path.join(this.storageDir, 'archival', `${entry.id}.json`);
580
+ await fs.mkdir(path.dirname(entryPath), { recursive: true });
581
+ await fs.writeFile(entryPath, JSON.stringify(entry, null, 2), 'utf-8');
582
+ } catch {
583
+ // Best-effort
584
+ }
585
+ }
586
+
587
+ private trimSummary(summary: StructuredSummary, maxTokens: number): StructuredSummary {
588
+ // Trim each section to fit within budget
589
+ const entries = [
590
+ ...summary.fileModifications.slice(0, 10),
591
+ ...summary.decisions.slice(0, 5),
592
+ ...summary.nextSteps.slice(0, 5),
593
+ ];
594
+
595
+ let text = `Session goal: ${summary.sessionIntent}\n`;
596
+ text += `Completed: ${summary.completedActions.slice(0, 10).join('; ')}\n`;
597
+ text += `Files: ${summary.fileModifications.slice(0, 10).map((f) => `${f.action} ${f.path}`).join('; ')}`;
598
+
599
+ if (this.estimateTokens(text) > maxTokens) {
600
+ // Just truncate the intent and keep only the most essential info
601
+ text = `Session goal: ${summary.sessionIntent.slice(0, 100)}\n`;
602
+ text += `Files changed: ${summary.fileModifications.length}, Decisions: ${summary.decisions.length}`;
603
+ }
604
+
605
+ return {
606
+ ...summary,
607
+ fileModifications: summary.fileModifications.slice(0, 10),
608
+ decisions: summary.decisions.slice(0, 5),
609
+ nextSteps: summary.nextSteps.slice(0, 5),
610
+ completedActions: summary.completedActions.slice(0, 10),
611
+ tokenCount: this.estimateTokens(text),
612
+ lastUpdated: Date.now(),
613
+ };
614
+ }
615
+
616
+ private estimateTokens(text: string): number {
617
+ return Math.ceil(text.length / 4);
618
+ }
619
+
620
+ private estimateMessagesTokens(messages: Message[]): number {
621
+ let total = 0;
622
+ for (const msg of messages) {
623
+ for (const block of msg.content) {
624
+ if (block.type === 'text') {
625
+ total += this.estimateTokens(block.text);
626
+ } else if (block.type === 'tool_use') {
627
+ total += this.estimateTokens(JSON.stringify(block.input));
628
+ } else if (block.type === 'tool_result') {
629
+ const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
630
+ total += this.estimateTokens(content);
631
+ }
632
+ }
633
+ }
634
+ return total;
635
+ }
636
+
637
+ // ========================================================================
638
+ // Memory Statistics
639
+ // ========================================================================
640
+
641
+ getStats(): {
642
+ l1Count: number;
643
+ l2Tokens: number;
644
+ l3Count: number;
645
+ l4Count: number;
646
+ totalTokens: number;
647
+ } {
648
+ return {
649
+ l1Count: this.immediateBuffer.size,
650
+ l2Tokens: this.workingSummary?.tokenCount || 0,
651
+ l3Count: this.longTermMemory.size,
652
+ l4Count: this.archivalIndex.size,
653
+ totalTokens: this.workingSummary?.tokenCount || 0 +
654
+ Array.from(this.longTermMemory.values()).reduce((s, e) => s + e.tokenCount, 0),
655
+ };
656
+ }
657
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MemoryDiscovery.d.ts","sourceRoot":"","sources":["MemoryDiscovery.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC;CACtD;AAED,MAAM,WAAW,qBAAqB;IACpC,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAeD,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,KAAK,CAAiC;gBAElC,MAAM,EAAE,qBAAqB;IAKzC,+CAA+C;IACzC,QAAQ,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IA2DvC,iCAAiC;IAC3B,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAmB3D,sBAAsB;IACtB,UAAU,IAAI,IAAI;IAIlB,sDAAsD;IACtD,OAAO,CAAC,iBAAiB;IAuBzB,+BAA+B;IAC/B,OAAO,CAAC,UAAU;CAenB"}