opencode-fractal-memory 0.2.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +493 -0
  3. package/agent/memory-hints.md +98 -0
  4. package/agent/memory-researcher.md +56 -0
  5. package/commands/memory-auto-test.md +10 -0
  6. package/commands/memory-cache-status.md +13 -0
  7. package/commands/memory-check-context.md +4 -0
  8. package/commands/memory-compress.md +13 -0
  9. package/commands/memory-dashboard.md +23 -0
  10. package/commands/memory-delete.md +24 -0
  11. package/commands/memory-detect-topics.md +28 -0
  12. package/commands/memory-distill.md +35 -0
  13. package/commands/memory-drilldown-query.md +28 -0
  14. package/commands/memory-drilldown.md +11 -0
  15. package/commands/memory-extract-patterns.md +4 -0
  16. package/commands/memory-generate-embeddings.md +26 -0
  17. package/commands/memory-get.md +26 -0
  18. package/commands/memory-help.md +55 -0
  19. package/commands/memory-injection-feedback.md +26 -0
  20. package/commands/memory-injection-stats.md +11 -0
  21. package/commands/memory-list.md +4 -0
  22. package/commands/memory-llm-compress.md +34 -0
  23. package/commands/memory-mcp.md +20 -0
  24. package/commands/memory-prune.md +4 -0
  25. package/commands/memory-rate.md +48 -0
  26. package/commands/memory-reflect.md +37 -0
  27. package/commands/memory-replace.md +26 -0
  28. package/commands/memory-retrieve.md +34 -0
  29. package/commands/memory-search.md +28 -0
  30. package/commands/memory-session-stats.md +4 -0
  31. package/commands/memory-set.md +31 -0
  32. package/commands/memory-stats.md +11 -0
  33. package/commands/memory-summarize.md +29 -0
  34. package/commands/memory-tool-stats.md +4 -0
  35. package/commands/memory-total-tokens.md +10 -0
  36. package/commands/memory-verify.md +4 -0
  37. package/commands/memory-version.md +9 -0
  38. package/dist/cache.js +39 -0
  39. package/dist/config.js +120 -0
  40. package/dist/embeddings.js +125 -0
  41. package/dist/ensure-models.js +70 -0
  42. package/dist/file-summary.js +143 -0
  43. package/dist/frontmatter.js +28 -0
  44. package/dist/hnsw-index.js +138 -0
  45. package/dist/hooks/auto-discover.js +4 -0
  46. package/dist/hooks/auto-distill.js +120 -0
  47. package/dist/hooks/auto-retrieve/content.js +47 -0
  48. package/dist/hooks/auto-retrieve/detection.js +50 -0
  49. package/dist/hooks/auto-retrieve/formatting.js +19 -0
  50. package/dist/hooks/auto-retrieve/index.js +163 -0
  51. package/dist/hooks/auto-retrieve/scoring.js +56 -0
  52. package/dist/hooks/auto-retrieve.js +1 -0
  53. package/dist/hooks/index.js +4 -0
  54. package/dist/hooks/predictive-rating.js +87 -0
  55. package/dist/journal.js +279 -0
  56. package/dist/logging.js +147 -0
  57. package/dist/management/helpers.js +227 -0
  58. package/dist/management/router.js +48 -0
  59. package/dist/management/routes.js +197 -0
  60. package/dist/management-server.js +4 -0
  61. package/dist/management-standalone.js +31 -0
  62. package/dist/mcp/logging.js +57 -0
  63. package/dist/mcp/server.js +251 -0
  64. package/dist/mcp/transform.js +48 -0
  65. package/dist/mcp-server.js +18 -0
  66. package/dist/memory.js +2 -0
  67. package/dist/ollama.js +74 -0
  68. package/dist/plugin/hooks.js +168 -0
  69. package/dist/plugin/index.js +28 -0
  70. package/dist/plugin/init.js +109 -0
  71. package/dist/plugin/state.js +75 -0
  72. package/dist/plugin/tools.js +45 -0
  73. package/dist/plugin.js +2 -0
  74. package/dist/procedural/store.js +1 -0
  75. package/dist/procedural/types.js +1 -0
  76. package/dist/seed-nodes.js +804 -0
  77. package/dist/storage/compress-ops.js +129 -0
  78. package/dist/storage/compression/formatters.js +243 -0
  79. package/dist/storage/compression/index.js +107 -0
  80. package/dist/storage/compression/patterns.js +138 -0
  81. package/dist/storage/expiration.js +66 -0
  82. package/dist/storage/index.js +1 -0
  83. package/dist/storage/injection-events.js +82 -0
  84. package/dist/storage/lifecycle.js +65 -0
  85. package/dist/storage/maintenance.js +60 -0
  86. package/dist/storage/migrations/definitions.js +374 -0
  87. package/dist/storage/migrations/index.js +21 -0
  88. package/dist/storage/navigation.js +98 -0
  89. package/dist/storage/queries/base.js +44 -0
  90. package/dist/storage/queries/links.js +32 -0
  91. package/dist/storage/queries/nodes.js +189 -0
  92. package/dist/storage/queries/search-helpers.js +239 -0
  93. package/dist/storage/scoring.js +36 -0
  94. package/dist/storage/search.js +233 -0
  95. package/dist/storage/session-tracking.js +180 -0
  96. package/dist/storage/sqlite.js +329 -0
  97. package/dist/storage/tool-usage.js +56 -0
  98. package/dist/storage/types.js +1 -0
  99. package/dist/storage/utils.js +94 -0
  100. package/dist/tools/auto-test.js +24 -0
  101. package/dist/tools/cache-status.js +36 -0
  102. package/dist/tools/compress.js +186 -0
  103. package/dist/tools/core.js +307 -0
  104. package/dist/tools/dashboard.js +97 -0
  105. package/dist/tools/help.js +59 -0
  106. package/dist/tools/index.js +12 -0
  107. package/dist/tools/inject.js +91 -0
  108. package/dist/tools/injection-debug.js +48 -0
  109. package/dist/tools/journal.js +105 -0
  110. package/dist/tools/llm-compress.js +41 -0
  111. package/dist/tools/middle-term.js +68 -0
  112. package/dist/tools/playbook.js +64 -0
  113. package/dist/tools/reflect.js +291 -0
  114. package/dist/tools/search.js +188 -0
  115. package/dist/tools/session.js +189 -0
  116. package/dist/tools/shared.js +74 -0
  117. package/dist/tools/skill.js +37 -0
  118. package/dist/tools/stats.js +256 -0
  119. package/dist/tools/version.js +13 -0
  120. package/dist/tools.js +18 -0
  121. package/dist/utils/hybridScore.js +67 -0
  122. package/management/public/app.js +1529 -0
  123. package/management/public/index.html +486 -0
  124. package/management/public/three.min.js +6 -0
  125. package/package.json +65 -0
  126. package/scripts/download-models.ts +16 -0
  127. package/scripts/postinstall.cjs +30 -0
