@agentmemory/agentmemory 0.8.11 → 0.9.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.
- package/AGENTS.md +2 -2
- package/README.md +80 -9
- package/dist/cli.mjs +47 -5
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +866 -33
- package/dist/index.mjs.map +1 -1
- package/dist/{src-M6V9yZW5.mjs → src-B3pEsBSb.mjs} +838 -29
- package/dist/src-B3pEsBSb.mjs.map +1 -0
- package/dist/standalone-DXc-BEqr.mjs +457 -0
- package/dist/standalone-DXc-BEqr.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +230 -66
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-DmTkd0if.mjs → tools-registry-DXIK5CxQ.mjs} +31 -7
- package/dist/tools-registry-DXIK5CxQ.mjs.map +1 -0
- package/dist/viewer/index.html +264 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/dist/src-M6V9yZW5.mjs.map +0 -1
- package/dist/standalone-XB9gPYmo.mjs +0 -313
- package/dist/standalone-XB9gPYmo.mjs.map +0 -1
- package/dist/tools-registry-DmTkd0if.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { TriggerAction, registerWorker } from "iii-sdk";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import Anthropic from "@anthropic-ai/sdk";
|
|
7
7
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -10,7 +10,7 @@ import { execFile } from "node:child_process";
|
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { lookup } from "node:dns/promises";
|
|
12
12
|
import { isIP } from "node:net";
|
|
13
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { createServer } from "node:http";
|
|
16
16
|
|
|
@@ -51,16 +51,20 @@ function detectProvider(env) {
|
|
|
51
51
|
maxTokens,
|
|
52
52
|
baseURL: env["ANTHROPIC_BASE_URL"]
|
|
53
53
|
};
|
|
54
|
-
if (env["GEMINI_API_KEY"])
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (env["GEMINI_API_KEY"] || env["GOOGLE_API_KEY"]) {
|
|
55
|
+
if (!env["GEMINI_API_KEY"] && env["GOOGLE_API_KEY"]) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
|
|
56
|
+
return {
|
|
57
|
+
provider: "gemini",
|
|
58
|
+
model: env["GEMINI_MODEL"] || "gemini-2.0-flash",
|
|
59
|
+
maxTokens
|
|
60
|
+
};
|
|
61
|
+
}
|
|
59
62
|
if (env["OPENROUTER_API_KEY"]) return {
|
|
60
63
|
provider: "openrouter",
|
|
61
64
|
model: env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514",
|
|
62
65
|
maxTokens
|
|
63
66
|
};
|
|
67
|
+
if (env["AGENTMEMORY_AUTO_COMPRESS"] === "true") process.stderr.write("[agentmemory] WARNING: AGENTMEMORY_AUTO_COMPRESS=true but no LLM provider key found (GEMINI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY). Falling back to agent-sdk which shares Claude Code's API quota — this can exhaust a Pro subscription during heavy sessions. Set an API key in ~/.agentmemory/.env to avoid rate limits (#149).\n");
|
|
64
68
|
return {
|
|
65
69
|
provider: "agent-sdk",
|
|
66
70
|
model: "claude-sonnet-4-20250514",
|
|
@@ -726,7 +730,11 @@ function createBaseProvider(config) {
|
|
|
726
730
|
switch (config.provider) {
|
|
727
731
|
case "minimax": return new MinimaxProvider(requireEnvVar("MINIMAX_API_KEY"), config.model, config.maxTokens);
|
|
728
732
|
case "anthropic": return new AnthropicProvider(requireEnvVar("ANTHROPIC_API_KEY"), config.model, config.maxTokens, config.baseURL);
|
|
729
|
-
case "gemini":
|
|
733
|
+
case "gemini": {
|
|
734
|
+
const geminiKey = getEnvVar("GEMINI_API_KEY") || getEnvVar("GOOGLE_API_KEY");
|
|
735
|
+
if (!geminiKey) throw new Error("GEMINI_API_KEY (or GOOGLE_API_KEY) is required for the gemini provider");
|
|
736
|
+
return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
|
|
737
|
+
}
|
|
730
738
|
case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
|
|
731
739
|
default: return new AgentSDKProvider();
|
|
732
740
|
}
|
|
@@ -2121,7 +2129,7 @@ function stringifyForNarrative(v) {
|
|
|
2121
2129
|
return String(v);
|
|
2122
2130
|
}
|
|
2123
2131
|
}
|
|
2124
|
-
function truncate$
|
|
2132
|
+
function truncate$2(s, n) {
|
|
2125
2133
|
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
2126
2134
|
}
|
|
2127
2135
|
function buildSyntheticCompression(raw) {
|
|
@@ -2138,10 +2146,10 @@ function buildSyntheticCompression(raw) {
|
|
|
2138
2146
|
sessionId: raw.sessionId,
|
|
2139
2147
|
timestamp: raw.timestamp,
|
|
2140
2148
|
type: inferType(toolName, raw.hookType),
|
|
2141
|
-
title: truncate$
|
|
2142
|
-
subtitle: inputStr ? truncate$
|
|
2149
|
+
title: truncate$2(toolName || "observation", 80),
|
|
2150
|
+
subtitle: inputStr ? truncate$2(inputStr, 120) : void 0,
|
|
2143
2151
|
facts: [],
|
|
2144
|
-
narrative: truncate$
|
|
2152
|
+
narrative: truncate$2(narrativeParts.join(" | "), 400),
|
|
2145
2153
|
concepts: [],
|
|
2146
2154
|
files: extractFiles$1(raw.toolInput),
|
|
2147
2155
|
importance: 5,
|
|
@@ -2267,6 +2275,17 @@ function registerSearchFunction(sdk, kv) {
|
|
|
2267
2275
|
}
|
|
2268
2276
|
const projectFilter = typeof data.project === "string" && data.project.length > 0 ? data.project : void 0;
|
|
2269
2277
|
const cwdFilter = typeof data.cwd === "string" && data.cwd.length > 0 ? data.cwd : void 0;
|
|
2278
|
+
const format = typeof data.format === "string" ? data.format : "full";
|
|
2279
|
+
if (![
|
|
2280
|
+
"full",
|
|
2281
|
+
"compact",
|
|
2282
|
+
"narrative"
|
|
2283
|
+
].includes(format)) throw new Error("mem::search: format must be one of 'full', 'compact', or 'narrative'");
|
|
2284
|
+
let tokenBudget;
|
|
2285
|
+
if (data.token_budget !== void 0) {
|
|
2286
|
+
if (!Number.isInteger(data.token_budget) || data.token_budget < 1) throw new Error("mem::search: token_budget must be a positive integer");
|
|
2287
|
+
tokenBudget = data.token_budget;
|
|
2288
|
+
}
|
|
2270
2289
|
if (idx.size === 0) {
|
|
2271
2290
|
const count = await rebuildIndex(kv);
|
|
2272
2291
|
logger.info("Search index rebuilt", { entries: count });
|
|
@@ -2303,13 +2322,81 @@ function registerSearchFunction(sdk, kv) {
|
|
|
2303
2322
|
});
|
|
2304
2323
|
}
|
|
2305
2324
|
recordAccessBatch(kv, enriched.map((r) => r.observation.id));
|
|
2325
|
+
const estimateTokens = (value) => Math.max(1, Math.ceil(JSON.stringify(value).length / 3));
|
|
2326
|
+
const applyTokenBudget = (items) => {
|
|
2327
|
+
if (!tokenBudget) return {
|
|
2328
|
+
items,
|
|
2329
|
+
used: items.reduce((sum, item) => sum + estimateTokens(item), 0),
|
|
2330
|
+
truncated: false
|
|
2331
|
+
};
|
|
2332
|
+
const selected = [];
|
|
2333
|
+
let used = 0;
|
|
2334
|
+
for (const item of items) {
|
|
2335
|
+
const itemTokens = estimateTokens(item);
|
|
2336
|
+
if (used + itemTokens > tokenBudget) return {
|
|
2337
|
+
items: selected,
|
|
2338
|
+
used,
|
|
2339
|
+
truncated: selected.length < items.length
|
|
2340
|
+
};
|
|
2341
|
+
selected.push(item);
|
|
2342
|
+
used += itemTokens;
|
|
2343
|
+
}
|
|
2344
|
+
return {
|
|
2345
|
+
items: selected,
|
|
2346
|
+
used,
|
|
2347
|
+
truncated: false
|
|
2348
|
+
};
|
|
2349
|
+
};
|
|
2350
|
+
if (format === "compact") {
|
|
2351
|
+
const packed = applyTokenBudget(enriched.map((r) => ({
|
|
2352
|
+
obsId: r.observation.id,
|
|
2353
|
+
sessionId: r.sessionId,
|
|
2354
|
+
title: r.observation.title,
|
|
2355
|
+
type: r.observation.type,
|
|
2356
|
+
score: r.score,
|
|
2357
|
+
timestamp: r.observation.timestamp
|
|
2358
|
+
})));
|
|
2359
|
+
return {
|
|
2360
|
+
format,
|
|
2361
|
+
results: packed.items,
|
|
2362
|
+
tokens_used: packed.used,
|
|
2363
|
+
tokens_budget: tokenBudget,
|
|
2364
|
+
truncated: packed.truncated
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
if (format === "narrative") {
|
|
2368
|
+
const packed = applyTokenBudget(enriched.map((r) => ({
|
|
2369
|
+
obsId: r.observation.id,
|
|
2370
|
+
sessionId: r.sessionId,
|
|
2371
|
+
title: r.observation.title,
|
|
2372
|
+
narrative: r.observation.narrative,
|
|
2373
|
+
score: r.score,
|
|
2374
|
+
timestamp: r.observation.timestamp
|
|
2375
|
+
})));
|
|
2376
|
+
const text = packed.items.map((r, index) => `${index + 1}. ${r.title}\n${r.narrative}`).join("\n\n");
|
|
2377
|
+
return {
|
|
2378
|
+
format,
|
|
2379
|
+
results: packed.items,
|
|
2380
|
+
text,
|
|
2381
|
+
tokens_used: packed.used,
|
|
2382
|
+
tokens_budget: tokenBudget,
|
|
2383
|
+
truncated: packed.truncated
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
const packed = applyTokenBudget(enriched);
|
|
2306
2387
|
logger.info("Search completed", {
|
|
2307
2388
|
query,
|
|
2308
|
-
results:
|
|
2389
|
+
results: packed.items.length,
|
|
2309
2390
|
hasProjectFilter: !!projectFilter,
|
|
2310
2391
|
hasCwdFilter: !!cwdFilter
|
|
2311
2392
|
});
|
|
2312
|
-
return {
|
|
2393
|
+
return {
|
|
2394
|
+
format,
|
|
2395
|
+
results: packed.items,
|
|
2396
|
+
tokens_used: packed.used,
|
|
2397
|
+
tokens_budget: tokenBudget,
|
|
2398
|
+
truncated: packed.truncated
|
|
2399
|
+
};
|
|
2313
2400
|
});
|
|
2314
2401
|
}
|
|
2315
2402
|
|
|
@@ -2486,16 +2573,16 @@ function buildCompressionPrompt(observation) {
|
|
|
2486
2573
|
if (observation.toolName) parts.push(`Tool: ${observation.toolName}`);
|
|
2487
2574
|
if (observation.toolInput) {
|
|
2488
2575
|
const input = typeof observation.toolInput === "string" ? observation.toolInput : JSON.stringify(observation.toolInput, null, 2);
|
|
2489
|
-
parts.push(`Input:\n${truncate(input, 4e3)}`);
|
|
2576
|
+
parts.push(`Input:\n${truncate$1(input, 4e3)}`);
|
|
2490
2577
|
}
|
|
2491
2578
|
if (observation.toolOutput) {
|
|
2492
2579
|
const output = typeof observation.toolOutput === "string" ? observation.toolOutput : JSON.stringify(observation.toolOutput, null, 2);
|
|
2493
|
-
parts.push(`Output:\n${truncate(output, 4e3)}`);
|
|
2580
|
+
parts.push(`Output:\n${truncate$1(output, 4e3)}`);
|
|
2494
2581
|
}
|
|
2495
|
-
if (observation.userPrompt) parts.push(`User prompt:\n${truncate(observation.userPrompt, 2e3)}`);
|
|
2582
|
+
if (observation.userPrompt) parts.push(`User prompt:\n${truncate$1(observation.userPrompt, 2e3)}`);
|
|
2496
2583
|
return parts.join("\n\n");
|
|
2497
2584
|
}
|
|
2498
|
-
function truncate(s, max) {
|
|
2585
|
+
function truncate$1(s, max) {
|
|
2499
2586
|
return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
|
|
2500
2587
|
}
|
|
2501
2588
|
|
|
@@ -3660,25 +3747,40 @@ function registerRememberFunction(sdk, kv) {
|
|
|
3660
3747
|
});
|
|
3661
3748
|
sdk.registerFunction("mem::forget", async (data) => {
|
|
3662
3749
|
let deleted = 0;
|
|
3750
|
+
const deletedMemoryIds = [];
|
|
3751
|
+
const deletedObservationIds = [];
|
|
3752
|
+
let deletedSession = false;
|
|
3663
3753
|
if (data.memoryId) {
|
|
3664
3754
|
await kv.delete(KV.memories, data.memoryId);
|
|
3665
3755
|
await deleteAccessLog(kv, data.memoryId);
|
|
3756
|
+
deletedMemoryIds.push(data.memoryId);
|
|
3666
3757
|
deleted++;
|
|
3667
3758
|
}
|
|
3668
3759
|
if (data.sessionId && data.observationIds && data.observationIds.length > 0) for (const obsId of data.observationIds) {
|
|
3669
3760
|
await kv.delete(KV.observations(data.sessionId), obsId);
|
|
3761
|
+
deletedObservationIds.push(obsId);
|
|
3670
3762
|
deleted++;
|
|
3671
3763
|
}
|
|
3672
3764
|
if (data.sessionId && (!data.observationIds || data.observationIds.length === 0) && !data.memoryId) {
|
|
3673
3765
|
const observations = await kv.list(KV.observations(data.sessionId));
|
|
3674
3766
|
for (const obs of observations) {
|
|
3675
3767
|
await kv.delete(KV.observations(data.sessionId), obs.id);
|
|
3768
|
+
deletedObservationIds.push(obs.id);
|
|
3676
3769
|
deleted++;
|
|
3677
3770
|
}
|
|
3678
3771
|
await kv.delete(KV.sessions, data.sessionId);
|
|
3679
3772
|
await kv.delete(KV.summaries, data.sessionId);
|
|
3773
|
+
deletedSession = true;
|
|
3680
3774
|
deleted += 2;
|
|
3681
3775
|
}
|
|
3776
|
+
if (deleted > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
|
|
3777
|
+
sessionId: data.sessionId,
|
|
3778
|
+
deleted,
|
|
3779
|
+
memoriesDeleted: deletedMemoryIds.length,
|
|
3780
|
+
observationsDeleted: deletedObservationIds.length,
|
|
3781
|
+
sessionDeleted: deletedSession,
|
|
3782
|
+
reason: "user-initiated forget"
|
|
3783
|
+
});
|
|
3682
3784
|
logger.info("Memory forgotten", { deleted });
|
|
3683
3785
|
return {
|
|
3684
3786
|
success: true,
|
|
@@ -4398,7 +4500,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4398
4500
|
|
|
4399
4501
|
//#endregion
|
|
4400
4502
|
//#region src/version.ts
|
|
4401
|
-
const VERSION = "0.
|
|
4503
|
+
const VERSION = "0.9.0";
|
|
4402
4504
|
|
|
4403
4505
|
//#endregion
|
|
4404
4506
|
//#region src/functions/export-import.ts
|
|
@@ -4513,7 +4615,10 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
4513
4615
|
"0.8.8",
|
|
4514
4616
|
"0.8.9",
|
|
4515
4617
|
"0.8.10",
|
|
4516
|
-
"0.8.11"
|
|
4618
|
+
"0.8.11",
|
|
4619
|
+
"0.8.12",
|
|
4620
|
+
"0.8.13",
|
|
4621
|
+
"0.9.0"
|
|
4517
4622
|
]).has(importData.version)) return {
|
|
4518
4623
|
success: false,
|
|
4519
4624
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -10885,8 +10990,8 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10885
10990
|
let evictedSemantic = 0;
|
|
10886
10991
|
const evictedIds = [];
|
|
10887
10992
|
for (const candidate of candidates) try {
|
|
10888
|
-
let scope;
|
|
10889
|
-
let resolvedSource;
|
|
10993
|
+
let scope = null;
|
|
10994
|
+
let resolvedSource = null;
|
|
10890
10995
|
if (candidate.source === "semantic") {
|
|
10891
10996
|
scope = KV.semantic;
|
|
10892
10997
|
resolvedSource = "semantic";
|
|
@@ -10896,10 +11001,11 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10896
11001
|
} else if (await kv.get(KV.memories, candidate.memoryId) !== null) {
|
|
10897
11002
|
scope = KV.memories;
|
|
10898
11003
|
resolvedSource = "episodic";
|
|
10899
|
-
} else {
|
|
11004
|
+
} else if (await kv.get(KV.semantic, candidate.memoryId) !== null) {
|
|
10900
11005
|
scope = KV.semantic;
|
|
10901
11006
|
resolvedSource = "semantic";
|
|
10902
11007
|
}
|
|
11008
|
+
if (!scope || !resolvedSource) continue;
|
|
10903
11009
|
await kv.delete(scope, candidate.memoryId);
|
|
10904
11010
|
await kv.delete(KV.retentionScores, candidate.memoryId);
|
|
10905
11011
|
await deleteAccessLog(kv, candidate.memoryId);
|
|
@@ -10932,6 +11038,561 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10932
11038
|
});
|
|
10933
11039
|
}
|
|
10934
11040
|
|
|
11041
|
+
//#endregion
|
|
11042
|
+
//#region src/functions/compress-file.ts
|
|
11043
|
+
const SENSITIVE_PATH_TERMS = [
|
|
11044
|
+
"secret",
|
|
11045
|
+
"credential",
|
|
11046
|
+
"private_key",
|
|
11047
|
+
".env",
|
|
11048
|
+
"id_rsa",
|
|
11049
|
+
"token"
|
|
11050
|
+
];
|
|
11051
|
+
const COMPRESS_FILE_SYSTEM_PROMPT = `You compress markdown while preserving structure.
|
|
11052
|
+
Rules:
|
|
11053
|
+
- Keep all headings exactly as-is.
|
|
11054
|
+
- Keep all URLs exactly as-is.
|
|
11055
|
+
- Keep all fenced code blocks exactly as-is.
|
|
11056
|
+
- Do not remove sections; shorten prose under each section.
|
|
11057
|
+
- Output only markdown, no wrappers or explanations.`;
|
|
11058
|
+
function stripMarkdownFence(text) {
|
|
11059
|
+
const trimmed = text.trim();
|
|
11060
|
+
const match = trimmed.match(/^```(?:markdown|md)?\s*([\s\S]*?)\s*```$/i);
|
|
11061
|
+
return match ? match[1].trim() : trimmed;
|
|
11062
|
+
}
|
|
11063
|
+
function extractUrls(text) {
|
|
11064
|
+
return Array.from(new Set(text.match(/https?:\/\/[^\s)]+/g) || []));
|
|
11065
|
+
}
|
|
11066
|
+
function extractHeadings(text) {
|
|
11067
|
+
return text.split("\n").map((line) => line.trim()).filter((line) => /^#{1,6}\s+/.test(line));
|
|
11068
|
+
}
|
|
11069
|
+
function extractCodeBlocks(text) {
|
|
11070
|
+
return text.match(/```[\s\S]*?```/g) || [];
|
|
11071
|
+
}
|
|
11072
|
+
function validateCompression(original, compressed) {
|
|
11073
|
+
const errors = [];
|
|
11074
|
+
const originalHeadings = extractHeadings(original);
|
|
11075
|
+
const compressedHeadings = extractHeadings(compressed);
|
|
11076
|
+
for (const heading of originalHeadings) if (!compressedHeadings.includes(heading)) errors.push(`missing heading: ${heading}`);
|
|
11077
|
+
const originalUrls = extractUrls(original).sort();
|
|
11078
|
+
const compressedUrls = extractUrls(compressed).sort();
|
|
11079
|
+
if (originalUrls.length !== compressedUrls.length) errors.push("url count changed");
|
|
11080
|
+
else for (let i = 0; i < originalUrls.length; i++) if (originalUrls[i] !== compressedUrls[i]) {
|
|
11081
|
+
errors.push("url set changed");
|
|
11082
|
+
break;
|
|
11083
|
+
}
|
|
11084
|
+
const originalBlocks = extractCodeBlocks(original);
|
|
11085
|
+
const compressedBlocks = extractCodeBlocks(compressed);
|
|
11086
|
+
if (originalBlocks.length !== compressedBlocks.length) errors.push("code block count changed");
|
|
11087
|
+
else for (let i = 0; i < originalBlocks.length; i++) if (originalBlocks[i] !== compressedBlocks[i]) {
|
|
11088
|
+
errors.push("code block content changed");
|
|
11089
|
+
break;
|
|
11090
|
+
}
|
|
11091
|
+
return errors;
|
|
11092
|
+
}
|
|
11093
|
+
function resolveBackupPath(filePath) {
|
|
11094
|
+
const base = basename(filePath, extname(filePath));
|
|
11095
|
+
const name = base.endsWith(".original") ? `${base}.backup` : `${base}.original`;
|
|
11096
|
+
return join(dirname(filePath), `${name}.md`);
|
|
11097
|
+
}
|
|
11098
|
+
function registerCompressFileFunction(sdk, kv, provider) {
|
|
11099
|
+
sdk.registerFunction("mem::compress-file", async (data) => {
|
|
11100
|
+
if (!data?.filePath || typeof data.filePath !== "string") return {
|
|
11101
|
+
success: false,
|
|
11102
|
+
error: "filePath is required"
|
|
11103
|
+
};
|
|
11104
|
+
const absolutePath = resolve(data.filePath);
|
|
11105
|
+
const lowerPath = absolutePath.toLowerCase();
|
|
11106
|
+
if (extname(absolutePath).toLowerCase() !== ".md") return {
|
|
11107
|
+
success: false,
|
|
11108
|
+
error: "filePath must point to a .md file"
|
|
11109
|
+
};
|
|
11110
|
+
if (SENSITIVE_PATH_TERMS.some((term) => lowerPath.includes(term))) return {
|
|
11111
|
+
success: false,
|
|
11112
|
+
error: "refusing to process sensitive-looking path"
|
|
11113
|
+
};
|
|
11114
|
+
try {
|
|
11115
|
+
if ((await lstat(absolutePath)).isSymbolicLink()) return {
|
|
11116
|
+
success: false,
|
|
11117
|
+
error: "symlinks are not supported"
|
|
11118
|
+
};
|
|
11119
|
+
} catch {
|
|
11120
|
+
return {
|
|
11121
|
+
success: false,
|
|
11122
|
+
error: "file not found"
|
|
11123
|
+
};
|
|
11124
|
+
}
|
|
11125
|
+
let original;
|
|
11126
|
+
try {
|
|
11127
|
+
original = await readFile(absolutePath, "utf-8");
|
|
11128
|
+
} catch {
|
|
11129
|
+
return {
|
|
11130
|
+
success: false,
|
|
11131
|
+
error: "failed to read file"
|
|
11132
|
+
};
|
|
11133
|
+
}
|
|
11134
|
+
if (!original.trim()) return {
|
|
11135
|
+
success: true,
|
|
11136
|
+
skipped: true,
|
|
11137
|
+
reason: "file is empty"
|
|
11138
|
+
};
|
|
11139
|
+
const compressed = stripMarkdownFence(await provider.summarize(COMPRESS_FILE_SYSTEM_PROMPT, `Compress this markdown file while preserving structure and code blocks:\n\n${original}`));
|
|
11140
|
+
const validationErrors = validateCompression(original, compressed);
|
|
11141
|
+
if (validationErrors.length > 0) return {
|
|
11142
|
+
success: false,
|
|
11143
|
+
error: "compression validation failed",
|
|
11144
|
+
details: validationErrors
|
|
11145
|
+
};
|
|
11146
|
+
const backupPath = resolveBackupPath(absolutePath);
|
|
11147
|
+
await writeFile(backupPath, original, "utf-8");
|
|
11148
|
+
let fd = null;
|
|
11149
|
+
try {
|
|
11150
|
+
fd = await open(absolutePath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW);
|
|
11151
|
+
await fd.writeFile(compressed, "utf-8");
|
|
11152
|
+
} catch (err) {
|
|
11153
|
+
const code = err.code;
|
|
11154
|
+
if (code === "ELOOP" || code === "EINVAL") return {
|
|
11155
|
+
success: false,
|
|
11156
|
+
error: "symlinks are not supported"
|
|
11157
|
+
};
|
|
11158
|
+
return {
|
|
11159
|
+
success: false,
|
|
11160
|
+
error: "failed to write compressed file"
|
|
11161
|
+
};
|
|
11162
|
+
} finally {
|
|
11163
|
+
await fd?.close().catch(() => {});
|
|
11164
|
+
}
|
|
11165
|
+
try {
|
|
11166
|
+
await recordAudit(kv, "compress", "mem::compress-file", [], {
|
|
11167
|
+
filePath: absolutePath,
|
|
11168
|
+
backupPath,
|
|
11169
|
+
originalChars: original.length,
|
|
11170
|
+
compressedChars: compressed.length
|
|
11171
|
+
});
|
|
11172
|
+
} catch {}
|
|
11173
|
+
return {
|
|
11174
|
+
success: true,
|
|
11175
|
+
filePath: absolutePath,
|
|
11176
|
+
backupPath,
|
|
11177
|
+
originalChars: original.length,
|
|
11178
|
+
compressedChars: compressed.length
|
|
11179
|
+
};
|
|
11180
|
+
});
|
|
11181
|
+
}
|
|
11182
|
+
|
|
11183
|
+
//#endregion
|
|
11184
|
+
//#region src/replay/jsonl-parser.ts
|
|
11185
|
+
function deriveProject(cwd) {
|
|
11186
|
+
if (!cwd) return "unknown";
|
|
11187
|
+
const parts = cwd.split("/").filter(Boolean);
|
|
11188
|
+
return parts[parts.length - 1] || "unknown";
|
|
11189
|
+
}
|
|
11190
|
+
function toText(content) {
|
|
11191
|
+
if (typeof content === "string") return content;
|
|
11192
|
+
if (!Array.isArray(content)) return "";
|
|
11193
|
+
const parts = [];
|
|
11194
|
+
for (const item of content) {
|
|
11195
|
+
if (!item || typeof item !== "object") continue;
|
|
11196
|
+
const entry = item;
|
|
11197
|
+
if (entry.type === "text" && typeof entry.text === "string") parts.push(entry.text);
|
|
11198
|
+
}
|
|
11199
|
+
return parts.join("\n");
|
|
11200
|
+
}
|
|
11201
|
+
function extractToolUses(content) {
|
|
11202
|
+
if (!Array.isArray(content)) return [];
|
|
11203
|
+
const out = [];
|
|
11204
|
+
for (const item of content) {
|
|
11205
|
+
if (!item || typeof item !== "object") continue;
|
|
11206
|
+
const entry = item;
|
|
11207
|
+
if (entry.type === "tool_use") out.push({
|
|
11208
|
+
id: typeof entry.id === "string" ? entry.id : "",
|
|
11209
|
+
name: typeof entry.name === "string" ? entry.name : "unknown",
|
|
11210
|
+
input: entry.input
|
|
11211
|
+
});
|
|
11212
|
+
}
|
|
11213
|
+
return out;
|
|
11214
|
+
}
|
|
11215
|
+
function extractToolResults(content) {
|
|
11216
|
+
if (!Array.isArray(content)) return [];
|
|
11217
|
+
const out = [];
|
|
11218
|
+
for (const item of content) {
|
|
11219
|
+
if (!item || typeof item !== "object") continue;
|
|
11220
|
+
const entry = item;
|
|
11221
|
+
if (entry.type === "tool_result") out.push({
|
|
11222
|
+
toolUseId: typeof entry.tool_use_id === "string" ? entry.tool_use_id : "",
|
|
11223
|
+
output: entry.content,
|
|
11224
|
+
isError: entry.is_error === true
|
|
11225
|
+
});
|
|
11226
|
+
}
|
|
11227
|
+
return out;
|
|
11228
|
+
}
|
|
11229
|
+
function parseJsonlText(text, fallbackSessionId) {
|
|
11230
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
11231
|
+
const entries = [];
|
|
11232
|
+
for (const line of lines) try {
|
|
11233
|
+
const parsed = JSON.parse(line);
|
|
11234
|
+
if (parsed && typeof parsed === "object") entries.push(parsed);
|
|
11235
|
+
} catch {}
|
|
11236
|
+
let sessionId = fallbackSessionId || "";
|
|
11237
|
+
let cwd = "";
|
|
11238
|
+
let firstTs = "";
|
|
11239
|
+
let lastTs = "";
|
|
11240
|
+
const observations = [];
|
|
11241
|
+
for (const entry of entries) {
|
|
11242
|
+
if (entry.sessionId && !sessionId) sessionId = entry.sessionId;
|
|
11243
|
+
if (entry.cwd && !cwd) cwd = entry.cwd;
|
|
11244
|
+
const ts = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
11245
|
+
if (!firstTs) firstTs = ts;
|
|
11246
|
+
lastTs = ts;
|
|
11247
|
+
const role = entry.message?.role;
|
|
11248
|
+
const content = entry.message?.content;
|
|
11249
|
+
if (entry.type === "user" && role === "user") {
|
|
11250
|
+
const toolResults = extractToolResults(content);
|
|
11251
|
+
if (toolResults.length > 0) for (const result of toolResults) observations.push({
|
|
11252
|
+
id: generateId("obs"),
|
|
11253
|
+
sessionId: sessionId || "imported",
|
|
11254
|
+
timestamp: ts,
|
|
11255
|
+
hookType: result.isError ? "post_tool_failure" : "post_tool_use",
|
|
11256
|
+
toolName: void 0,
|
|
11257
|
+
toolInput: { toolUseId: result.toolUseId },
|
|
11258
|
+
toolOutput: result.output,
|
|
11259
|
+
raw: entry
|
|
11260
|
+
});
|
|
11261
|
+
else {
|
|
11262
|
+
const text = toText(content);
|
|
11263
|
+
if (text.trim().length > 0) observations.push({
|
|
11264
|
+
id: generateId("obs"),
|
|
11265
|
+
sessionId: sessionId || "imported",
|
|
11266
|
+
timestamp: ts,
|
|
11267
|
+
hookType: "prompt_submit",
|
|
11268
|
+
userPrompt: text,
|
|
11269
|
+
raw: entry
|
|
11270
|
+
});
|
|
11271
|
+
}
|
|
11272
|
+
} else if (entry.type === "assistant" && role === "assistant") {
|
|
11273
|
+
const text = toText(content);
|
|
11274
|
+
const tools = extractToolUses(content);
|
|
11275
|
+
if (text.trim().length > 0) observations.push({
|
|
11276
|
+
id: generateId("obs"),
|
|
11277
|
+
sessionId: sessionId || "imported",
|
|
11278
|
+
timestamp: ts,
|
|
11279
|
+
hookType: "stop",
|
|
11280
|
+
assistantResponse: text,
|
|
11281
|
+
raw: entry
|
|
11282
|
+
});
|
|
11283
|
+
for (const tool of tools) observations.push({
|
|
11284
|
+
id: generateId("obs"),
|
|
11285
|
+
sessionId: sessionId || "imported",
|
|
11286
|
+
timestamp: ts,
|
|
11287
|
+
hookType: "pre_tool_use",
|
|
11288
|
+
toolName: tool.name,
|
|
11289
|
+
toolInput: tool.input,
|
|
11290
|
+
raw: {
|
|
11291
|
+
toolUseId: tool.id,
|
|
11292
|
+
entry
|
|
11293
|
+
}
|
|
11294
|
+
});
|
|
11295
|
+
} else if (entry.type === "summary" || entry.type === "system") {}
|
|
11296
|
+
}
|
|
11297
|
+
const effectiveSessionId = sessionId || generateId("sess");
|
|
11298
|
+
for (const obs of observations) if (obs.sessionId === "imported") obs.sessionId = effectiveSessionId;
|
|
11299
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
11300
|
+
return {
|
|
11301
|
+
sessionId: effectiveSessionId,
|
|
11302
|
+
project: deriveProject(cwd),
|
|
11303
|
+
cwd: cwd || process.cwd(),
|
|
11304
|
+
startedAt: firstTs || nowIso,
|
|
11305
|
+
endedAt: lastTs || nowIso,
|
|
11306
|
+
observations
|
|
11307
|
+
};
|
|
11308
|
+
}
|
|
11309
|
+
|
|
11310
|
+
//#endregion
|
|
11311
|
+
//#region src/replay/timeline.ts
|
|
11312
|
+
const DEFAULT_CHARS_PER_SEC = 40;
|
|
11313
|
+
const MIN_EVENT_MS = 300;
|
|
11314
|
+
const MAX_EVENT_MS = 2e4;
|
|
11315
|
+
function kindFromHook(obs) {
|
|
11316
|
+
switch (obs.hookType) {
|
|
11317
|
+
case "session_start": return "session_start";
|
|
11318
|
+
case "session_end": return "session_end";
|
|
11319
|
+
case "prompt_submit": return "prompt";
|
|
11320
|
+
case "stop": return obs.assistantResponse ? "response" : "hook";
|
|
11321
|
+
case "pre_tool_use": return "tool_call";
|
|
11322
|
+
case "post_tool_use": return "tool_result";
|
|
11323
|
+
case "post_tool_failure": return "tool_error";
|
|
11324
|
+
default: return "hook";
|
|
11325
|
+
}
|
|
11326
|
+
}
|
|
11327
|
+
function labelFor(obs, kind) {
|
|
11328
|
+
switch (kind) {
|
|
11329
|
+
case "prompt": return truncate(obs.userPrompt || "User prompt", 80);
|
|
11330
|
+
case "response": return truncate(obs.assistantResponse || "Assistant response", 80);
|
|
11331
|
+
case "tool_call": return `${obs.toolName || "tool"} ▸ call`;
|
|
11332
|
+
case "tool_result": return `${obs.toolName || "tool"} ▸ result`;
|
|
11333
|
+
case "tool_error": return `${obs.toolName || "tool"} ▸ error`;
|
|
11334
|
+
case "session_start": return "Session start";
|
|
11335
|
+
case "session_end": return "Session end";
|
|
11336
|
+
default: return obs.hookType;
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
11339
|
+
function truncate(text, max) {
|
|
11340
|
+
if (text.length <= max) return text;
|
|
11341
|
+
return text.slice(0, max - 1) + "…";
|
|
11342
|
+
}
|
|
11343
|
+
function bodyFor(obs, kind) {
|
|
11344
|
+
if (kind === "prompt") return obs.userPrompt;
|
|
11345
|
+
if (kind === "response") return obs.assistantResponse;
|
|
11346
|
+
}
|
|
11347
|
+
function estimateDurationMs(ev) {
|
|
11348
|
+
const chars = (ev.body?.length || 0) + (typeof ev.toolInput === "string" ? ev.toolInput.length : 0) + (typeof ev.toolOutput === "string" ? ev.toolOutput.length : 0);
|
|
11349
|
+
if (chars === 0) return MIN_EVENT_MS;
|
|
11350
|
+
const ms = Math.round(chars / DEFAULT_CHARS_PER_SEC * 1e3);
|
|
11351
|
+
return Math.max(MIN_EVENT_MS, Math.min(MAX_EVENT_MS, ms));
|
|
11352
|
+
}
|
|
11353
|
+
function projectTimeline(observations) {
|
|
11354
|
+
if (observations.length === 0) {
|
|
11355
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11356
|
+
return {
|
|
11357
|
+
sessionId: "",
|
|
11358
|
+
startedAt: now,
|
|
11359
|
+
endedAt: now,
|
|
11360
|
+
totalDurationMs: 0,
|
|
11361
|
+
eventCount: 0,
|
|
11362
|
+
events: []
|
|
11363
|
+
};
|
|
11364
|
+
}
|
|
11365
|
+
const sorted = [...observations].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
11366
|
+
const startedAt = sorted[0].timestamp;
|
|
11367
|
+
const startMs = Date.parse(startedAt);
|
|
11368
|
+
const events = [];
|
|
11369
|
+
let syntheticOffset = 0;
|
|
11370
|
+
const allSameTs = sorted.every((o) => o.timestamp === startedAt);
|
|
11371
|
+
for (const obs of sorted) {
|
|
11372
|
+
const kind = kindFromHook(obs);
|
|
11373
|
+
const body = bodyFor(obs, kind);
|
|
11374
|
+
const obsMs = Date.parse(obs.timestamp);
|
|
11375
|
+
const offsetMs = allSameTs ? syntheticOffset : Number.isFinite(obsMs) && Number.isFinite(startMs) ? Math.max(0, obsMs - startMs) : syntheticOffset;
|
|
11376
|
+
const event = {
|
|
11377
|
+
id: obs.id,
|
|
11378
|
+
sessionId: obs.sessionId,
|
|
11379
|
+
ts: obs.timestamp,
|
|
11380
|
+
offsetMs,
|
|
11381
|
+
durationMs: 0,
|
|
11382
|
+
kind,
|
|
11383
|
+
label: labelFor(obs, kind),
|
|
11384
|
+
body,
|
|
11385
|
+
toolName: obs.toolName,
|
|
11386
|
+
toolInput: obs.toolInput,
|
|
11387
|
+
toolOutput: obs.toolOutput
|
|
11388
|
+
};
|
|
11389
|
+
event.durationMs = estimateDurationMs(event);
|
|
11390
|
+
events.push(event);
|
|
11391
|
+
syntheticOffset += event.durationMs;
|
|
11392
|
+
}
|
|
11393
|
+
const last = events[events.length - 1];
|
|
11394
|
+
const totalDurationMs = last.offsetMs + last.durationMs;
|
|
11395
|
+
return {
|
|
11396
|
+
sessionId: sorted[0].sessionId,
|
|
11397
|
+
startedAt,
|
|
11398
|
+
endedAt: sorted[sorted.length - 1].timestamp,
|
|
11399
|
+
totalDurationMs,
|
|
11400
|
+
eventCount: events.length,
|
|
11401
|
+
events
|
|
11402
|
+
};
|
|
11403
|
+
}
|
|
11404
|
+
|
|
11405
|
+
//#endregion
|
|
11406
|
+
//#region src/functions/replay.ts
|
|
11407
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
11408
|
+
/(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
|
|
11409
|
+
/(^|[\\/_.-])credentials?([\\/_.-]|$)/i,
|
|
11410
|
+
/(^|[\\/_.-])private[_-]?key([\\/_.-]|$)/i,
|
|
11411
|
+
/(^|[\\/])\.env(\.[\w-]+)?$/i,
|
|
11412
|
+
/(^|[\\/_.-])id_rsa([\\/_.-]|$)/i,
|
|
11413
|
+
/(^|[\\/])auth[_-]?token([\\/_.-]|$)/i,
|
|
11414
|
+
/(^|[\\/])bearer[_-]?token([\\/_.-]|$)/i,
|
|
11415
|
+
/(^|[\\/])access[_-]?token([\\/_.-]|$)/i,
|
|
11416
|
+
/(^|[\\/])api[_-]?token([\\/_.-]|$)/i
|
|
11417
|
+
];
|
|
11418
|
+
function isSensitive(path) {
|
|
11419
|
+
return SENSITIVE_PATH_PATTERNS.some((re) => re.test(path));
|
|
11420
|
+
}
|
|
11421
|
+
async function isSymlink(path) {
|
|
11422
|
+
try {
|
|
11423
|
+
return (await lstat(path)).isSymbolicLink();
|
|
11424
|
+
} catch {
|
|
11425
|
+
return false;
|
|
11426
|
+
}
|
|
11427
|
+
}
|
|
11428
|
+
function rawFromCompressed(obs) {
|
|
11429
|
+
return {
|
|
11430
|
+
id: obs.id,
|
|
11431
|
+
sessionId: obs.sessionId,
|
|
11432
|
+
timestamp: obs.timestamp,
|
|
11433
|
+
hookType: "post_tool_use",
|
|
11434
|
+
toolName: void 0,
|
|
11435
|
+
toolInput: void 0,
|
|
11436
|
+
toolOutput: void 0,
|
|
11437
|
+
userPrompt: obs.type === "conversation" ? obs.narrative : void 0,
|
|
11438
|
+
assistantResponse: void 0,
|
|
11439
|
+
raw: {
|
|
11440
|
+
title: obs.title,
|
|
11441
|
+
narrative: obs.narrative,
|
|
11442
|
+
facts: obs.facts
|
|
11443
|
+
}
|
|
11444
|
+
};
|
|
11445
|
+
}
|
|
11446
|
+
function isRawShape(o) {
|
|
11447
|
+
if (!o || typeof o !== "object") return false;
|
|
11448
|
+
return typeof o.hookType === "string";
|
|
11449
|
+
}
|
|
11450
|
+
async function loadObservations(kv, sessionId) {
|
|
11451
|
+
return (await kv.list(KV.observations(sessionId))).map((r) => isRawShape(r) ? r : rawFromCompressed(r));
|
|
11452
|
+
}
|
|
11453
|
+
async function findJsonlFiles(root, limit = 200) {
|
|
11454
|
+
const out = [];
|
|
11455
|
+
async function walk(dir) {
|
|
11456
|
+
if (out.length >= limit) return;
|
|
11457
|
+
let names;
|
|
11458
|
+
try {
|
|
11459
|
+
names = await readdir(dir);
|
|
11460
|
+
} catch {
|
|
11461
|
+
return;
|
|
11462
|
+
}
|
|
11463
|
+
for (const name of names) {
|
|
11464
|
+
if (out.length >= limit) return;
|
|
11465
|
+
const full = join(dir, name);
|
|
11466
|
+
let st;
|
|
11467
|
+
try {
|
|
11468
|
+
st = await lstat(full);
|
|
11469
|
+
} catch {
|
|
11470
|
+
continue;
|
|
11471
|
+
}
|
|
11472
|
+
if (st.isSymbolicLink()) continue;
|
|
11473
|
+
if (st.isDirectory()) await walk(full);
|
|
11474
|
+
else if (st.isFile() && name.endsWith(".jsonl")) out.push(full);
|
|
11475
|
+
}
|
|
11476
|
+
}
|
|
11477
|
+
await walk(root);
|
|
11478
|
+
return out;
|
|
11479
|
+
}
|
|
11480
|
+
function registerReplayFunctions(sdk, kv) {
|
|
11481
|
+
sdk.registerFunction("mem::replay::load", async (data) => {
|
|
11482
|
+
if (!data?.sessionId || typeof data.sessionId !== "string") return {
|
|
11483
|
+
success: false,
|
|
11484
|
+
error: "sessionId is required"
|
|
11485
|
+
};
|
|
11486
|
+
const session = await kv.get(KV.sessions, data.sessionId);
|
|
11487
|
+
return {
|
|
11488
|
+
success: true,
|
|
11489
|
+
timeline: projectTimeline(await loadObservations(kv, data.sessionId)),
|
|
11490
|
+
session
|
|
11491
|
+
};
|
|
11492
|
+
});
|
|
11493
|
+
sdk.registerFunction("mem::replay::sessions", async () => {
|
|
11494
|
+
const sessions = await kv.list(KV.sessions);
|
|
11495
|
+
sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
|
|
11496
|
+
return {
|
|
11497
|
+
success: true,
|
|
11498
|
+
sessions
|
|
11499
|
+
};
|
|
11500
|
+
});
|
|
11501
|
+
sdk.registerFunction("mem::replay::import-jsonl", async (data = {}) => {
|
|
11502
|
+
const defaultRoot = join(homedir(), ".claude", "projects");
|
|
11503
|
+
const rawPath = data.path || defaultRoot;
|
|
11504
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) return {
|
|
11505
|
+
success: false,
|
|
11506
|
+
error: "path must be a non-empty string"
|
|
11507
|
+
};
|
|
11508
|
+
const abs = resolve(rawPath.startsWith("~") ? join(homedir(), rawPath.slice(1)) : rawPath);
|
|
11509
|
+
if (isSensitive(abs)) return {
|
|
11510
|
+
success: false,
|
|
11511
|
+
error: "refusing to process sensitive-looking path"
|
|
11512
|
+
};
|
|
11513
|
+
if (await isSymlink(abs)) return {
|
|
11514
|
+
success: false,
|
|
11515
|
+
error: "symlinks are not supported"
|
|
11516
|
+
};
|
|
11517
|
+
let stat;
|
|
11518
|
+
try {
|
|
11519
|
+
stat = await lstat(abs);
|
|
11520
|
+
} catch {
|
|
11521
|
+
return {
|
|
11522
|
+
success: false,
|
|
11523
|
+
error: "path not found"
|
|
11524
|
+
};
|
|
11525
|
+
}
|
|
11526
|
+
let files = [];
|
|
11527
|
+
if (stat.isDirectory()) files = await findJsonlFiles(abs, data.maxFiles || 200);
|
|
11528
|
+
else if (stat.isFile() && abs.endsWith(".jsonl")) files = [abs];
|
|
11529
|
+
else return {
|
|
11530
|
+
success: false,
|
|
11531
|
+
error: "path must be a .jsonl file or directory"
|
|
11532
|
+
};
|
|
11533
|
+
if (files.length === 0) return {
|
|
11534
|
+
success: true,
|
|
11535
|
+
imported: 0,
|
|
11536
|
+
sessionIds: [],
|
|
11537
|
+
observations: 0
|
|
11538
|
+
};
|
|
11539
|
+
const sessionIds = [];
|
|
11540
|
+
let observationCount = 0;
|
|
11541
|
+
for (const file of files) {
|
|
11542
|
+
if (isSensitive(file)) continue;
|
|
11543
|
+
if (await isSymlink(file)) continue;
|
|
11544
|
+
let text;
|
|
11545
|
+
try {
|
|
11546
|
+
text = await readFile(file, "utf-8");
|
|
11547
|
+
} catch (err) {
|
|
11548
|
+
logger.warn("replay: failed to read jsonl", {
|
|
11549
|
+
file,
|
|
11550
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11551
|
+
});
|
|
11552
|
+
continue;
|
|
11553
|
+
}
|
|
11554
|
+
const parsed = parseJsonlText(text, generateId("sess"));
|
|
11555
|
+
if (parsed.observations.length === 0) continue;
|
|
11556
|
+
const existing = await kv.get(KV.sessions, parsed.sessionId);
|
|
11557
|
+
if (existing) {
|
|
11558
|
+
existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
|
|
11559
|
+
if (parsed.endedAt > (existing.endedAt || "")) existing.endedAt = parsed.endedAt;
|
|
11560
|
+
if (existing.status === "active") existing.status = "completed";
|
|
11561
|
+
const existingTags = existing.tags || [];
|
|
11562
|
+
if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
|
|
11563
|
+
await kv.set(KV.sessions, existing.id, existing);
|
|
11564
|
+
} else {
|
|
11565
|
+
const session = {
|
|
11566
|
+
id: parsed.sessionId,
|
|
11567
|
+
project: parsed.project,
|
|
11568
|
+
cwd: parsed.cwd,
|
|
11569
|
+
startedAt: parsed.startedAt,
|
|
11570
|
+
endedAt: parsed.endedAt,
|
|
11571
|
+
status: "completed",
|
|
11572
|
+
observationCount: parsed.observations.length,
|
|
11573
|
+
tags: ["jsonl-import"]
|
|
11574
|
+
};
|
|
11575
|
+
await kv.set(KV.sessions, session.id, session);
|
|
11576
|
+
}
|
|
11577
|
+
await Promise.all(parsed.observations.map((obs) => kv.set(KV.observations(parsed.sessionId), obs.id, obs)));
|
|
11578
|
+
observationCount += parsed.observations.length;
|
|
11579
|
+
sessionIds.push(parsed.sessionId);
|
|
11580
|
+
}
|
|
11581
|
+
await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
|
|
11582
|
+
source: "jsonl",
|
|
11583
|
+
path: abs,
|
|
11584
|
+
files: files.length,
|
|
11585
|
+
observations: observationCount
|
|
11586
|
+
});
|
|
11587
|
+
return {
|
|
11588
|
+
success: true,
|
|
11589
|
+
imported: files.length,
|
|
11590
|
+
sessionIds,
|
|
11591
|
+
observations: observationCount
|
|
11592
|
+
};
|
|
11593
|
+
});
|
|
11594
|
+
}
|
|
11595
|
+
|
|
10935
11596
|
//#endregion
|
|
10936
11597
|
//#region src/health/thresholds.ts
|
|
10937
11598
|
const DEFAULTS = {
|
|
@@ -10940,7 +11601,8 @@ const DEFAULTS = {
|
|
|
10940
11601
|
cpuWarnPercent: 80,
|
|
10941
11602
|
cpuCriticalPercent: 90,
|
|
10942
11603
|
memoryWarnPercent: 80,
|
|
10943
|
-
memoryCriticalPercent: 95
|
|
11604
|
+
memoryCriticalPercent: 95,
|
|
11605
|
+
memoryRssFloorBytes: 512 * 1024 * 1024
|
|
10944
11606
|
};
|
|
10945
11607
|
function evaluateHealth(snapshot, config = {}) {
|
|
10946
11608
|
const cfg = {
|
|
@@ -10972,13 +11634,16 @@ function evaluateHealth(snapshot, config = {}) {
|
|
|
10972
11634
|
degraded = true;
|
|
10973
11635
|
}
|
|
10974
11636
|
const memPercent = snapshot.memory.heapTotal > 0 ? snapshot.memory.heapUsed / snapshot.memory.heapTotal * 100 : 0;
|
|
10975
|
-
|
|
10976
|
-
|
|
11637
|
+
const rss = snapshot.memory.rss ?? 0;
|
|
11638
|
+
const rssAboveFloor = rss >= cfg.memoryRssFloorBytes;
|
|
11639
|
+
const memMb = Math.round(rss / (1024 * 1024));
|
|
11640
|
+
if (memPercent > cfg.memoryCriticalPercent && rssAboveFloor) {
|
|
11641
|
+
alerts.push(`memory_critical_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
10977
11642
|
critical = true;
|
|
10978
|
-
} else if (memPercent > cfg.memoryWarnPercent) {
|
|
10979
|
-
alerts.push(`memory_warn_${Math.round(memPercent)}
|
|
11643
|
+
} else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
|
|
11644
|
+
alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
10980
11645
|
degraded = true;
|
|
10981
|
-
}
|
|
11646
|
+
} else if (memPercent > cfg.memoryWarnPercent) alerts.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
10982
11647
|
return {
|
|
10983
11648
|
status: critical ? "critical" : degraded ? "degraded" : "healthy",
|
|
10984
11649
|
alerts
|
|
@@ -11312,11 +11977,25 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
11312
11977
|
status_code: 400,
|
|
11313
11978
|
body: { error: "cwd must be a string" }
|
|
11314
11979
|
};
|
|
11980
|
+
if (body.format !== void 0 && (typeof body.format !== "string" || ![
|
|
11981
|
+
"full",
|
|
11982
|
+
"compact",
|
|
11983
|
+
"narrative"
|
|
11984
|
+
].includes(body.format.trim().toLowerCase()))) return {
|
|
11985
|
+
status_code: 400,
|
|
11986
|
+
body: { error: "format must be one of: full, compact, narrative" }
|
|
11987
|
+
};
|
|
11988
|
+
if (body.token_budget !== void 0 && (!Number.isInteger(body.token_budget) || body.token_budget < 1)) return {
|
|
11989
|
+
status_code: 400,
|
|
11990
|
+
body: { error: "token_budget must be a positive integer" }
|
|
11991
|
+
};
|
|
11315
11992
|
const payload = {
|
|
11316
11993
|
query: body.query.trim(),
|
|
11317
11994
|
limit: body.limit,
|
|
11318
11995
|
project: body.project,
|
|
11319
|
-
cwd: body.cwd
|
|
11996
|
+
cwd: body.cwd,
|
|
11997
|
+
format: typeof body.format === "string" ? body.format.trim().toLowerCase() : void 0,
|
|
11998
|
+
token_budget: body.token_budget
|
|
11320
11999
|
};
|
|
11321
12000
|
return {
|
|
11322
12001
|
status_code: 200,
|
|
@@ -11335,6 +12014,105 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
11335
12014
|
middleware_function_ids: ["middleware::api-auth"]
|
|
11336
12015
|
}
|
|
11337
12016
|
});
|
|
12017
|
+
sdk.registerFunction("api::compress-file", async (req) => {
|
|
12018
|
+
const authErr = checkAuth(req, secret);
|
|
12019
|
+
if (authErr) return authErr;
|
|
12020
|
+
const filePath = asNonEmptyString$1((req.body ?? {}).filePath);
|
|
12021
|
+
if (!filePath) return {
|
|
12022
|
+
status_code: 400,
|
|
12023
|
+
body: { error: "filePath is required and must be a non-empty string" }
|
|
12024
|
+
};
|
|
12025
|
+
return {
|
|
12026
|
+
status_code: 200,
|
|
12027
|
+
body: await sdk.trigger({
|
|
12028
|
+
function_id: "mem::compress-file",
|
|
12029
|
+
payload: { filePath }
|
|
12030
|
+
})
|
|
12031
|
+
};
|
|
12032
|
+
});
|
|
12033
|
+
sdk.registerTrigger({
|
|
12034
|
+
type: "http",
|
|
12035
|
+
function_id: "api::compress-file",
|
|
12036
|
+
config: {
|
|
12037
|
+
api_path: "/agentmemory/compress-file",
|
|
12038
|
+
http_method: "POST"
|
|
12039
|
+
}
|
|
12040
|
+
});
|
|
12041
|
+
sdk.registerFunction("api::replay::load", async (req) => {
|
|
12042
|
+
const authErr = checkAuth(req, secret);
|
|
12043
|
+
if (authErr) return authErr;
|
|
12044
|
+
const sessionId = asNonEmptyString$1(req.query_params?.["sessionId"]);
|
|
12045
|
+
if (!sessionId) return {
|
|
12046
|
+
status_code: 400,
|
|
12047
|
+
body: { error: "sessionId is required" }
|
|
12048
|
+
};
|
|
12049
|
+
return {
|
|
12050
|
+
status_code: 200,
|
|
12051
|
+
body: await sdk.trigger({
|
|
12052
|
+
function_id: "mem::replay::load",
|
|
12053
|
+
payload: { sessionId }
|
|
12054
|
+
})
|
|
12055
|
+
};
|
|
12056
|
+
});
|
|
12057
|
+
sdk.registerTrigger({
|
|
12058
|
+
type: "http",
|
|
12059
|
+
function_id: "api::replay::load",
|
|
12060
|
+
config: {
|
|
12061
|
+
api_path: "/agentmemory/replay/load",
|
|
12062
|
+
http_method: "GET"
|
|
12063
|
+
}
|
|
12064
|
+
});
|
|
12065
|
+
sdk.registerFunction("api::replay::sessions", async (req) => {
|
|
12066
|
+
const authErr = checkAuth(req, secret);
|
|
12067
|
+
if (authErr) return authErr;
|
|
12068
|
+
return {
|
|
12069
|
+
status_code: 200,
|
|
12070
|
+
body: await sdk.trigger({ function_id: "mem::replay::sessions" })
|
|
12071
|
+
};
|
|
12072
|
+
});
|
|
12073
|
+
sdk.registerTrigger({
|
|
12074
|
+
type: "http",
|
|
12075
|
+
function_id: "api::replay::sessions",
|
|
12076
|
+
config: {
|
|
12077
|
+
api_path: "/agentmemory/replay/sessions",
|
|
12078
|
+
http_method: "GET"
|
|
12079
|
+
}
|
|
12080
|
+
});
|
|
12081
|
+
sdk.registerFunction("api::replay::import", async (req) => {
|
|
12082
|
+
const authErr = checkAuth(req, secret);
|
|
12083
|
+
if (authErr) return authErr;
|
|
12084
|
+
const body = req.body ?? {};
|
|
12085
|
+
const payload = {};
|
|
12086
|
+
if (body.path !== void 0) {
|
|
12087
|
+
if (typeof body.path !== "string" || body.path.trim().length === 0) return {
|
|
12088
|
+
status_code: 400,
|
|
12089
|
+
body: { error: "path must be a non-empty string" }
|
|
12090
|
+
};
|
|
12091
|
+
payload.path = body.path.trim();
|
|
12092
|
+
}
|
|
12093
|
+
if (body.maxFiles !== void 0) {
|
|
12094
|
+
if (!Number.isInteger(body.maxFiles) || body.maxFiles < 1) return {
|
|
12095
|
+
status_code: 400,
|
|
12096
|
+
body: { error: "maxFiles must be a positive integer" }
|
|
12097
|
+
};
|
|
12098
|
+
payload.maxFiles = body.maxFiles;
|
|
12099
|
+
}
|
|
12100
|
+
return {
|
|
12101
|
+
status_code: 202,
|
|
12102
|
+
body: await sdk.trigger({
|
|
12103
|
+
function_id: "mem::replay::import-jsonl",
|
|
12104
|
+
payload
|
|
12105
|
+
})
|
|
12106
|
+
};
|
|
12107
|
+
});
|
|
12108
|
+
sdk.registerTrigger({
|
|
12109
|
+
type: "http",
|
|
12110
|
+
function_id: "api::replay::import",
|
|
12111
|
+
config: {
|
|
12112
|
+
api_path: "/agentmemory/replay/import-jsonl",
|
|
12113
|
+
http_method: "POST"
|
|
12114
|
+
}
|
|
12115
|
+
});
|
|
11338
12116
|
sdk.registerFunction("api::session::start", async (req) => {
|
|
11339
12117
|
const body = req.body ?? {};
|
|
11340
12118
|
const sessionId = asNonEmptyString$1(body.sessionId);
|
|
@@ -13799,11 +14577,31 @@ const CORE_TOOLS = [
|
|
|
13799
14577
|
limit: {
|
|
13800
14578
|
type: "number",
|
|
13801
14579
|
description: "Max results to return (default 10)"
|
|
14580
|
+
},
|
|
14581
|
+
format: {
|
|
14582
|
+
type: "string",
|
|
14583
|
+
description: "Result format: full, compact, or narrative (default full)"
|
|
14584
|
+
},
|
|
14585
|
+
token_budget: {
|
|
14586
|
+
type: "number",
|
|
14587
|
+
description: "Optional token budget to trim returned results"
|
|
13802
14588
|
}
|
|
13803
14589
|
},
|
|
13804
14590
|
required: ["query"]
|
|
13805
14591
|
}
|
|
13806
14592
|
},
|
|
14593
|
+
{
|
|
14594
|
+
name: "memory_compress_file",
|
|
14595
|
+
description: "Compress a markdown file to reduce token usage while preserving headings, URLs, and code blocks. Creates a .original.md backup before writing.",
|
|
14596
|
+
inputSchema: {
|
|
14597
|
+
type: "object",
|
|
14598
|
+
properties: { filePath: {
|
|
14599
|
+
type: "string",
|
|
14600
|
+
description: "Path to the markdown file to compress"
|
|
14601
|
+
} },
|
|
14602
|
+
required: ["filePath"]
|
|
14603
|
+
}
|
|
14604
|
+
},
|
|
13807
14605
|
{
|
|
13808
14606
|
name: "memory_save",
|
|
13809
14607
|
description: "Explicitly save an important insight, decision, or pattern to long-term memory.",
|
|
@@ -14758,13 +15556,46 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
14758
15556
|
status_code: 400,
|
|
14759
15557
|
body: { error: "query is required for memory_recall" }
|
|
14760
15558
|
};
|
|
15559
|
+
const format = typeof args.format === "string" ? args.format.trim().toLowerCase() : "full";
|
|
15560
|
+
if (![
|
|
15561
|
+
"full",
|
|
15562
|
+
"compact",
|
|
15563
|
+
"narrative"
|
|
15564
|
+
].includes(format)) return {
|
|
15565
|
+
status_code: 400,
|
|
15566
|
+
body: { error: "format must be one of: full, compact, narrative" }
|
|
15567
|
+
};
|
|
15568
|
+
const tokenBudget = asNumber(args.token_budget);
|
|
15569
|
+
if (args.token_budget !== void 0 && (!Number.isInteger(tokenBudget) || (tokenBudget ?? 0) < 1)) return {
|
|
15570
|
+
status_code: 400,
|
|
15571
|
+
body: { error: "token_budget must be a positive integer" }
|
|
15572
|
+
};
|
|
14761
15573
|
const result = await sdk.trigger({
|
|
14762
15574
|
function_id: "mem::search",
|
|
14763
15575
|
payload: {
|
|
14764
15576
|
query: args.query,
|
|
14765
|
-
limit: typeof args.limit === "number" ? args.limit : 10
|
|
15577
|
+
limit: typeof args.limit === "number" ? args.limit : 10,
|
|
15578
|
+
format,
|
|
15579
|
+
token_budget: tokenBudget
|
|
14766
15580
|
}
|
|
14767
15581
|
});
|
|
15582
|
+
return {
|
|
15583
|
+
status_code: 200,
|
|
15584
|
+
body: { content: [{
|
|
15585
|
+
type: "text",
|
|
15586
|
+
text: format === "narrative" && result && typeof result === "object" && "text" in result && typeof result.text === "string" ? result.text : JSON.stringify(result, null, 2)
|
|
15587
|
+
}] }
|
|
15588
|
+
};
|
|
15589
|
+
}
|
|
15590
|
+
case "memory_compress_file": {
|
|
15591
|
+
if (typeof args.filePath !== "string" || !args.filePath.trim()) return {
|
|
15592
|
+
status_code: 400,
|
|
15593
|
+
body: { error: "filePath is required for memory_compress_file" }
|
|
15594
|
+
};
|
|
15595
|
+
const result = await sdk.trigger({
|
|
15596
|
+
function_id: "mem::compress-file",
|
|
15597
|
+
payload: { filePath: args.filePath.trim() }
|
|
15598
|
+
});
|
|
14768
15599
|
return {
|
|
14769
15600
|
status_code: 200,
|
|
14770
15601
|
body: { content: [{
|
|
@@ -16472,6 +17303,8 @@ async function main() {
|
|
|
16472
17303
|
registerQueryExpansionFunction(sdk, provider);
|
|
16473
17304
|
registerTemporalGraphFunctions(sdk, kv, provider);
|
|
16474
17305
|
registerRetentionFunctions(sdk, kv);
|
|
17306
|
+
registerCompressFileFunction(sdk, kv, provider);
|
|
17307
|
+
registerReplayFunctions(sdk, kv);
|
|
16475
17308
|
console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
|
|
16476
17309
|
console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
|
|
16477
17310
|
const snapshotConfig = loadSnapshotConfig();
|
|
@@ -16511,7 +17344,7 @@ async function main() {
|
|
|
16511
17344
|
}
|
|
16512
17345
|
}
|
|
16513
17346
|
console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
16514
|
-
console.log(`[agentmemory] Endpoints:
|
|
17347
|
+
console.log(`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
|
|
16515
17348
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
16516
17349
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|
|
16517
17350
|
const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);
|