@agentmemory/agentmemory 0.8.12 → 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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { TriggerAction, registerWorker } from "iii-sdk";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
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";
@@ -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, readFile, 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"]) return {
55
- provider: "gemini",
56
- model: env["GEMINI_MODEL"] || "gemini-2.0-flash",
57
- maxTokens
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": return new OpenRouterProvider(requireEnvVar("GEMINI_API_KEY"), config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
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$1(s, n) {
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$1(toolName || "observation", 80),
2142
- subtitle: inputStr ? truncate$1(inputStr, 120) : void 0,
2149
+ title: truncate$2(toolName || "observation", 80),
2150
+ subtitle: inputStr ? truncate$2(inputStr, 120) : void 0,
2143
2151
  facts: [],
2144
- narrative: truncate$1(narrativeParts.join(" | "), 400),
2152
+ narrative: truncate$2(narrativeParts.join(" | "), 400),
2145
2153
  concepts: [],
2146
2154
  files: extractFiles$1(raw.toolInput),
2147
2155
  importance: 5,
@@ -2565,16 +2573,16 @@ function buildCompressionPrompt(observation) {
2565
2573
  if (observation.toolName) parts.push(`Tool: ${observation.toolName}`);
2566
2574
  if (observation.toolInput) {
2567
2575
  const input = typeof observation.toolInput === "string" ? observation.toolInput : JSON.stringify(observation.toolInput, null, 2);
2568
- parts.push(`Input:\n${truncate(input, 4e3)}`);
2576
+ parts.push(`Input:\n${truncate$1(input, 4e3)}`);
2569
2577
  }
2570
2578
  if (observation.toolOutput) {
2571
2579
  const output = typeof observation.toolOutput === "string" ? observation.toolOutput : JSON.stringify(observation.toolOutput, null, 2);
2572
- parts.push(`Output:\n${truncate(output, 4e3)}`);
2580
+ parts.push(`Output:\n${truncate$1(output, 4e3)}`);
2573
2581
  }
2574
- 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)}`);
2575
2583
  return parts.join("\n\n");
2576
2584
  }
2577
- function truncate(s, max) {
2585
+ function truncate$1(s, max) {
2578
2586
  return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
2579
2587
  }
2580
2588
 
@@ -3739,25 +3747,40 @@ function registerRememberFunction(sdk, kv) {
3739
3747
  });
3740
3748
  sdk.registerFunction("mem::forget", async (data) => {
3741
3749
  let deleted = 0;
3750
+ const deletedMemoryIds = [];
3751
+ const deletedObservationIds = [];
3752
+ let deletedSession = false;
3742
3753
  if (data.memoryId) {
3743
3754
  await kv.delete(KV.memories, data.memoryId);
3744
3755
  await deleteAccessLog(kv, data.memoryId);
3756
+ deletedMemoryIds.push(data.memoryId);
3745
3757
  deleted++;
3746
3758
  }
3747
3759
  if (data.sessionId && data.observationIds && data.observationIds.length > 0) for (const obsId of data.observationIds) {
3748
3760
  await kv.delete(KV.observations(data.sessionId), obsId);
3761
+ deletedObservationIds.push(obsId);
3749
3762
  deleted++;
3750
3763
  }
3751
3764
  if (data.sessionId && (!data.observationIds || data.observationIds.length === 0) && !data.memoryId) {
3752
3765
  const observations = await kv.list(KV.observations(data.sessionId));
3753
3766
  for (const obs of observations) {
3754
3767
  await kv.delete(KV.observations(data.sessionId), obs.id);
3768
+ deletedObservationIds.push(obs.id);
3755
3769
  deleted++;
3756
3770
  }
3757
3771
  await kv.delete(KV.sessions, data.sessionId);
3758
3772
  await kv.delete(KV.summaries, data.sessionId);
3773
+ deletedSession = true;
3759
3774
  deleted += 2;
3760
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
+ });
3761
3784
  logger.info("Memory forgotten", { deleted });
3762
3785
  return {
3763
3786
  success: true,
@@ -4477,7 +4500,7 @@ function registerAutoForgetFunction(sdk, kv) {
4477
4500
 
4478
4501
  //#endregion
4479
4502
  //#region src/version.ts
4480
- const VERSION = "0.8.12";
4503
+ const VERSION = "0.9.0";
4481
4504
 
4482
4505
  //#endregion
4483
4506
  //#region src/functions/export-import.ts
@@ -4593,7 +4616,9 @@ function registerExportImportFunction(sdk, kv) {
4593
4616
  "0.8.9",
4594
4617
  "0.8.10",
4595
4618
  "0.8.11",
4596
- "0.8.12"
4619
+ "0.8.12",
4620
+ "0.8.13",
4621
+ "0.9.0"
4597
4622
  ]).has(importData.version)) return {
4598
4623
  success: false,
4599
4624
  error: `Unsupported export version: ${importData.version}`
@@ -10965,8 +10990,8 @@ function registerRetentionFunctions(sdk, kv) {
10965
10990
  let evictedSemantic = 0;
10966
10991
  const evictedIds = [];
10967
10992
  for (const candidate of candidates) try {
10968
- let scope;
10969
- let resolvedSource;
10993
+ let scope = null;
10994
+ let resolvedSource = null;
10970
10995
  if (candidate.source === "semantic") {
10971
10996
  scope = KV.semantic;
10972
10997
  resolvedSource = "semantic";
@@ -10976,10 +11001,11 @@ function registerRetentionFunctions(sdk, kv) {
10976
11001
  } else if (await kv.get(KV.memories, candidate.memoryId) !== null) {
10977
11002
  scope = KV.memories;
10978
11003
  resolvedSource = "episodic";
10979
- } else {
11004
+ } else if (await kv.get(KV.semantic, candidate.memoryId) !== null) {
10980
11005
  scope = KV.semantic;
10981
11006
  resolvedSource = "semantic";
10982
11007
  }
11008
+ if (!scope || !resolvedSource) continue;
10983
11009
  await kv.delete(scope, candidate.memoryId);
10984
11010
  await kv.delete(KV.retentionScores, candidate.memoryId);
10985
11011
  await deleteAccessLog(kv, candidate.memoryId);
@@ -11066,7 +11092,7 @@ function validateCompression(original, compressed) {
11066
11092
  }
11067
11093
  function resolveBackupPath(filePath) {
11068
11094
  const base = basename(filePath, extname(filePath));
11069
- const name = base.endsWith(".original") ? base : `${base}.original`;
11095
+ const name = base.endsWith(".original") ? `${base}.backup` : `${base}.original`;
11070
11096
  return join(dirname(filePath), `${name}.md`);
11071
11097
  }
11072
11098
  function registerCompressFileFunction(sdk, kv, provider) {
@@ -11085,6 +11111,17 @@ function registerCompressFileFunction(sdk, kv, provider) {
11085
11111
  success: false,
11086
11112
  error: "refusing to process sensitive-looking path"
11087
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
+ }
11088
11125
  let original;
11089
11126
  try {
11090
11127
  original = await readFile(absolutePath, "utf-8");
@@ -11108,7 +11145,23 @@ function registerCompressFileFunction(sdk, kv, provider) {
11108
11145
  };
11109
11146
  const backupPath = resolveBackupPath(absolutePath);
11110
11147
  await writeFile(backupPath, original, "utf-8");
11111
- await writeFile(absolutePath, compressed, "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
+ }
11112
11165
  try {
11113
11166
  await recordAudit(kv, "compress", "mem::compress-file", [], {
11114
11167
  filePath: absolutePath,
@@ -11127,6 +11180,419 @@ function registerCompressFileFunction(sdk, kv, provider) {
11127
11180
  });
11128
11181
  }
11129
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
+
11130
11596
  //#endregion
11131
11597
  //#region src/health/thresholds.ts
11132
11598
  const DEFAULTS = {
@@ -11135,7 +11601,8 @@ const DEFAULTS = {
11135
11601
  cpuWarnPercent: 80,
11136
11602
  cpuCriticalPercent: 90,
11137
11603
  memoryWarnPercent: 80,
11138
- memoryCriticalPercent: 95
11604
+ memoryCriticalPercent: 95,
11605
+ memoryRssFloorBytes: 512 * 1024 * 1024
11139
11606
  };
11140
11607
  function evaluateHealth(snapshot, config = {}) {
11141
11608
  const cfg = {
@@ -11167,13 +11634,16 @@ function evaluateHealth(snapshot, config = {}) {
11167
11634
  degraded = true;
11168
11635
  }
11169
11636
  const memPercent = snapshot.memory.heapTotal > 0 ? snapshot.memory.heapUsed / snapshot.memory.heapTotal * 100 : 0;
11170
- if (memPercent > cfg.memoryCriticalPercent) {
11171
- alerts.push(`memory_critical_${Math.round(memPercent)}%`);
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`);
11172
11642
  critical = true;