@@ -0,0 +1,87 @@
1
+ import { memLog } from "../logging";
2
+ export async function predictiveRateToolCall(store, input, output, config) {
3
+ const success = output.metadata?.error ? false : true;
4
+ let nodeId;
5
+ const tool = input.tool;
6
+ const args = input.args ?? {};
7
+ if (tool === "memory_get" && (args.id || args.label)) {
8
+ nodeId = args.id;
9
+ if (!nodeId && args.label) {
10
+ try {
11
+ const node = await store.getNodeByLabel(args.scope ?? "project", args.label);
12
+ nodeId = node.id;
13
+ }
14
+ catch { /* Not found, skip rating */
15
+ return;
16
+ }
17
+ }
18
+ }
19
+ else if (tool === "memory_fetch" && args.label) {
20
+ try {
21
+ const node = await store.getNodeByLabel("global", args.label);
22
+ nodeId = node.id;
23
+ }
24
+ catch {
25
+ try {
26
+ const node = await store.getNodeByLabel("project", args.label);
27
+ nodeId = node.id;
28
+ }
29
+ catch { /* Not found in project, skip */
30
+ return;
31
+ }
32
+ }
33
+ }
34
+ else if (tool === "memory_rate" && (args.id || args.label)) {
35
+ nodeId = args.id;
36
+ if (!nodeId && args.label) {
37
+ try {
38
+ const node = await store.getNodeByLabel(args.scope ?? "project", args.label);
39
+ nodeId = node.id;
40
+ }
41
+ catch { /* Not found, skip rating */
42
+ return;
43
+ }
44
+ }
45
+ }
46
+ if (!nodeId)
47
+ return;
48
+ try {
49
+ const node = await store.getNode(nodeId);
50
+ if (!node)
51
+ return;
52
+ const currentScore = node.usefulnessScore ?? 0;
53
+ const currentHelpful = node.timesHelpful ?? 0;
54
+ if (tool === "memory_rate") {
55
+ if (args.helpful === true) {
56
+ const newScore = Math.min(5, currentScore + config.positiveBoost);
57
+ await store.updateNode(nodeId, {
58
+ usefulnessScore: newScore,
59
+ timesHelpful: currentHelpful + 1,
60
+ });
61
+ memLog("debug", "predictive-rating", `Boosted ${nodeId.slice(0, 8)} to ${newScore}`);
62
+ }
63
+ else if (success && args.helpful === false) {
64
+ const newScore = Math.max(0, currentScore - config.negativePenalty);
65
+ await store.updateNode(nodeId, { usefulnessScore: newScore });
66
+ memLog("debug", "predictive-rating", `Penalized ${nodeId.slice(0, 8)} to ${newScore}`);
67
+ }
68
+ }
69
+ else if (success) {
70
+ const newScore = Math.min(5, currentScore + config.positiveBoost);
71
+ await store.updateNode(nodeId, { usefulnessScore: newScore });
72
+ memLog("debug", "predictive-rating", `Access boost ${nodeId.slice(0, 8)} to ${newScore}`);
73
+ }
74
+ else {
75
+ const newScore = Math.max(0, currentScore - config.negativePenalty);
76
+ await store.updateNode(nodeId, { usefulnessScore: newScore });
77
+ memLog("debug", "predictive-rating", `Failure penalty ${nodeId.slice(0, 8)} to ${newScore}`);
78
+ }
79
+ }
80
+ catch (err) {
81
+ memLog("warn", "predictive-rating", "Rating update failed", { error: String(err) });
82
+ }
83
+ }
84
+ export async function applyScoreDecay(store, config) {
85
+ const decayed = await store.runScoreDecay(config.decayDays);
86
+ return `Predictive rating: decayed ${decayed} nodes`;
87
+ }
@@ -0,0 +1,279 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import yaml from "js-yaml";
5
+ import { z } from "zod";
6
+ import { cosineSimilarity, generateEmbedding } from "./embeddings";
7
+ import { atomicWriteFile, buildFrontmatterDocument, splitFrontmatter } from "./frontmatter";
8
+ const TagSchema = z.looseObject({
9
+ name: z.string().min(1),
10
+ description: z.string().min(1),
11
+ });
12
+ const ConfigSchema = z.looseObject({
13
+ journal: z
14
+ .looseObject({
15
+ enabled: z.boolean().optional(),
16
+ tags: z.array(TagSchema).optional(),
17
+ })
18
+ .optional(),
19
+ management: z
20
+ .looseObject({
21
+ enabled: z.boolean().optional(),
22
+ port: z.number().optional(),
23
+ })
24
+ .optional(),
25
+ });
26
+ export async function loadConfig(configDir) {
27
+ const dir = configDir ?? path.join(os.homedir(), ".config", "opencode");
28
+ const configPath = path.join(dir, "agent-memory.json");
29
+ try {
30
+ const raw = await fs.readFile(configPath, "utf-8");
31
+ const parsed = ConfigSchema.safeParse(JSON.parse(raw));
32
+ if (!parsed.success)
33
+ return {};
34
+ return parsed.data;
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ const EntryFrontmatterSchema = z.looseObject({
41
+ title: z.string().min(1),
42
+ project: z.string().optional(),
43
+ model: z.string().optional(),
44
+ provider: z.string().optional(),
45
+ agent: z.string().optional(),
46
+ session_id: z.string().optional(),
47
+ created: z.string().optional(),
48
+ tags: z.array(z.string().min(1)).optional(),
49
+ });
50
+ function entryFilename(date) {
51
+ const pad = (n, len = 2) => String(n).padStart(len, "0");
52
+ return [
53
+ `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}`,
54
+ "-",
55
+ `${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`,
56
+ "-",
57
+ `${pad(date.getUTCMilliseconds(), 3)}`,
58
+ ".md",
59
+ ].join("");
60
+ }
61
+ function embeddingPath(entryPath) {
62
+ return entryPath.replace(/\.md$/, ".embedding");
63
+ }
64
+ async function readEntryFile(filePath) {
65
+ const raw = await fs.readFile(filePath, "utf-8");
66
+ const { frontmatterText, body } = splitFrontmatter(raw);
67
+ if (!frontmatterText) {
68
+ throw new Error(`Journal entry missing frontmatter: ${filePath}`);
69
+ }
70
+ const loaded = yaml.load(frontmatterText);
71
+ const parsed = EntryFrontmatterSchema.safeParse(loaded);
72
+ if (!parsed.success) {
73
+ throw new Error(`Invalid journal frontmatter in ${filePath}: ${parsed.error.message}`);
74
+ }
75
+ const fm = parsed.data;
76
+ const id = path.basename(filePath, ".md");
77
+ return {
78
+ id,
79
+ title: fm.title,
80
+ project: fm.project ?? "",
81
+ model: fm.model ?? "",
82
+ provider: fm.provider ?? "",
83
+ agent: fm.agent ?? "",
84
+ sessionId: fm.session_id ?? "",
85
+ created: fm.created ? new Date(fm.created) : new Date(),
86
+ tags: fm.tags ?? [],
87
+ body: body.trim(),
88
+ filePath,
89
+ };
90
+ }
91
+ async function loadEmbedding(entryPath) {
92
+ const ePath = embeddingPath(entryPath);
93
+ try {
94
+ const raw = await fs.readFile(ePath, "utf-8");
95
+ return JSON.parse(raw);
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
102
+ function validateId(id) {
103
+ const trimmed = id.trim();
104
+ if (!SAFE_ID.test(trimmed)) {
105
+ throw new Error(`Invalid journal entry ID: "${id}"`);
106
+ }
107
+ return trimmed;
108
+ }
109
+ export function createJournalStore(configDir) {
110
+ const journalDir = path.join(configDir ?? path.join(os.homedir(), ".config", "opencode"), "journal");
111
+ return {
112
+ async write(entry) {
113
+ await fs.mkdir(journalDir, { recursive: true });
114
+ const created = new Date();
115
+ const filename = entryFilename(created);
116
+ const filePath = path.join(journalDir, filename);
117
+ const frontmatter = {
118
+ title: entry.title,
119
+ created: created.toISOString(),
120
+ };
121
+ if (entry.project)
122
+ frontmatter.project = entry.project;
123
+ if (entry.model)
124
+ frontmatter.model = entry.model;
125
+ if (entry.provider)
126
+ frontmatter.provider = entry.provider;
127
+ if (entry.agent)
128
+ frontmatter.agent = entry.agent;
129
+ if (entry.sessionId)
130
+ frontmatter.session_id = entry.sessionId;
131
+ if (entry.tags && entry.tags.length > 0)
132
+ frontmatter.tags = entry.tags;
133
+ const content = buildFrontmatterDocument(frontmatter, entry.body);
134
+ await atomicWriteFile(filePath, content);
135
+ // Generate and save embedding for semantic search
136
+ const searchableText = `${entry.title}\n${entry.body}`;
137
+ try {
138
+ const embedding = await generateEmbedding(searchableText);
139
+ await fs.writeFile(embeddingPath(filePath), JSON.stringify(embedding), "utf-8");
140
+ }
141
+ catch {
142
+ // Embedding generation can fail (e.g. model download issue).
143
+ // The entry is still saved; text search remains available.
144
+ }
145
+ return {
146
+ id: path.basename(filePath, ".md"),
147
+ title: entry.title,
148
+ project: entry.project ?? "",
149
+ model: entry.model ?? "",
150
+ provider: entry.provider ?? "",
151
+ agent: entry.agent ?? "",
152
+ sessionId: entry.sessionId ?? "",
153
+ created,
154
+ tags: entry.tags ?? [],
155
+ body: entry.body,
156
+ filePath,
157
+ };
158
+ },
159
+ async read(id) {
160
+ const safeId = validateId(id);
161
+ const filePath = path.join(journalDir, `${safeId}.md`);
162
+ try {
163
+ await fs.access(filePath);
164
+ }
165
+ catch {
166
+ throw new Error(`Journal entry not found: ${safeId}`);
167
+ }
168
+ return readEntryFile(filePath);
169
+ },
170
+ async search(query) {
171
+ const limit = Math.min(Math.max(query.limit ?? 20, 1), 50);
172
+ const offset = Math.max(query.offset ?? 0, 0);
173
+ let entries = [];
174
+ // Read all entry files
175
+ let files;
176
+ try {
177
+ const dirEntries = await fs.readdir(journalDir, {
178
+ withFileTypes: true,
179
+ });
180
+ files = dirEntries
181
+ .filter((e) => e.isFile() && e.name.endsWith(".md"))
182
+ .map((e) => e.name)
183
+ .sort()
184
+ .reverse(); // Newest first
185
+ }
186
+ catch {
187
+ return { entries: [], total: 0, allTags: [] };
188
+ }
189
+ // If a text query is provided, try semantic search first
190
+ let queryEmbedding;
191
+ if (query.text) {
192
+ try {
193
+ queryEmbedding = await generateEmbedding(query.text);
194
+ }
195
+ catch {
196
+ // Fall back to text search if embedding fails
197
+ }
198
+ }
199
+ // Collect all tags across every entry (before filtering)
200
+ const tagSet = new Set();
201
+ for (const file of files) {
202
+ const filePath = path.join(journalDir, file);
203
+ let entry;
204
+ try {
205
+ entry = await readEntryFile(filePath);
206
+ }
207
+ catch {
208
+ continue;
209
+ }
210
+ // Collect tags before applying filters
211
+ for (const tag of entry.tags) {
212
+ tagSet.add(tag);
213
+ }
214
+ // Apply metadata filters (AND logic)
215
+ if (query.project && entry.project !== query.project) {
216
+ continue;
217
+ }
218
+ if (query.tags && query.tags.length > 0) {
219
+ const entryTagNames = entry.tags.map((t) => t.toLowerCase());
220
+ const allTagsMatch = query.tags.every((t) => entryTagNames.includes(t.toLowerCase()));
221
+ if (!allTagsMatch)
222
+ continue;
223
+ }
224
+ // Score the entry
225
+ let score = 0;
226
+ if (query.text) {
227
+ if (queryEmbedding) {
228
+ // Semantic search
229
+ const entryEmbedding = await loadEmbedding(filePath);
230
+ if (entryEmbedding) {
231
+ score = cosineSimilarity(queryEmbedding, entryEmbedding);
232
+ }
233
+ else {
234
+ // No embedding stored; fall back to text match
235
+ const haystack = `${entry.title}\n${entry.body}`.toLowerCase();
236
+ score = haystack.includes(query.text.toLowerCase()) ? 0.5 : 0;
237
+ }
238
+ }
239
+ else {
240
+ // Text search fallback
241
+ const haystack = `${entry.title}\n${entry.body}`.toLowerCase();
242
+ score = haystack.includes(query.text.toLowerCase()) ? 1 : 0;
243
+ }
244
+ if (score <= 0)
245
+ continue;
246
+ }
247
+ else {
248
+ // No text query - chronological order (score by recency)
249
+ score = entry.created.getTime();
250
+ }
251
+ entries.push({ entry, score });
252
+ }
253
+ const total = entries.length;
254
+ // Sort by score descending
255
+ entries.sort((a, b) => b.score - a.score);
256
+ // Apply offset + limit
257
+ entries = entries.slice(offset, offset + limit);
258
+ return {
259
+ entries: entries.map((e) => e.entry),
260
+ total,
261
+ allTags: Array.from(tagSet).sort(),
262
+ };
263
+ },
264
+ };
265
+ }
266
+ export function buildJournalSystemNote(tags) {
267
+ const tagSection = tags && tags.length > 0
268
+ ? `\n\nSuggested tags:\n${tags.map((t) => `- ${t.name}: ${t.description}`).join("\n")}`
269
+ : "";
270
+ return `<journal_instructions>
271
+ You have access to a private journal. Use it to record thoughts, discoveries, and decisions as you work.
272
+ Tags are free-form strings — use them to classify entries however makes sense.${tagSection}
273
+
274
+ Before starting complex tasks, search the journal for relevant past context.
275
+ Journal entries are append-only: you write new entries but never edit old ones.
276
+ Use journal_search to find past entries semantically, and journal_read to read a specific entry.
277
+ The journal is global across all projects but each entry records which project it was written from.
278
+ </journal_instructions>`;
279
+ }
@@ -0,0 +1,147 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ const LOG_FILE = path.join(os.homedir(), ".config", "opencode", "memory-plugin.log");
5
+ const CONTEXT_DUMP_FILE = path.join(os.homedir(), ".config", "opencode", "context_dump.log");
6
+ const MAX_LOG_SIZE = 5 * 1024 * 1024;
7
+ let currentSessionId = null;
8
+ let logLevel = "info";
9
+ // Categories to always skip (OpenCode core noise)
10
+ const SKIP_CATEGORIES = ["event"];
11
+ export function setSessionId(sessionId) {
12
+ currentSessionId = sessionId;
13
+ }
14
+ export function setLogLevel(level) {
15
+ logLevel = level;
16
+ }
17
+ function shouldLog(level, category) {
18
+ if (SKIP_CATEGORIES.includes(category || ""))
19
+ return false;
20
+ const levels = { debug: 0, info: 1, warn: 2, error: 3 };
21
+ return levels[level] >= levels[logLevel];
22
+ }
23
+ function writeLog(line) {
24
+ try {
25
+ try {
26
+ const stat = fs.statSync(LOG_FILE);
27
+ if (stat.size > MAX_LOG_SIZE) {
28
+ fs.renameSync(LOG_FILE, LOG_FILE + ".old");
29
+ }
30
+ }
31
+ catch { /* file doesn't exist yet */ }
32
+ fs.appendFileSync(LOG_FILE, line + "\n");
33
+ }
34
+ catch { /* silent fail */ }
35
+ }
36
+ export function memLog(level, category, msg, data) {
37
+ if (!shouldLog(level, category))
38
+ return;
39
+ const ts = new Date().toISOString().slice(0, 19).replace("T", " ");
40
+ const session = currentSessionId ? `[${currentSessionId.slice(0, 8)}]` : "";
41
+ const line = `[${ts}] [${level.padEnd(5)}] [${category}]${session} ${msg}` +
42
+ (Object.keys(data || {}).length > 0 ? ` ${JSON.stringify(data)}` : "");
43
+ writeLog(line);
44
+ }
45
+ export function memLogSimple(msg, data) {
46
+ memLog("info", "general", msg, data);
47
+ }
48
+ // Performance timing wrapper
49
+ const perfTimers = new Map();
50
+ export function perfStart(label) {
51
+ perfTimers.set(label, Date.now());
52
+ }
53
+ export function perfEnd(label, category = "perf") {
54
+ const start = perfTimers.get(label);
55
+ if (!start)
56
+ return null;
57
+ const duration = Date.now() - start;
58
+ memLog("debug", category, `${label} completed`, { durationMs: duration, label });
59
+ perfTimers.delete(label);
60
+ return duration;
61
+ }
62
+ export function perfNow() {
63
+ return Date.now();
64
+ }
65
+ export function memLogInjection(action, data) {
66
+ switch (action) {
67
+ case "start":
68
+ memLog("info", "injection", "Memory injection started", {
69
+ query: data.query?.slice(0, 100),
70
+ mode: data.mode,
71
+ nodeCount: data.nodes,
72
+ estimatedTokens: data.tokens,
73
+ });
74
+ break;
75
+ case "complete":
76
+ const durationStr = data.durationMs ? ` (${data.durationMs}ms)` : "";
77
+ memLog("info", "injection", `Injected ${data.selectedIds?.length || 0} nodes${durationStr}`, {
78
+ nodes: data.nodes,
79
+ tokens: data.tokens,
80
+ mode: data.mode,
81
+ durationMs: data.durationMs,
82
+ selectedCount: data.selectedIds?.length,
83
+ });
84
+ break;
85
+ case "skip":
86
+ memLog("info", "injection", `Skipped: ${data.reason}`, {
87
+ reason: data.reason,
88
+ query: data.query?.slice(0, 100),
89
+ });
90
+ break;
91
+ }
92
+ }
93
+ export function memLogToolCall(tool, data) {
94
+ memLog("debug", "tool", `Tool call: ${tool}`, {
95
+ success: data.success,
96
+ durationMs: data.durationMs,
97
+ error: data.error,
98
+ });
99
+ }
100
+ export function memLogSession(action, data) {
101
+ const level = action === "error" ? "warn" : "info";
102
+ memLog(level, "session", `Session ${action}`, {
103
+ sessionId: data.sessionId,
104
+ error: data.error,
105
+ });
106
+ }
107
+ export function memLogAutoCompress(action, data) {
108
+ switch (action) {
109
+ case "triggered":
110
+ memLog("info", "compress", "Auto-compress triggered", data);
111
+ break;
112
+ case "skipped":
113
+ memLog("debug", "compress", "Auto-compress skipped", data);
114
+ break;
115
+ case "complete":
116
+ memLog("info", "compress", "Auto-compress complete", data);
117
+ break;
118
+ case "failed":
119
+ memLog("error", "compress", "Auto-compress failed", { error: data.error });
120
+ break;
121
+ }
122
+ }
123
+ export function memLogAgentTool(tool, data) {
124
+ // Only log tools that matter (not debug/internal)
125
+ const importantTools = ["read", "edit", "write", "bash", "grep", "glob", "task", "memory_search", "memory_set", "memory_get", "memory_list"];
126
+ const isImportant = importantTools.includes(tool) || tool.startsWith("memory_");
127
+ if (isImportant) {
128
+ const durationStr = data.durationMs ? ` (${data.durationMs}ms)` : "";
129
+ const successStr = data.success === false ? " FAILED" : "";
130
+ const lengthStr = data.outputLength ? ` [${data.outputLength} chars]` : "";
131
+ memLog("info", "agent_tool", `${tool}${successStr}${lengthStr}${durationStr}`, {
132
+ sessionId: data.sessionId?.slice(0, 8),
133
+ success: data.success,
134
+ outputLength: data.outputLength,
135
+ durationMs: data.durationMs,
136
+ });
137
+ }
138
+ }
139
+ export function writeContextDump(systemBlocks) {
140
+ try {
141
+ const ts = new Date().toISOString();
142
+ const header = `\n\n========== CONTEXT DUMP: ${ts} ==========\n\n`;
143
+ const content = header + systemBlocks.map((s, i) => `--- BLOCK ${i} ---\n${s}\n`).join("\n");
144
+ fs.appendFileSync(CONTEXT_DUMP_FILE, content);
145
+ }
146
+ catch { /* silent fail */ }
147
+ }