@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./cli.mjs";
|
|
2
|
-
import { a as getEnvVar, c as isConsolidationEnabled, d as loadClaudeBridgeConfig, f as loadConfig, g as loadTeamConfig, h as loadSnapshotConfig, i as getConsolidationDecayDays, l as isContextInjectionEnabled, m as loadFallbackConfig, n as VERSION, p as loadEmbeddingConfig, r as detectEmbeddingProvider, s as isAutoCompressEnabled, t as getVisibleTools, u as isGraphExtractionEnabled } from "./tools-registry-
|
|
2
|
+
import { a as getEnvVar, c as isConsolidationEnabled, d as loadClaudeBridgeConfig, f as loadConfig, g as loadTeamConfig, h as loadSnapshotConfig, i as getConsolidationDecayDays, l as isContextInjectionEnabled, m as loadFallbackConfig, n as VERSION, p as loadEmbeddingConfig, r as detectEmbeddingProvider, s as isAutoCompressEnabled, t as getVisibleTools, u as isGraphExtractionEnabled } from "./tools-registry-DXIK5CxQ.mjs";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -12,7 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
import { promisify } from "node:util";
|
|
13
13
|
import { lookup } from "node:dns/promises";
|
|
14
14
|
import { isIP } from "node:net";
|
|
15
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
15
|
+
import { lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
|
|
16
16
|
import { createServer } from "node:http";
|
|
17
17
|
|
|
18
18
|
//#region src/providers/agent-sdk.ts
|
|
@@ -564,7 +564,11 @@ function createBaseProvider(config) {
|
|
|
564
564
|
switch (config.provider) {
|
|
565
565
|
case "minimax": return new MinimaxProvider(requireEnvVar("MINIMAX_API_KEY"), config.model, config.maxTokens);
|
|
566
566
|
case "anthropic": return new AnthropicProvider(requireEnvVar("ANTHROPIC_API_KEY"), config.model, config.maxTokens, config.baseURL);
|
|
567
|
-
case "gemini":
|
|
567
|
+
case "gemini": {
|
|
568
|
+
const geminiKey = getEnvVar("GEMINI_API_KEY") || getEnvVar("GOOGLE_API_KEY");
|
|
569
|
+
if (!geminiKey) throw new Error("GEMINI_API_KEY (or GOOGLE_API_KEY) is required for the gemini provider");
|
|
570
|
+
return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
|
|
571
|
+
}
|
|
568
572
|
case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
|
|
569
573
|
default: return new AgentSDKProvider();
|
|
570
574
|
}
|
|
@@ -1895,7 +1899,7 @@ function stringifyForNarrative(v) {
|
|
|
1895
1899
|
return String(v);
|
|
1896
1900
|
}
|
|
1897
1901
|
}
|
|
1898
|
-
function truncate$
|
|
1902
|
+
function truncate$2(s, n) {
|
|
1899
1903
|
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
1900
1904
|
}
|
|
1901
1905
|
function buildSyntheticCompression(raw) {
|
|
@@ -1912,10 +1916,10 @@ function buildSyntheticCompression(raw) {
|
|
|
1912
1916
|
sessionId: raw.sessionId,
|
|
1913
1917
|
timestamp: raw.timestamp,
|
|
1914
1918
|
type: inferType(toolName, raw.hookType),
|
|
1915
|
-
title: truncate$
|
|
1916
|
-
subtitle: inputStr ? truncate$
|
|
1919
|
+
title: truncate$2(toolName || "observation", 80),
|
|
1920
|
+
subtitle: inputStr ? truncate$2(inputStr, 120) : void 0,
|
|
1917
1921
|
facts: [],
|
|
1918
|
-
narrative: truncate$
|
|
1922
|
+
narrative: truncate$2(narrativeParts.join(" | "), 400),
|
|
1919
1923
|
concepts: [],
|
|
1920
1924
|
files: extractFiles$1(raw.toolInput),
|
|
1921
1925
|
importance: 5,
|
|
@@ -2041,6 +2045,17 @@ function registerSearchFunction(sdk, kv) {
|
|
|
2041
2045
|
}
|
|
2042
2046
|
const projectFilter = typeof data.project === "string" && data.project.length > 0 ? data.project : void 0;
|
|
2043
2047
|
const cwdFilter = typeof data.cwd === "string" && data.cwd.length > 0 ? data.cwd : void 0;
|
|
2048
|
+
const format = typeof data.format === "string" ? data.format : "full";
|
|
2049
|
+
if (![
|
|
2050
|
+
"full",
|
|
2051
|
+
"compact",
|
|
2052
|
+
"narrative"
|
|
2053
|
+
].includes(format)) throw new Error("mem::search: format must be one of 'full', 'compact', or 'narrative'");
|
|
2054
|
+
let tokenBudget;
|
|
2055
|
+
if (data.token_budget !== void 0) {
|
|
2056
|
+
if (!Number.isInteger(data.token_budget) || data.token_budget < 1) throw new Error("mem::search: token_budget must be a positive integer");
|
|
2057
|
+
tokenBudget = data.token_budget;
|
|
2058
|
+
}
|
|
2044
2059
|
if (idx.size === 0) {
|
|
2045
2060
|
const count = await rebuildIndex(kv);
|
|
2046
2061
|
logger.info("Search index rebuilt", { entries: count });
|
|
@@ -2077,13 +2092,81 @@ function registerSearchFunction(sdk, kv) {
|
|
|
2077
2092
|
});
|
|
2078
2093
|
}
|
|
2079
2094
|
recordAccessBatch(kv, enriched.map((r) => r.observation.id));
|
|
2095
|
+
const estimateTokens = (value) => Math.max(1, Math.ceil(JSON.stringify(value).length / 3));
|
|
2096
|
+
const applyTokenBudget = (items) => {
|
|
2097
|
+
if (!tokenBudget) return {
|
|
2098
|
+
items,
|
|
2099
|
+
used: items.reduce((sum, item) => sum + estimateTokens(item), 0),
|
|
2100
|
+
truncated: false
|
|
2101
|
+
};
|
|
2102
|
+
const selected = [];
|
|
2103
|
+
let used = 0;
|
|
2104
|
+
for (const item of items) {
|
|
2105
|
+
const itemTokens = estimateTokens(item);
|
|
2106
|
+
if (used + itemTokens > tokenBudget) return {
|
|
2107
|
+
items: selected,
|
|
2108
|
+
used,
|
|
2109
|
+
truncated: selected.length < items.length
|
|
2110
|
+
};
|
|
2111
|
+
selected.push(item);
|
|
2112
|
+
used += itemTokens;
|
|
2113
|
+
}
|
|
2114
|
+
return {
|
|
2115
|
+
items: selected,
|
|
2116
|
+
used,
|
|
2117
|
+
truncated: false
|
|
2118
|
+
};
|
|
2119
|
+
};
|
|
2120
|
+
if (format === "compact") {
|
|
2121
|
+
const packed = applyTokenBudget(enriched.map((r) => ({
|
|
2122
|
+
obsId: r.observation.id,
|
|
2123
|
+
sessionId: r.sessionId,
|
|
2124
|
+
title: r.observation.title,
|
|
2125
|
+
type: r.observation.type,
|
|
2126
|
+
score: r.score,
|
|
2127
|
+
timestamp: r.observation.timestamp
|
|
2128
|
+
})));
|
|
2129
|
+
return {
|
|
2130
|
+
format,
|
|
2131
|
+
results: packed.items,
|
|
2132
|
+
tokens_used: packed.used,
|
|
2133
|
+
tokens_budget: tokenBudget,
|
|
2134
|
+
truncated: packed.truncated
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
if (format === "narrative") {
|
|
2138
|
+
const packed = applyTokenBudget(enriched.map((r) => ({
|
|
2139
|
+
obsId: r.observation.id,
|
|
2140
|
+
sessionId: r.sessionId,
|
|
2141
|
+
title: r.observation.title,
|
|
2142
|
+
narrative: r.observation.narrative,
|
|
2143
|
+
score: r.score,
|
|
2144
|
+
timestamp: r.observation.timestamp
|
|
2145
|
+
})));
|
|
2146
|
+
const text = packed.items.map((r, index) => `${index + 1}. ${r.title}\n${r.narrative}`).join("\n\n");
|
|
2147
|
+
return {
|
|
2148
|
+
format,
|
|
2149
|
+
results: packed.items,
|
|
2150
|
+
text,
|
|
2151
|
+
tokens_used: packed.used,
|
|
2152
|
+
tokens_budget: tokenBudget,
|
|
2153
|
+
truncated: packed.truncated
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
const packed = applyTokenBudget(enriched);
|
|
2080
2157
|
logger.info("Search completed", {
|
|
2081
2158
|
query,
|
|
2082
|
-
results:
|
|
2159
|
+
results: packed.items.length,
|
|
2083
2160
|
hasProjectFilter: !!projectFilter,
|
|
2084
2161
|
hasCwdFilter: !!cwdFilter
|
|
2085
2162
|
});
|
|
2086
|
-
return {
|
|
2163
|
+
return {
|
|
2164
|
+
format,
|
|
2165
|
+
results: packed.items,
|
|
2166
|
+
tokens_used: packed.used,
|
|
2167
|
+
tokens_budget: tokenBudget,
|
|
2168
|
+
truncated: packed.truncated
|
|
2169
|
+
};
|
|
2087
2170
|
});
|
|
2088
2171
|
}
|
|
2089
2172
|
|
|
@@ -2260,16 +2343,16 @@ function buildCompressionPrompt(observation) {
|
|
|
2260
2343
|
if (observation.toolName) parts.push(`Tool: ${observation.toolName}`);
|
|
2261
2344
|
if (observation.toolInput) {
|
|
2262
2345
|
const input = typeof observation.toolInput === "string" ? observation.toolInput : JSON.stringify(observation.toolInput, null, 2);
|
|
2263
|
-
parts.push(`Input:\n${truncate(input, 4e3)}`);
|
|
2346
|
+
parts.push(`Input:\n${truncate$1(input, 4e3)}`);
|
|
2264
2347
|
}
|
|
2265
2348
|
if (observation.toolOutput) {
|
|
2266
2349
|
const output = typeof observation.toolOutput === "string" ? observation.toolOutput : JSON.stringify(observation.toolOutput, null, 2);
|
|
2267
|
-
parts.push(`Output:\n${truncate(output, 4e3)}`);
|
|
2350
|
+
parts.push(`Output:\n${truncate$1(output, 4e3)}`);
|
|
2268
2351
|
}
|
|
2269
|
-
if (observation.userPrompt) parts.push(`User prompt:\n${truncate(observation.userPrompt, 2e3)}`);
|
|
2352
|
+
if (observation.userPrompt) parts.push(`User prompt:\n${truncate$1(observation.userPrompt, 2e3)}`);
|
|
2270
2353
|
return parts.join("\n\n");
|
|
2271
2354
|
}
|
|
2272
|
-
function truncate(s, max) {
|
|
2355
|
+
function truncate$1(s, max) {
|
|
2273
2356
|
return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
|
|
2274
2357
|
}
|
|
2275
2358
|
|
|
@@ -3434,25 +3517,40 @@ function registerRememberFunction(sdk, kv) {
|
|
|
3434
3517
|
});
|
|
3435
3518
|
sdk.registerFunction("mem::forget", async (data) => {
|
|
3436
3519
|
let deleted = 0;
|
|
3520
|
+
const deletedMemoryIds = [];
|
|
3521
|
+
const deletedObservationIds = [];
|
|
3522
|
+
let deletedSession = false;
|
|
3437
3523
|
if (data.memoryId) {
|
|
3438
3524
|
await kv.delete(KV.memories, data.memoryId);
|
|
3439
3525
|
await deleteAccessLog(kv, data.memoryId);
|
|
3526
|
+
deletedMemoryIds.push(data.memoryId);
|
|
3440
3527
|
deleted++;
|
|
3441
3528
|
}
|
|
3442
3529
|
if (data.sessionId && data.observationIds && data.observationIds.length > 0) for (const obsId of data.observationIds) {
|
|
3443
3530
|
await kv.delete(KV.observations(data.sessionId), obsId);
|
|
3531
|
+
deletedObservationIds.push(obsId);
|
|
3444
3532
|
deleted++;
|
|
3445
3533
|
}
|
|
3446
3534
|
if (data.sessionId && (!data.observationIds || data.observationIds.length === 0) && !data.memoryId) {
|
|
3447
3535
|
const observations = await kv.list(KV.observations(data.sessionId));
|
|
3448
3536
|
for (const obs of observations) {
|
|
3449
3537
|
await kv.delete(KV.observations(data.sessionId), obs.id);
|
|
3538
|
+
deletedObservationIds.push(obs.id);
|
|
3450
3539
|
deleted++;
|
|
3451
3540
|
}
|
|
3452
3541
|
await kv.delete(KV.sessions, data.sessionId);
|
|
3453
3542
|
await kv.delete(KV.summaries, data.sessionId);
|
|
3543
|
+
deletedSession = true;
|
|
3454
3544
|
deleted += 2;
|
|
3455
3545
|
}
|
|
3546
|
+
if (deleted > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
|
|
3547
|
+
sessionId: data.sessionId,
|
|
3548
|
+
deleted,
|
|
3549
|
+
memoriesDeleted: deletedMemoryIds.length,
|
|
3550
|
+
observationsDeleted: deletedObservationIds.length,
|
|
3551
|
+
sessionDeleted: deletedSession,
|
|
3552
|
+
reason: "user-initiated forget"
|
|
3553
|
+
});
|
|
3456
3554
|
logger.info("Memory forgotten", { deleted });
|
|
3457
3555
|
return {
|
|
3458
3556
|
success: true,
|
|
@@ -4283,7 +4381,10 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
4283
4381
|
"0.8.8",
|
|
4284
4382
|
"0.8.9",
|
|
4285
4383
|
"0.8.10",
|
|
4286
|
-
"0.8.11"
|
|
4384
|
+
"0.8.11",
|
|
4385
|
+
"0.8.12",
|
|
4386
|
+
"0.8.13",
|
|
4387
|
+
"0.9.0"
|
|
4287
4388
|
]).has(importData.version)) return {
|
|
4288
4389
|
success: false,
|
|
4289
4390
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -10655,8 +10756,8 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10655
10756
|
let evictedSemantic = 0;
|
|
10656
10757
|
const evictedIds = [];
|
|
10657
10758
|
for (const candidate of candidates) try {
|
|
10658
|
-
let scope;
|
|
10659
|
-
let resolvedSource;
|
|
10759
|
+
let scope = null;
|
|
10760
|
+
let resolvedSource = null;
|
|
10660
10761
|
if (candidate.source === "semantic") {
|
|
10661
10762
|
scope = KV.semantic;
|
|
10662
10763
|
resolvedSource = "semantic";
|
|
@@ -10666,10 +10767,11 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10666
10767
|
} else if (await kv.get(KV.memories, candidate.memoryId) !== null) {
|
|
10667
10768
|
scope = KV.memories;
|
|
10668
10769
|
resolvedSource = "episodic";
|
|
10669
|
-
} else {
|
|
10770
|
+
} else if (await kv.get(KV.semantic, candidate.memoryId) !== null) {
|
|
10670
10771
|
scope = KV.semantic;
|
|
10671
10772
|
resolvedSource = "semantic";
|
|
10672
10773
|
}
|
|
10774
|
+
if (!scope || !resolvedSource) continue;
|
|
10673
10775
|
await kv.delete(scope, candidate.memoryId);
|
|
10674
10776
|
await kv.delete(KV.retentionScores, candidate.memoryId);
|
|
10675
10777
|
await deleteAccessLog(kv, candidate.memoryId);
|
|
@@ -10702,6 +10804,561 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10702
10804
|
});
|
|
10703
10805
|
}
|
|
10704
10806
|
|
|
10807
|
+
//#endregion
|
|
10808
|
+
//#region src/functions/compress-file.ts
|
|
10809
|
+
const SENSITIVE_PATH_TERMS = [
|
|
10810
|
+
"secret",
|
|
10811
|
+
"credential",
|
|
10812
|
+
"private_key",
|
|
10813
|
+
".env",
|
|
10814
|
+
"id_rsa",
|
|
10815
|
+
"token"
|
|
10816
|
+
];
|
|
10817
|
+
const COMPRESS_FILE_SYSTEM_PROMPT = `You compress markdown while preserving structure.
|
|
10818
|
+
Rules:
|
|
10819
|
+
- Keep all headings exactly as-is.
|
|
10820
|
+
- Keep all URLs exactly as-is.
|
|
10821
|
+
- Keep all fenced code blocks exactly as-is.
|
|
10822
|
+
- Do not remove sections; shorten prose under each section.
|
|
10823
|
+
- Output only markdown, no wrappers or explanations.`;
|
|
10824
|
+
function stripMarkdownFence(text) {
|
|
10825
|
+
const trimmed = text.trim();
|
|
10826
|
+
const match = trimmed.match(/^```(?:markdown|md)?\s*([\s\S]*?)\s*```$/i);
|
|
10827
|
+
return match ? match[1].trim() : trimmed;
|
|
10828
|
+
}
|
|
10829
|
+
function extractUrls(text) {
|
|
10830
|
+
return Array.from(new Set(text.match(/https?:\/\/[^\s)]+/g) || []));
|
|
10831
|
+
}
|
|
10832
|
+
function extractHeadings(text) {
|
|
10833
|
+
return text.split("\n").map((line) => line.trim()).filter((line) => /^#{1,6}\s+/.test(line));
|
|
10834
|
+
}
|
|
10835
|
+
function extractCodeBlocks(text) {
|
|
10836
|
+
return text.match(/```[\s\S]*?```/g) || [];
|
|
10837
|
+
}
|
|
10838
|
+
function validateCompression(original, compressed) {
|
|
10839
|
+
const errors = [];
|
|
10840
|
+
const originalHeadings = extractHeadings(original);
|
|
10841
|
+
const compressedHeadings = extractHeadings(compressed);
|
|
10842
|
+
for (const heading of originalHeadings) if (!compressedHeadings.includes(heading)) errors.push(`missing heading: ${heading}`);
|
|
10843
|
+
const originalUrls = extractUrls(original).sort();
|
|
10844
|
+
const compressedUrls = extractUrls(compressed).sort();
|
|
10845
|
+
if (originalUrls.length !== compressedUrls.length) errors.push("url count changed");
|
|
10846
|
+
else for (let i = 0; i < originalUrls.length; i++) if (originalUrls[i] !== compressedUrls[i]) {
|
|
10847
|
+
errors.push("url set changed");
|
|
10848
|
+
break;
|
|
10849
|
+
}
|
|
10850
|
+
const originalBlocks = extractCodeBlocks(original);
|
|
10851
|
+
const compressedBlocks = extractCodeBlocks(compressed);
|
|
10852
|
+
if (originalBlocks.length !== compressedBlocks.length) errors.push("code block count changed");
|
|
10853
|
+
else for (let i = 0; i < originalBlocks.length; i++) if (originalBlocks[i] !== compressedBlocks[i]) {
|
|
10854
|
+
errors.push("code block content changed");
|
|
10855
|
+
break;
|
|
10856
|
+
}
|
|
10857
|
+
return errors;
|
|
10858
|
+
}
|
|
10859
|
+
function resolveBackupPath(filePath) {
|
|
10860
|
+
const base = basename(filePath, extname(filePath));
|
|
10861
|
+
const name = base.endsWith(".original") ? `${base}.backup` : `${base}.original`;
|
|
10862
|
+
return join(dirname(filePath), `${name}.md`);
|
|
10863
|
+
}
|
|
10864
|
+
function registerCompressFileFunction(sdk, kv, provider) {
|
|
10865
|
+
sdk.registerFunction("mem::compress-file", async (data) => {
|
|
10866
|
+
if (!data?.filePath || typeof data.filePath !== "string") return {
|
|
10867
|
+
success: false,
|
|
10868
|
+
error: "filePath is required"
|
|
10869
|
+
};
|
|
10870
|
+
const absolutePath = resolve(data.filePath);
|
|
10871
|
+
const lowerPath = absolutePath.toLowerCase();
|
|
10872
|
+
if (extname(absolutePath).toLowerCase() !== ".md") return {
|
|
10873
|
+
success: false,
|
|
10874
|
+
error: "filePath must point to a .md file"
|
|
10875
|
+
};
|
|
10876
|
+
if (SENSITIVE_PATH_TERMS.some((term) => lowerPath.includes(term))) return {
|
|
10877
|
+
success: false,
|
|
10878
|
+
error: "refusing to process sensitive-looking path"
|
|
10879
|
+
};
|
|
10880
|
+
try {
|
|
10881
|
+
if ((await lstat(absolutePath)).isSymbolicLink()) return {
|
|
10882
|
+
success: false,
|
|
10883
|
+
error: "symlinks are not supported"
|
|
10884
|
+
};
|
|
10885
|
+
} catch {
|
|
10886
|
+
return {
|
|
10887
|
+
success: false,
|
|
10888
|
+
error: "file not found"
|
|
10889
|
+
};
|
|
10890
|
+
}
|
|
10891
|
+
let original;
|
|
10892
|
+
try {
|
|
10893
|
+
original = await readFile(absolutePath, "utf-8");
|
|
10894
|
+
} catch {
|
|
10895
|
+
return {
|
|
10896
|
+
success: false,
|
|
10897
|
+
error: "failed to read file"
|
|
10898
|
+
};
|
|
10899
|
+
}
|
|
10900
|
+
if (!original.trim()) return {
|
|
10901
|
+
success: true,
|
|
10902
|
+
skipped: true,
|
|
10903
|
+
reason: "file is empty"
|
|
10904
|
+
};
|
|
10905
|
+
const compressed = stripMarkdownFence(await provider.summarize(COMPRESS_FILE_SYSTEM_PROMPT, `Compress this markdown file while preserving structure and code blocks:\n\n${original}`));
|
|
10906
|
+
const validationErrors = validateCompression(original, compressed);
|
|
10907
|
+
if (validationErrors.length > 0) return {
|
|
10908
|
+
success: false,
|
|
10909
|
+
error: "compression validation failed",
|
|
10910
|
+
details: validationErrors
|
|
10911
|
+
};
|
|
10912
|
+
const backupPath = resolveBackupPath(absolutePath);
|
|
10913
|
+
await writeFile(backupPath, original, "utf-8");
|
|
10914
|
+
let fd = null;
|
|
10915
|
+
try {
|
|
10916
|
+
fd = await open(absolutePath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW);
|
|
10917
|
+
await fd.writeFile(compressed, "utf-8");
|
|
10918
|
+
} catch (err) {
|
|
10919
|
+
const code = err.code;
|
|
10920
|
+
if (code === "ELOOP" || code === "EINVAL") return {
|
|
10921
|
+
success: false,
|
|
10922
|
+
error: "symlinks are not supported"
|
|
10923
|
+
};
|
|
10924
|
+
return {
|
|
10925
|
+
success: false,
|
|
10926
|
+
error: "failed to write compressed file"
|
|
10927
|
+
};
|
|
10928
|
+
} finally {
|
|
10929
|
+
await fd?.close().catch(() => {});
|
|
10930
|
+
}
|
|
10931
|
+
try {
|
|
10932
|
+
await recordAudit(kv, "compress", "mem::compress-file", [], {
|
|
10933
|
+
filePath: absolutePath,
|
|
10934
|
+
backupPath,
|
|
10935
|
+
originalChars: original.length,
|
|
10936
|
+
compressedChars: compressed.length
|
|
10937
|
+
});
|
|
10938
|
+
} catch {}
|
|
10939
|
+
return {
|
|
10940
|
+
success: true,
|
|
10941
|
+
filePath: absolutePath,
|
|
10942
|
+
backupPath,
|
|
10943
|
+
originalChars: original.length,
|
|
10944
|
+
compressedChars: compressed.length
|
|
10945
|
+
};
|
|
10946
|
+
});
|
|
10947
|
+
}
|
|
10948
|
+
|
|
10949
|
+
//#endregion
|
|
10950
|
+
//#region src/replay/jsonl-parser.ts
|
|
10951
|
+
function deriveProject(cwd) {
|
|
10952
|
+
if (!cwd) return "unknown";
|
|
10953
|
+
const parts = cwd.split("/").filter(Boolean);
|
|
10954
|
+
return parts[parts.length - 1] || "unknown";
|
|
10955
|
+
}
|
|
10956
|
+
function toText(content) {
|
|
10957
|
+
if (typeof content === "string") return content;
|
|
10958
|
+
if (!Array.isArray(content)) return "";
|
|
10959
|
+
const parts = [];
|
|
10960
|
+
for (const item of content) {
|
|
10961
|
+
if (!item || typeof item !== "object") continue;
|
|
10962
|
+
const entry = item;
|
|
10963
|
+
if (entry.type === "text" && typeof entry.text === "string") parts.push(entry.text);
|
|
10964
|
+
}
|
|
10965
|
+
return parts.join("\n");
|
|
10966
|
+
}
|
|
10967
|
+
function extractToolUses(content) {
|
|
10968
|
+
if (!Array.isArray(content)) return [];
|
|
10969
|
+
const out = [];
|
|
10970
|
+
for (const item of content) {
|
|
10971
|
+
if (!item || typeof item !== "object") continue;
|
|
10972
|
+
const entry = item;
|
|
10973
|
+
if (entry.type === "tool_use") out.push({
|
|
10974
|
+
id: typeof entry.id === "string" ? entry.id : "",
|
|
10975
|
+
name: typeof entry.name === "string" ? entry.name : "unknown",
|
|
10976
|
+
input: entry.input
|
|
10977
|
+
});
|
|
10978
|
+
}
|
|
10979
|
+
return out;
|
|
10980
|
+
}
|
|
10981
|
+
function extractToolResults(content) {
|
|
10982
|
+
if (!Array.isArray(content)) return [];
|
|
10983
|
+
const out = [];
|
|
10984
|
+
for (const item of content) {
|
|
10985
|
+
if (!item || typeof item !== "object") continue;
|
|
10986
|
+
const entry = item;
|
|
10987
|
+
if (entry.type === "tool_result") out.push({
|
|
10988
|
+
toolUseId: typeof entry.tool_use_id === "string" ? entry.tool_use_id : "",
|
|
10989
|
+
output: entry.content,
|
|
10990
|
+
isError: entry.is_error === true
|
|
10991
|
+
});
|
|
10992
|
+
}
|
|
10993
|
+
return out;
|
|
10994
|
+
}
|
|
10995
|
+
function parseJsonlText(text, fallbackSessionId) {
|
|
10996
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
10997
|
+
const entries = [];
|
|
10998
|
+
for (const line of lines) try {
|
|
10999
|
+
const parsed = JSON.parse(line);
|
|
11000
|
+
if (parsed && typeof parsed === "object") entries.push(parsed);
|
|
11001
|
+
} catch {}
|
|
11002
|
+
let sessionId = fallbackSessionId || "";
|
|
11003
|
+
let cwd = "";
|
|
11004
|
+
let firstTs = "";
|
|
11005
|
+
let lastTs = "";
|
|
11006
|
+
const observations = [];
|
|
11007
|
+
for (const entry of entries) {
|
|
11008
|
+
if (entry.sessionId && !sessionId) sessionId = entry.sessionId;
|
|
11009
|
+
if (entry.cwd && !cwd) cwd = entry.cwd;
|
|
11010
|
+
const ts = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
11011
|
+
if (!firstTs) firstTs = ts;
|
|
11012
|
+
lastTs = ts;
|
|
11013
|
+
const role = entry.message?.role;
|
|
11014
|
+
const content = entry.message?.content;
|
|
11015
|
+
if (entry.type === "user" && role === "user") {
|
|
11016
|
+
const toolResults = extractToolResults(content);
|
|
11017
|
+
if (toolResults.length > 0) for (const result of toolResults) observations.push({
|
|
11018
|
+
id: generateId("obs"),
|
|
11019
|
+
sessionId: sessionId || "imported",
|
|
11020
|
+
timestamp: ts,
|
|
11021
|
+
hookType: result.isError ? "post_tool_failure" : "post_tool_use",
|
|
11022
|
+
toolName: void 0,
|
|
11023
|
+
toolInput: { toolUseId: result.toolUseId },
|
|
11024
|
+
toolOutput: result.output,
|
|
11025
|
+
raw: entry
|
|
11026
|
+
});
|
|
11027
|
+
else {
|
|
11028
|
+
const text = toText(content);
|
|
11029
|
+
if (text.trim().length > 0) observations.push({
|
|
11030
|
+
id: generateId("obs"),
|
|
11031
|
+
sessionId: sessionId || "imported",
|
|
11032
|
+
timestamp: ts,
|
|
11033
|
+
hookType: "prompt_submit",
|
|
11034
|
+
userPrompt: text,
|
|
11035
|
+
raw: entry
|
|
11036
|
+
});
|
|
11037
|
+
}
|
|
11038
|
+
} else if (entry.type === "assistant" && role === "assistant") {
|
|
11039
|
+
const text = toText(content);
|
|
11040
|
+
const tools = extractToolUses(content);
|
|
11041
|
+
if (text.trim().length > 0) observations.push({
|
|
11042
|
+
id: generateId("obs"),
|
|
11043
|
+
sessionId: sessionId || "imported",
|
|
11044
|
+
timestamp: ts,
|
|
11045
|
+
hookType: "stop",
|
|
11046
|
+
assistantResponse: text,
|
|
11047
|
+
raw: entry
|
|
11048
|
+
});
|
|
11049
|
+
for (const tool of tools) observations.push({
|
|
11050
|
+
id: generateId("obs"),
|
|
11051
|
+
sessionId: sessionId || "imported",
|
|
11052
|
+
timestamp: ts,
|
|
11053
|
+
hookType: "pre_tool_use",
|
|
11054
|
+
toolName: tool.name,
|
|
11055
|
+
toolInput: tool.input,
|
|
11056
|
+
raw: {
|
|
11057
|
+
toolUseId: tool.id,
|
|
11058
|
+
entry
|
|
11059
|
+
}
|
|
11060
|
+
});
|
|
11061
|
+
} else if (entry.type === "summary" || entry.type === "system") {}
|
|
11062
|
+
}
|
|
11063
|
+
const effectiveSessionId = sessionId || generateId("sess");
|
|
11064
|
+
for (const obs of observations) if (obs.sessionId === "imported") obs.sessionId = effectiveSessionId;
|
|
11065
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
11066
|
+
return {
|
|
11067
|
+
sessionId: effectiveSessionId,
|
|
11068
|
+
project: deriveProject(cwd),
|
|
11069
|
+
cwd: cwd || process.cwd(),
|
|
11070
|
+
startedAt: firstTs || nowIso,
|
|
11071
|
+
endedAt: lastTs || nowIso,
|
|
11072
|
+
observations
|
|
11073
|
+
};
|
|
11074
|
+
}
|
|
11075
|
+
|
|
11076
|
+
//#endregion
|
|
11077
|
+
//#region src/replay/timeline.ts
|
|
11078
|
+
const DEFAULT_CHARS_PER_SEC = 40;
|
|
11079
|
+
const MIN_EVENT_MS = 300;
|
|
11080
|
+
const MAX_EVENT_MS = 2e4;
|
|
11081
|
+
function kindFromHook(obs) {
|
|
11082
|
+
switch (obs.hookType) {
|
|
11083
|
+
case "session_start": return "session_start";
|
|
11084
|
+
case "session_end": return "session_end";
|
|
11085
|
+
case "prompt_submit": return "prompt";
|
|
11086
|
+
case "stop": return obs.assistantResponse ? "response" : "hook";
|
|
11087
|
+
case "pre_tool_use": return "tool_call";
|
|
11088
|
+
case "post_tool_use": return "tool_result";
|
|
11089
|
+
case "post_tool_failure": return "tool_error";
|
|
11090
|
+
default: return "hook";
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
function labelFor(obs, kind) {
|
|
11094
|
+
switch (kind) {
|
|
11095
|
+
case "prompt": return truncate(obs.userPrompt || "User prompt", 80);
|
|
11096
|
+
case "response": return truncate(obs.assistantResponse || "Assistant response", 80);
|
|
11097
|
+
case "tool_call": return `${obs.toolName || "tool"} ▸ call`;
|
|
11098
|
+
case "tool_result": return `${obs.toolName || "tool"} ▸ result`;
|
|
11099
|
+
case "tool_error": return `${obs.toolName || "tool"} ▸ error`;
|
|
11100
|
+
case "session_start": return "Session start";
|
|
11101
|
+
case "session_end": return "Session end";
|
|
11102
|
+
default: return obs.hookType;
|
|
11103
|
+
}
|
|
11104
|
+
}
|
|
11105
|
+
function truncate(text, max) {
|
|
11106
|
+
if (text.length <= max) return text;
|
|
11107
|
+
return text.slice(0, max - 1) + "…";
|
|
11108
|
+
}
|
|
11109
|
+
function bodyFor(obs, kind) {
|
|
11110
|
+
if (kind === "prompt") return obs.userPrompt;
|
|
11111
|
+
if (kind === "response") return obs.assistantResponse;
|
|
11112
|
+
}
|
|
11113
|
+
function estimateDurationMs(ev) {
|
|
11114
|
+
const chars = (ev.body?.length || 0) + (typeof ev.toolInput === "string" ? ev.toolInput.length : 0) + (typeof ev.toolOutput === "string" ? ev.toolOutput.length : 0);
|
|
11115
|
+
if (chars === 0) return MIN_EVENT_MS;
|
|
11116
|
+
const ms = Math.round(chars / DEFAULT_CHARS_PER_SEC * 1e3);
|
|
11117
|
+
return Math.max(MIN_EVENT_MS, Math.min(MAX_EVENT_MS, ms));
|
|
11118
|
+
}
|
|
11119
|
+
function projectTimeline(observations) {
|
|
11120
|
+
if (observations.length === 0) {
|
|
11121
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11122
|
+
return {
|
|
11123
|
+
sessionId: "",
|
|
11124
|
+
startedAt: now,
|
|
11125
|
+
endedAt: now,
|
|
11126
|
+
totalDurationMs: 0,
|
|
11127
|
+
eventCount: 0,
|
|
11128
|
+
events: []
|
|
11129
|
+
};
|
|
11130
|
+
}
|
|
11131
|
+
const sorted = [...observations].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
11132
|
+
const startedAt = sorted[0].timestamp;
|
|
11133
|
+
const startMs = Date.parse(startedAt);
|
|
11134
|
+
const events = [];
|
|
11135
|
+
let syntheticOffset = 0;
|
|
11136
|
+
const allSameTs = sorted.every((o) => o.timestamp === startedAt);
|
|
11137
|
+
for (const obs of sorted) {
|
|
11138
|
+
const kind = kindFromHook(obs);
|
|
11139
|
+
const body = bodyFor(obs, kind);
|
|
11140
|
+
const obsMs = Date.parse(obs.timestamp);
|
|
11141
|
+
const offsetMs = allSameTs ? syntheticOffset : Number.isFinite(obsMs) && Number.isFinite(startMs) ? Math.max(0, obsMs - startMs) : syntheticOffset;
|
|
11142
|
+
const event = {
|
|
11143
|
+
id: obs.id,
|
|
11144
|
+
sessionId: obs.sessionId,
|
|
11145
|
+
ts: obs.timestamp,
|
|
11146
|
+
offsetMs,
|
|
11147
|
+
durationMs: 0,
|
|
11148
|
+
kind,
|
|
11149
|
+
label: labelFor(obs, kind),
|
|
11150
|
+
body,
|
|
11151
|
+
toolName: obs.toolName,
|
|
11152
|
+
toolInput: obs.toolInput,
|
|
11153
|
+
toolOutput: obs.toolOutput
|
|
11154
|
+
};
|
|
11155
|
+
event.durationMs = estimateDurationMs(event);
|
|
11156
|
+
events.push(event);
|
|
11157
|
+
syntheticOffset += event.durationMs;
|
|
11158
|
+
}
|
|
11159
|
+
const last = events[events.length - 1];
|
|
11160
|
+
const totalDurationMs = last.offsetMs + last.durationMs;
|
|
11161
|
+
return {
|
|
11162
|
+
sessionId: sorted[0].sessionId,
|
|
11163
|
+
startedAt,
|
|
11164
|
+
endedAt: sorted[sorted.length - 1].timestamp,
|
|
11165
|
+
totalDurationMs,
|
|
11166
|
+
eventCount: events.length,
|
|
11167
|
+
events
|
|
11168
|
+
};
|
|
11169
|
+
}
|
|
11170
|
+
|
|
11171
|
+
//#endregion
|
|
11172
|
+
//#region src/functions/replay.ts
|
|
11173
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
11174
|
+
/(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
|
|
11175
|
+
/(^|[\\/_.-])credentials?([\\/_.-]|$)/i,
|
|
11176
|
+
/(^|[\\/_.-])private[_-]?key([\\/_.-]|$)/i,
|
|
11177
|
+
/(^|[\\/])\.env(\.[\w-]+)?$/i,
|
|
11178
|
+
/(^|[\\/_.-])id_rsa([\\/_.-]|$)/i,
|
|
11179
|
+
/(^|[\\/])auth[_-]?token([\\/_.-]|$)/i,
|
|
11180
|
+
/(^|[\\/])bearer[_-]?token([\\/_.-]|$)/i,
|
|
11181
|
+
/(^|[\\/])access[_-]?token([\\/_.-]|$)/i,
|
|
11182
|
+
/(^|[\\/])api[_-]?token([\\/_.-]|$)/i
|
|
11183
|
+
];
|
|
11184
|
+
function isSensitive(path) {
|
|
11185
|
+
return SENSITIVE_PATH_PATTERNS.some((re) => re.test(path));
|
|
11186
|
+
}
|
|
11187
|
+
async function isSymlink(path) {
|
|
11188
|
+
try {
|
|
11189
|
+
return (await lstat(path)).isSymbolicLink();
|
|
11190
|
+
} catch {
|
|
11191
|
+
return false;
|
|
11192
|
+
}
|
|
11193
|
+
}
|
|
11194
|
+
function rawFromCompressed(obs) {
|
|
11195
|
+
return {
|
|
11196
|
+
id: obs.id,
|
|
11197
|
+
sessionId: obs.sessionId,
|
|
11198
|
+
timestamp: obs.timestamp,
|
|
11199
|
+
hookType: "post_tool_use",
|
|
11200
|
+
toolName: void 0,
|
|
11201
|
+
toolInput: void 0,
|
|
11202
|
+
toolOutput: void 0,
|
|
11203
|
+
userPrompt: obs.type === "conversation" ? obs.narrative : void 0,
|
|
11204
|
+
assistantResponse: void 0,
|
|
11205
|
+
raw: {
|
|
11206
|
+
title: obs.title,
|
|
11207
|
+
narrative: obs.narrative,
|
|
11208
|
+
facts: obs.facts
|
|
11209
|
+
}
|
|
11210
|
+
};
|
|
11211
|
+
}
|
|
11212
|
+
function isRawShape(o) {
|
|
11213
|
+
if (!o || typeof o !== "object") return false;
|
|
11214
|
+
return typeof o.hookType === "string";
|
|
11215
|
+
}
|
|
11216
|
+
async function loadObservations(kv, sessionId) {
|
|
11217
|
+
return (await kv.list(KV.observations(sessionId))).map((r) => isRawShape(r) ? r : rawFromCompressed(r));
|
|
11218
|
+
}
|
|
11219
|
+
async function findJsonlFiles(root, limit = 200) {
|
|
11220
|
+
const out = [];
|
|
11221
|
+
async function walk(dir) {
|
|
11222
|
+
if (out.length >= limit) return;
|
|
11223
|
+
let names;
|
|
11224
|
+
try {
|
|
11225
|
+
names = await readdir(dir);
|
|
11226
|
+
} catch {
|
|
11227
|
+
return;
|
|
11228
|
+
}
|
|
11229
|
+
for (const name of names) {
|
|
11230
|
+
if (out.length >= limit) return;
|
|
11231
|
+
const full = join(dir, name);
|
|
11232
|
+
let st;
|
|
11233
|
+
try {
|
|
11234
|
+
st = await lstat(full);
|
|
11235
|
+
} catch {
|
|
11236
|
+
continue;
|
|
11237
|
+
}
|
|
11238
|
+
if (st.isSymbolicLink()) continue;
|
|
11239
|
+
if (st.isDirectory()) await walk(full);
|
|
11240
|
+
else if (st.isFile() && name.endsWith(".jsonl")) out.push(full);
|
|
11241
|
+
}
|
|
11242
|
+
}
|
|
11243
|
+
await walk(root);
|
|
11244
|
+
return out;
|
|
11245
|
+
}
|
|
11246
|
+
function registerReplayFunctions(sdk, kv) {
|
|
11247
|
+
sdk.registerFunction("mem::replay::load", async (data) => {
|
|
11248
|
+
if (!data?.sessionId || typeof data.sessionId !== "string") return {
|
|
11249
|
+
success: false,
|
|
11250
|
+
error: "sessionId is required"
|
|
11251
|
+
};
|
|
11252
|
+
const session = await kv.get(KV.sessions, data.sessionId);
|
|
11253
|
+
return {
|
|
11254
|
+
success: true,
|
|
11255
|
+
timeline: projectTimeline(await loadObservations(kv, data.sessionId)),
|
|
11256
|
+
session
|
|
11257
|
+
};
|
|
11258
|
+
});
|
|
11259
|
+
sdk.registerFunction("mem::replay::sessions", async () => {
|
|
11260
|
+
const sessions = await kv.list(KV.sessions);
|
|
11261
|
+
sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
|
|
11262
|
+
return {
|
|
11263
|
+
success: true,
|
|
11264
|
+
sessions
|
|
11265
|
+
};
|
|
11266
|
+
});
|
|
11267
|
+
sdk.registerFunction("mem::replay::import-jsonl", async (data = {}) => {
|
|
11268
|
+
const defaultRoot = join(homedir(), ".claude", "projects");
|
|
11269
|
+
const rawPath = data.path || defaultRoot;
|
|
11270
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) return {
|
|
11271
|
+
success: false,
|
|
11272
|
+
error: "path must be a non-empty string"
|
|
11273
|
+
};
|
|
11274
|
+
const abs = resolve(rawPath.startsWith("~") ? join(homedir(), rawPath.slice(1)) : rawPath);
|
|
11275
|
+
if (isSensitive(abs)) return {
|
|
11276
|
+
success: false,
|
|
11277
|
+
error: "refusing to process sensitive-looking path"
|
|
11278
|
+
};
|
|
11279
|
+
if (await isSymlink(abs)) return {
|
|
11280
|
+
success: false,
|
|
11281
|
+
error: "symlinks are not supported"
|
|
11282
|
+
};
|
|
11283
|
+
let stat;
|
|
11284
|
+
try {
|
|
11285
|
+
stat = await lstat(abs);
|
|
11286
|
+
} catch {
|
|
11287
|
+
return {
|
|
11288
|
+
success: false,
|
|
11289
|
+
error: "path not found"
|
|
11290
|
+
};
|
|
11291
|
+
}
|
|
11292
|
+
let files = [];
|
|
11293
|
+
if (stat.isDirectory()) files = await findJsonlFiles(abs, data.maxFiles || 200);
|
|
11294
|
+
else if (stat.isFile() && abs.endsWith(".jsonl")) files = [abs];
|
|
11295
|
+
else return {
|
|
11296
|
+
success: false,
|
|
11297
|
+
error: "path must be a .jsonl file or directory"
|
|
11298
|
+
};
|
|
11299
|
+
if (files.length === 0) return {
|
|
11300
|
+
success: true,
|
|
11301
|
+
imported: 0,
|
|
11302
|
+
sessionIds: [],
|
|
11303
|
+
observations: 0
|
|
11304
|
+
};
|
|
11305
|
+
const sessionIds = [];
|
|
11306
|
+
let observationCount = 0;
|
|
11307
|
+
for (const file of files) {
|
|
11308
|
+
if (isSensitive(file)) continue;
|
|
11309
|
+
if (await isSymlink(file)) continue;
|
|
11310
|
+
let text;
|
|
11311
|
+
try {
|
|
11312
|
+
text = await readFile(file, "utf-8");
|
|
11313
|
+
} catch (err) {
|
|
11314
|
+
logger.warn("replay: failed to read jsonl", {
|
|
11315
|
+
file,
|
|
11316
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11317
|
+
});
|
|
11318
|
+
continue;
|
|
11319
|
+
}
|
|
11320
|
+
const parsed = parseJsonlText(text, generateId("sess"));
|
|
11321
|
+
if (parsed.observations.length === 0) continue;
|
|
11322
|
+
const existing = await kv.get(KV.sessions, parsed.sessionId);
|
|
11323
|
+
if (existing) {
|
|
11324
|
+
existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
|
|
11325
|
+
if (parsed.endedAt > (existing.endedAt || "")) existing.endedAt = parsed.endedAt;
|
|
11326
|
+
if (existing.status === "active") existing.status = "completed";
|
|
11327
|
+
const existingTags = existing.tags || [];
|
|
11328
|
+
if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
|
|
11329
|
+
await kv.set(KV.sessions, existing.id, existing);
|
|
11330
|
+
} else {
|
|
11331
|
+
const session = {
|
|
11332
|
+
id: parsed.sessionId,
|
|
11333
|
+
project: parsed.project,
|
|
11334
|
+
cwd: parsed.cwd,
|
|
11335
|
+
startedAt: parsed.startedAt,
|
|
11336
|
+
endedAt: parsed.endedAt,
|
|
11337
|
+
status: "completed",
|
|
11338
|
+
observationCount: parsed.observations.length,
|
|
11339
|
+
tags: ["jsonl-import"]
|
|
11340
|
+
};
|
|
11341
|
+
await kv.set(KV.sessions, session.id, session);
|
|
11342
|
+
}
|
|
11343
|
+
await Promise.all(parsed.observations.map((obs) => kv.set(KV.observations(parsed.sessionId), obs.id, obs)));
|
|
11344
|
+
observationCount += parsed.observations.length;
|
|
11345
|
+
sessionIds.push(parsed.sessionId);
|
|
11346
|
+
}
|
|
11347
|
+
await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
|
|
11348
|
+
source: "jsonl",
|
|
11349
|
+
path: abs,
|
|
11350
|
+
files: files.length,
|
|
11351
|
+
observations: observationCount
|
|
11352
|
+
});
|
|
11353
|
+
return {
|
|
11354
|
+
success: true,
|
|
11355
|
+
imported: files.length,
|
|
11356
|
+
sessionIds,
|
|
11357
|
+
observations: observationCount
|
|
11358
|
+
};
|
|
11359
|
+
});
|
|
11360
|
+
}
|
|
11361
|
+
|
|
10705
11362
|
//#endregion
|
|
10706
11363
|
//#region src/health/thresholds.ts
|
|
10707
11364
|
const DEFAULTS = {
|
|
@@ -10710,7 +11367,8 @@ const DEFAULTS = {
|
|
|
10710
11367
|
cpuWarnPercent: 80,
|
|
10711
11368
|
cpuCriticalPercent: 90,
|
|
10712
11369
|
memoryWarnPercent: 80,
|
|
10713
|
-
memoryCriticalPercent: 95
|
|
11370
|
+
memoryCriticalPercent: 95,
|
|
11371
|
+
memoryRssFloorBytes: 512 * 1024 * 1024
|
|
10714
11372
|
};
|
|
10715
11373
|
function evaluateHealth(snapshot, config = {}) {
|
|
10716
11374
|
const cfg = {
|
|
@@ -10742,13 +11400,16 @@ function evaluateHealth(snapshot, config = {}) {
|
|
|
10742
11400
|
degraded = true;
|
|
10743
11401
|
}
|
|
10744
11402
|
const memPercent = snapshot.memory.heapTotal > 0 ? snapshot.memory.heapUsed / snapshot.memory.heapTotal * 100 : 0;
|
|
10745
|
-
|
|
10746
|
-
|
|
11403
|
+
const rss = snapshot.memory.rss ?? 0;
|
|
11404
|
+
const rssAboveFloor = rss >= cfg.memoryRssFloorBytes;
|
|
11405
|
+
const memMb = Math.round(rss / (1024 * 1024));
|
|
11406
|
+
if (memPercent > cfg.memoryCriticalPercent && rssAboveFloor) {
|
|
11407
|
+
alerts.push(`memory_critical_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
10747
11408
|
critical = true;
|
|
10748
|
-
} else if (memPercent > cfg.memoryWarnPercent) {
|
|
10749
|
-
alerts.push(`memory_warn_${Math.round(memPercent)}
|
|
11409
|
+
} else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
|
|
11410
|
+
alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
10750
11411
|
degraded = true;
|
|
10751
|
-
}
|
|
11412
|
+
} else if (memPercent > cfg.memoryWarnPercent) alerts.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
10752
11413
|
return {
|
|
10753
11414
|
status: critical ? "critical" : degraded ? "degraded" : "healthy",
|
|
10754
11415
|
alerts
|
|
@@ -11082,11 +11743,25 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
11082
11743
|
status_code: 400,
|
|
11083
11744
|
body: { error: "cwd must be a string" }
|
|
11084
11745
|
};
|
|
11746
|
+
if (body.format !== void 0 && (typeof body.format !== "string" || ![
|
|
11747
|
+
"full",
|
|
11748
|
+
"compact",
|
|
11749
|
+
"narrative"
|
|
11750
|
+
].includes(body.format.trim().toLowerCase()))) return {
|
|
11751
|
+
status_code: 400,
|
|
11752
|
+
body: { error: "format must be one of: full, compact, narrative" }
|
|
11753
|
+
};
|
|
11754
|
+
if (body.token_budget !== void 0 && (!Number.isInteger(body.token_budget) || body.token_budget < 1)) return {
|
|
11755
|
+
status_code: 400,
|
|
11756
|
+
body: { error: "token_budget must be a positive integer" }
|
|
11757
|
+
};
|
|
11085
11758
|
const payload = {
|
|
11086
11759
|
query: body.query.trim(),
|
|
11087
11760
|
limit: body.limit,
|
|
11088
11761
|
project: body.project,
|
|
11089
|
-
cwd: body.cwd
|
|
11762
|
+
cwd: body.cwd,
|
|
11763
|
+
format: typeof body.format === "string" ? body.format.trim().toLowerCase() : void 0,
|
|
11764
|
+
token_budget: body.token_budget
|
|
11090
11765
|
};
|
|
11091
11766
|
return {
|
|
11092
11767
|
status_code: 200,
|
|
@@ -11105,6 +11780,105 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
11105
11780
|
middleware_function_ids: ["middleware::api-auth"]
|
|
11106
11781
|
}
|
|
11107
11782
|
});
|
|
11783
|
+
sdk.registerFunction("api::compress-file", async (req) => {
|
|
11784
|
+
const authErr = checkAuth(req, secret);
|
|
11785
|
+
if (authErr) return authErr;
|
|
11786
|
+
const filePath = asNonEmptyString$1((req.body ?? {}).filePath);
|
|
11787
|
+
if (!filePath) return {
|
|
11788
|
+
status_code: 400,
|
|
11789
|
+
body: { error: "filePath is required and must be a non-empty string" }
|
|
11790
|
+
};
|
|
11791
|
+
return {
|
|
11792
|
+
status_code: 200,
|
|
11793
|
+
body: await sdk.trigger({
|
|
11794
|
+
function_id: "mem::compress-file",
|
|
11795
|
+
payload: { filePath }
|
|
11796
|
+
})
|
|
11797
|
+
};
|
|
11798
|
+
});
|
|
11799
|
+
sdk.registerTrigger({
|
|
11800
|
+
type: "http",
|
|
11801
|
+
function_id: "api::compress-file",
|
|
11802
|
+
config: {
|
|
11803
|
+
api_path: "/agentmemory/compress-file",
|
|
11804
|
+
http_method: "POST"
|
|
11805
|
+
}
|
|
11806
|
+
});
|
|
11807
|
+
sdk.registerFunction("api::replay::load", async (req) => {
|
|
11808
|
+
const authErr = checkAuth(req, secret);
|
|
11809
|
+
if (authErr) return authErr;
|
|
11810
|
+
const sessionId = asNonEmptyString$1(req.query_params?.["sessionId"]);
|
|
11811
|
+
if (!sessionId) return {
|
|
11812
|
+
status_code: 400,
|
|
11813
|
+
body: { error: "sessionId is required" }
|
|
11814
|
+
};
|
|
11815
|
+
return {
|
|
11816
|
+
status_code: 200,
|
|
11817
|
+
body: await sdk.trigger({
|
|
11818
|
+
function_id: "mem::replay::load",
|
|
11819
|
+
payload: { sessionId }
|
|
11820
|
+
})
|
|
11821
|
+
};
|
|
11822
|
+
});
|
|
11823
|
+
sdk.registerTrigger({
|
|
11824
|
+
type: "http",
|
|
11825
|
+
function_id: "api::replay::load",
|
|
11826
|
+
config: {
|
|
11827
|
+
api_path: "/agentmemory/replay/load",
|
|
11828
|
+
http_method: "GET"
|
|
11829
|
+
}
|
|
11830
|
+
});
|
|
11831
|
+
sdk.registerFunction("api::replay::sessions", async (req) => {
|
|
11832
|
+
const authErr = checkAuth(req, secret);
|
|
11833
|
+
if (authErr) return authErr;
|
|
11834
|
+
return {
|
|
11835
|
+
status_code: 200,
|
|
11836
|
+
body: await sdk.trigger({ function_id: "mem::replay::sessions" })
|
|
11837
|
+
};
|
|
11838
|
+
});
|
|
11839
|
+
sdk.registerTrigger({
|
|
11840
|
+
type: "http",
|
|
11841
|
+
function_id: "api::replay::sessions",
|
|
11842
|
+
config: {
|
|
11843
|
+
api_path: "/agentmemory/replay/sessions",
|
|
11844
|
+
http_method: "GET"
|
|
11845
|
+
}
|
|
11846
|
+
});
|
|
11847
|
+
sdk.registerFunction("api::replay::import", async (req) => {
|
|
11848
|
+
const authErr = checkAuth(req, secret);
|
|
11849
|
+
if (authErr) return authErr;
|
|
11850
|
+
const body = req.body ?? {};
|
|
11851
|
+
const payload = {};
|
|
11852
|
+
if (body.path !== void 0) {
|
|
11853
|
+
if (typeof body.path !== "string" || body.path.trim().length === 0) return {
|
|
11854
|
+
status_code: 400,
|
|
11855
|
+
body: { error: "path must be a non-empty string" }
|
|
11856
|
+
};
|
|
11857
|
+
payload.path = body.path.trim();
|
|
11858
|
+
}
|
|
11859
|
+
if (body.maxFiles !== void 0) {
|
|
11860
|
+
if (!Number.isInteger(body.maxFiles) || body.maxFiles < 1) return {
|
|
11861
|
+
status_code: 400,
|
|
11862
|
+
body: { error: "maxFiles must be a positive integer" }
|
|
11863
|
+
};
|
|
11864
|
+
payload.maxFiles = body.maxFiles;
|
|
11865
|
+
}
|
|
11866
|
+
return {
|
|
11867
|
+
status_code: 202,
|
|
11868
|
+
body: await sdk.trigger({
|
|
11869
|
+
function_id: "mem::replay::import-jsonl",
|
|
11870
|
+
payload
|
|
11871
|
+
})
|
|
11872
|
+
};
|
|
11873
|
+
});
|
|
11874
|
+
sdk.registerTrigger({
|
|
11875
|
+
type: "http",
|
|
11876
|
+
function_id: "api::replay::import",
|
|
11877
|
+
config: {
|
|
11878
|
+
api_path: "/agentmemory/replay/import-jsonl",
|
|
11879
|
+
http_method: "POST"
|
|
11880
|
+
}
|
|
11881
|
+
});
|
|
11108
11882
|
sdk.registerFunction("api::session::start", async (req) => {
|
|
11109
11883
|
const body = req.body ?? {};
|
|
11110
11884
|
const sessionId = asNonEmptyString$1(body.sessionId);
|
|
@@ -13609,13 +14383,46 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
13609
14383
|
status_code: 400,
|
|
13610
14384
|
body: { error: "query is required for memory_recall" }
|
|
13611
14385
|
};
|
|
14386
|
+
const format = typeof args.format === "string" ? args.format.trim().toLowerCase() : "full";
|
|
14387
|
+
if (![
|
|
14388
|
+
"full",
|
|
14389
|
+
"compact",
|
|
14390
|
+
"narrative"
|
|
14391
|
+
].includes(format)) return {
|
|
14392
|
+
status_code: 400,
|
|
14393
|
+
body: { error: "format must be one of: full, compact, narrative" }
|
|
14394
|
+
};
|
|
14395
|
+
const tokenBudget = asNumber(args.token_budget);
|
|
14396
|
+
if (args.token_budget !== void 0 && (!Number.isInteger(tokenBudget) || (tokenBudget ?? 0) < 1)) return {
|
|
14397
|
+
status_code: 400,
|
|
14398
|
+
body: { error: "token_budget must be a positive integer" }
|
|
14399
|
+
};
|
|
13612
14400
|
const result = await sdk.trigger({
|
|
13613
14401
|
function_id: "mem::search",
|
|
13614
14402
|
payload: {
|
|
13615
14403
|
query: args.query,
|
|
13616
|
-
limit: typeof args.limit === "number" ? args.limit : 10
|
|
14404
|
+
limit: typeof args.limit === "number" ? args.limit : 10,
|
|
14405
|
+
format,
|
|
14406
|
+
token_budget: tokenBudget
|
|
13617
14407
|
}
|
|
13618
14408
|
});
|
|
14409
|
+
return {
|
|
14410
|
+
status_code: 200,
|
|
14411
|
+
body: { content: [{
|
|
14412
|
+
type: "text",
|
|
14413
|
+
text: format === "narrative" && result && typeof result === "object" && "text" in result && typeof result.text === "string" ? result.text : JSON.stringify(result, null, 2)
|
|
14414
|
+
}] }
|
|
14415
|
+
};
|
|
14416
|
+
}
|
|
14417
|
+
case "memory_compress_file": {
|
|
14418
|
+
if (typeof args.filePath !== "string" || !args.filePath.trim()) return {
|
|
14419
|
+
status_code: 400,
|
|
14420
|
+
body: { error: "filePath is required for memory_compress_file" }
|
|
14421
|
+
};
|
|
14422
|
+
const result = await sdk.trigger({
|
|
14423
|
+
function_id: "mem::compress-file",
|
|
14424
|
+
payload: { filePath: args.filePath.trim() }
|
|
14425
|
+
});
|
|
13619
14426
|
return {
|
|
13620
14427
|
status_code: 200,
|
|
13621
14428
|
body: { content: [{
|
|
@@ -15323,6 +16130,8 @@ async function main() {
|
|
|
15323
16130
|
registerQueryExpansionFunction(sdk, provider);
|
|
15324
16131
|
registerTemporalGraphFunctions(sdk, kv, provider);
|
|
15325
16132
|
registerRetentionFunctions(sdk, kv);
|
|
16133
|
+
registerCompressFileFunction(sdk, kv, provider);
|
|
16134
|
+
registerReplayFunctions(sdk, kv);
|
|
15326
16135
|
console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
|
|
15327
16136
|
console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
|
|
15328
16137
|
const snapshotConfig = loadSnapshotConfig();
|
|
@@ -15362,7 +16171,7 @@ async function main() {
|
|
|
15362
16171
|
}
|
|
15363
16172
|
}
|
|
15364
16173
|
console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
15365
|
-
console.log(`[agentmemory] Endpoints:
|
|
16174
|
+
console.log(`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
|
|
15366
16175
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
15367
16176
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|
|
15368
16177
|
const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);
|
|
@@ -15429,4 +16238,4 @@ main().catch((err) => {
|
|
|
15429
16238
|
|
|
15430
16239
|
//#endregion
|
|
15431
16240
|
export { };
|
|
15432
|
-
//# sourceMappingURL=src-
|
|
16241
|
+
//# sourceMappingURL=src-B3pEsBSb.mjs.map
|