11173
- } else if (memPercent > cfg.memoryWarnPercent) {
11174
- 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`);
11175
11645
  degraded = true;
11176
- }
11646
+ } else if (memPercent > cfg.memoryWarnPercent) alerts.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
11177
11647
  return {
11178
11648
  status: critical ? "critical" : degraded ? "degraded" : "healthy",
11179
11649
  alerts
@@ -11568,6 +12038,81 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
11568
12038
  http_method: "POST"
11569
12039
  }
11570
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
+ });
11571
12116
  sdk.registerFunction("api::session::start", async (req) => {
11572
12117
  const body = req.body ?? {};
11573
12118
  const sessionId = asNonEmptyString$1(body.sessionId);
@@ -16759,6 +17304,7 @@ async function main() {
16759
17304
  registerTemporalGraphFunctions(sdk, kv, provider);
16760
17305
  registerRetentionFunctions(sdk, kv);
16761
17306
  registerCompressFileFunction(sdk, kv, provider);
17307
+ registerReplayFunctions(sdk, kv);
16762
17308
  console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
16763
17309
  console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
16764
17310
  const snapshotConfig = loadSnapshotConfig();
@@ -16798,7 +17344,7 @@ async function main() {
16798
17344
  }
16799
17345
  }
16800
17346
  console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
16801
- console.log(`[agentmemory] Endpoints: 104 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
17347
+ console.log(`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
16802
17348
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
16803
17349
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
16804
17350
  const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);