@agentmemory/agentmemory 0.9.16 → 0.9.18
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/.env.example +6 -0
- package/AGENTS.md +5 -5
- package/README.md +41 -6
- package/dist/.env.example +6 -0
- package/dist/cli.mjs +12 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +324 -17
- package/dist/index.mjs.map +1 -1
- package/dist/{src-3Oy_OOlF.mjs → src-C7vygXCj.mjs} +315 -15
- package/dist/src-C7vygXCj.mjs.map +1 -0
- package/dist/{standalone-BQOaGF4z.mjs → standalone-kg2TedgD.mjs} +18 -9
- package/dist/standalone-kg2TedgD.mjs.map +1 -0
- package/dist/standalone.mjs +17 -8
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-BF0pgZmI.mjs → tools-registry-BFKFKmYh.mjs} +11 -4
- package/dist/tools-registry-BFKFKmYh.mjs.map +1 -0
- package/dist/viewer/favicon.svg +1 -0
- package/dist/viewer/index.html +190 -60
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/src-3Oy_OOlF.mjs.map +0 -1
- package/dist/standalone-BQOaGF4z.mjs.map +0 -1
- package/dist/tools-registry-BF0pgZmI.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -68,6 +68,12 @@ function hasRealValue(v) {
|
|
|
68
68
|
}
|
|
69
69
|
function detectProvider(env) {
|
|
70
70
|
const maxTokens = parseInt(env["MAX_TOKENS"] || "4096", 10);
|
|
71
|
+
if (hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") return {
|
|
72
|
+
provider: "openai",
|
|
73
|
+
model: env["OPENAI_MODEL"] || "gpt-4o-mini",
|
|
74
|
+
maxTokens,
|
|
75
|
+
baseURL: env["OPENAI_BASE_URL"]
|
|
76
|
+
};
|
|
71
77
|
if (hasRealValue(env["MINIMAX_API_KEY"])) return {
|
|
72
78
|
provider: "minimax",
|
|
73
79
|
model: env["MINIMAX_MODEL"] || "MiniMax-M2.7",
|
|
@@ -93,7 +99,7 @@ function detectProvider(env) {
|
|
|
93
99
|
maxTokens
|
|
94
100
|
};
|
|
95
101
|
if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
|
|
96
|
-
process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY). LLM-backed compression and summarization are DISABLED — using no-op provider. This is the safe default: the agent-sdk fallback used to spawn Claude Agent SDK child sessions which inherit Claude Code's plugin hooks and cause infinite Stop-hook recursion (#149 follow-up). To opt in to the agent-sdk fallback anyway, set both AGENTMEMORY_AUTO_COMPRESS=true AND AGENTMEMORY_ALLOW_AGENT_SDK=true — but be aware it will burn your Claude Pro allocation and may still recurse if you use it from inside Claude Code itself.\n");
|
|
102
|
+
process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY, OPENAI_API_KEY). LLM-backed compression and summarization are DISABLED — using no-op provider. This is the safe default: the agent-sdk fallback used to spawn Claude Agent SDK child sessions which inherit Claude Code's plugin hooks and cause infinite Stop-hook recursion (#149 follow-up). To opt in to the agent-sdk fallback anyway, set both AGENTMEMORY_AUTO_COMPRESS=true AND AGENTMEMORY_ALLOW_AGENT_SDK=true — but be aware it will burn your Claude Pro allocation and may still recurse if you use it from inside Claude Code itself.\n");
|
|
97
103
|
return {
|
|
98
104
|
provider: "noop",
|
|
99
105
|
model: "noop",
|
|
@@ -133,7 +139,7 @@ function getEnvVar(key) {
|
|
|
133
139
|
}
|
|
134
140
|
function detectLlmProviderKind() {
|
|
135
141
|
const env = getMergedEnv();
|
|
136
|
-
if (hasRealValue(env["ANTHROPIC_API_KEY"]) || hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"]) || hasRealValue(env["OPENROUTER_API_KEY"]) || hasRealValue(env["MINIMAX_API_KEY"])) return "llm";
|
|
142
|
+
if (hasRealValue(env["ANTHROPIC_API_KEY"]) || hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"]) || hasRealValue(env["OPENROUTER_API_KEY"]) || hasRealValue(env["MINIMAX_API_KEY"]) || hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") return "llm";
|
|
137
143
|
return "noop";
|
|
138
144
|
}
|
|
139
145
|
function loadEmbeddingConfig() {
|
|
@@ -215,7 +221,8 @@ const VALID_PROVIDERS = new Set([
|
|
|
215
221
|
"gemini",
|
|
216
222
|
"openrouter",
|
|
217
223
|
"agent-sdk",
|
|
218
|
-
"minimax"
|
|
224
|
+
"minimax",
|
|
225
|
+
"openai"
|
|
219
226
|
]);
|
|
220
227
|
function loadFallbackConfig() {
|
|
221
228
|
const env = getMergedEnv();
|
|
@@ -318,6 +325,20 @@ var AnthropicProvider = class {
|
|
|
318
325
|
}
|
|
319
326
|
};
|
|
320
327
|
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/providers/_fetch.ts
|
|
330
|
+
function fetchWithTimeout(url, init, timeoutMs) {
|
|
331
|
+
const parsed = timeoutMs ?? Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10);
|
|
332
|
+
const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
|
|
333
|
+
const ctl = new AbortController();
|
|
334
|
+
const signal = init.signal ? AbortSignal.any([init.signal, ctl.signal]) : ctl.signal;
|
|
335
|
+
const t = setTimeout(() => ctl.abort(), ms);
|
|
336
|
+
return fetch(url, {
|
|
337
|
+
...init,
|
|
338
|
+
signal
|
|
339
|
+
}).finally(() => clearTimeout(t));
|
|
340
|
+
}
|
|
341
|
+
|
|
321
342
|
//#endregion
|
|
322
343
|
//#region src/providers/minimax.ts
|
|
323
344
|
/**
|
|
@@ -353,8 +374,7 @@ var MinimaxProvider = class {
|
|
|
353
374
|
return this.call(systemPrompt, userPrompt);
|
|
354
375
|
}
|
|
355
376
|
async call(systemPrompt, userPrompt) {
|
|
356
|
-
const
|
|
357
|
-
const response = await fetch(url, {
|
|
377
|
+
const response = await fetchWithTimeout(`${this.baseUrl}/v1/messages`, {
|
|
358
378
|
method: "POST",
|
|
359
379
|
headers: {
|
|
360
380
|
"Content-Type": "application/json",
|
|
@@ -398,6 +418,146 @@ var NoopProvider = class {
|
|
|
398
418
|
}
|
|
399
419
|
};
|
|
400
420
|
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/providers/openai.ts
|
|
423
|
+
const DEFAULT_BASE_URL$1 = "https://api.openai.com";
|
|
424
|
+
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
425
|
+
const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
|
|
426
|
+
/**
|
|
427
|
+
* OpenAI-compatible LLM provider.
|
|
428
|
+
*
|
|
429
|
+
* Uses raw fetch (no SDK) to support any OpenAI-compatible endpoint:
|
|
430
|
+
* - OpenAI official
|
|
431
|
+
* - Azure OpenAI (auto-detected from .openai.azure.com host)
|
|
432
|
+
* - DeepSeek
|
|
433
|
+
* - 硅基流动 (SiliconFlow)
|
|
434
|
+
* - vLLM / LM Studio / Ollama (with OpenAI compatibility layer)
|
|
435
|
+
* - Any other proxy implementing /v1/chat/completions
|
|
436
|
+
*
|
|
437
|
+
* Required env vars:
|
|
438
|
+
* OPENAI_API_KEY — API key
|
|
439
|
+
*
|
|
440
|
+
* Optional:
|
|
441
|
+
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
|
|
442
|
+
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
|
|
443
|
+
* OPENAI_MODEL — model name (default: gpt-4o-mini)
|
|
444
|
+
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
|
|
445
|
+
* OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
|
|
446
|
+
* takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
|
|
447
|
+
* for back-compat with the v0.9.17 shipping name).
|
|
448
|
+
* AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
|
|
449
|
+
* raw-fetch LLM + embedding providers. Used when
|
|
450
|
+
* OPENAI_TIMEOUT_MS is not set. Default: 60000.
|
|
451
|
+
* MAX_TOKENS — max output tokens (default: from config or 4096)
|
|
452
|
+
* OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
|
|
453
|
+
* Passthrough for reasoning models (e.g. Ollama Cloud
|
|
454
|
+
* thinking models). Set to "none" to ensure
|
|
455
|
+
* message.content is populated instead of only
|
|
456
|
+
* message.reasoning.
|
|
457
|
+
*/
|
|
458
|
+
var OpenAIProvider = class {
|
|
459
|
+
name = "openai";
|
|
460
|
+
apiKey;
|
|
461
|
+
model;
|
|
462
|
+
maxTokens;
|
|
463
|
+
baseUrl;
|
|
464
|
+
reasoningEffort;
|
|
465
|
+
timeoutMs;
|
|
466
|
+
isAzure;
|
|
467
|
+
azureApiVersion;
|
|
468
|
+
constructor(apiKey, model, maxTokens, baseURL) {
|
|
469
|
+
this.apiKey = apiKey;
|
|
470
|
+
this.model = model;
|
|
471
|
+
this.maxTokens = maxTokens;
|
|
472
|
+
this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL$1).replace(/\/+$/, "");
|
|
473
|
+
this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
|
|
474
|
+
this.timeoutMs = resolveTimeout();
|
|
475
|
+
this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
|
|
476
|
+
this.isAzure = detectAzure(this.baseUrl);
|
|
477
|
+
}
|
|
478
|
+
async compress(systemPrompt, userPrompt) {
|
|
479
|
+
return this.call(systemPrompt, userPrompt);
|
|
480
|
+
}
|
|
481
|
+
async summarize(systemPrompt, userPrompt) {
|
|
482
|
+
return this.call(systemPrompt, userPrompt);
|
|
483
|
+
}
|
|
484
|
+
buildUrl() {
|
|
485
|
+
if (this.isAzure) {
|
|
486
|
+
const sep = this.baseUrl.includes("?") ? "&" : "?";
|
|
487
|
+
return `${this.baseUrl}/chat/completions${sep}api-version=${encodeURIComponent(this.azureApiVersion)}`;
|
|
488
|
+
}
|
|
489
|
+
return `${this.baseUrl}/v1/chat/completions`;
|
|
490
|
+
}
|
|
491
|
+
buildHeaders() {
|
|
492
|
+
if (this.isAzure) return {
|
|
493
|
+
"Content-Type": "application/json",
|
|
494
|
+
"api-key": this.apiKey
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
"Content-Type": "application/json",
|
|
498
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async call(systemPrompt, userPrompt) {
|
|
502
|
+
const url = this.buildUrl();
|
|
503
|
+
const body = {
|
|
504
|
+
model: this.model,
|
|
505
|
+
max_tokens: this.maxTokens,
|
|
506
|
+
messages: [{
|
|
507
|
+
role: "system",
|
|
508
|
+
content: systemPrompt
|
|
509
|
+
}, {
|
|
510
|
+
role: "user",
|
|
511
|
+
content: userPrompt
|
|
512
|
+
}]
|
|
513
|
+
};
|
|
514
|
+
if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
|
|
515
|
+
let response;
|
|
516
|
+
try {
|
|
517
|
+
response = await fetchWithTimeout(url, {
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: this.buildHeaders(),
|
|
520
|
+
body: JSON.stringify(body)
|
|
521
|
+
}, this.timeoutMs);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (err instanceof Error && err.name === "AbortError") throw new Error(`OpenAI API request timed out after ${this.timeoutMs}ms — set OPENAI_TIMEOUT_MS (or AGENTMEMORY_LLM_TIMEOUT_MS) to raise the bound or check the provider status.`);
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
if (!response.ok) {
|
|
527
|
+
const text = await response.text();
|
|
528
|
+
throw new Error(`OpenAI API error (${response.status}): ${text}`);
|
|
529
|
+
}
|
|
530
|
+
const data = await response.json();
|
|
531
|
+
const message = data.choices?.[0]?.message;
|
|
532
|
+
const content = message?.content;
|
|
533
|
+
if (content) return content;
|
|
534
|
+
const reasoning = message?.reasoning;
|
|
535
|
+
if (reasoning) return reasoning;
|
|
536
|
+
throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
function resolveTimeout() {
|
|
540
|
+
const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
|
|
541
|
+
if (openai !== void 0) return openai;
|
|
542
|
+
const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
|
|
543
|
+
if (globalMs !== void 0) return globalMs;
|
|
544
|
+
return DEFAULT_TIMEOUT_MS;
|
|
545
|
+
}
|
|
546
|
+
function parsePositiveInt(raw) {
|
|
547
|
+
if (!raw) return void 0;
|
|
548
|
+
const trimmed = raw.trim();
|
|
549
|
+
if (!/^\d+$/.test(trimmed)) return void 0;
|
|
550
|
+
const n = Number(trimmed);
|
|
551
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
552
|
+
}
|
|
553
|
+
function detectAzure(baseUrl) {
|
|
554
|
+
try {
|
|
555
|
+
return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
401
561
|
//#endregion
|
|
402
562
|
//#region src/providers/openrouter.ts
|
|
403
563
|
var OpenRouterProvider = class {
|
|
@@ -420,7 +580,7 @@ var OpenRouterProvider = class {
|
|
|
420
580
|
return this.call(systemPrompt, userPrompt);
|
|
421
581
|
}
|
|
422
582
|
async call(systemPrompt, userPrompt) {
|
|
423
|
-
const response = await
|
|
583
|
+
const response = await fetchWithTimeout(this.baseUrl, {
|
|
424
584
|
method: "POST",
|
|
425
585
|
headers: {
|
|
426
586
|
"Content-Type": "application/json",
|
|
@@ -589,7 +749,7 @@ var GeminiEmbeddingProvider = class {
|
|
|
589
749
|
const results = [];
|
|
590
750
|
for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
|
|
591
751
|
const chunk = texts.slice(i, i + BATCH_LIMIT);
|
|
592
|
-
const response = await
|
|
752
|
+
const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
|
|
593
753
|
method: "POST",
|
|
594
754
|
headers: { "Content-Type": "application/json" },
|
|
595
755
|
body: JSON.stringify({ requests: chunk.map((t) => ({
|
|
@@ -678,8 +838,7 @@ var OpenAIEmbeddingProvider = class {
|
|
|
678
838
|
return result;
|
|
679
839
|
}
|
|
680
840
|
async embedBatch(texts) {
|
|
681
|
-
const
|
|
682
|
-
const response = await fetch(url, {
|
|
841
|
+
const response = await fetchWithTimeout(`${this.baseUrl}/v1/embeddings`, {
|
|
683
842
|
method: "POST",
|
|
684
843
|
headers: {
|
|
685
844
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -714,7 +873,7 @@ var VoyageEmbeddingProvider = class {
|
|
|
714
873
|
return result;
|
|
715
874
|
}
|
|
716
875
|
async embedBatch(texts) {
|
|
717
|
-
const response = await
|
|
876
|
+
const response = await fetchWithTimeout(API_URL$2, {
|
|
718
877
|
method: "POST",
|
|
719
878
|
headers: {
|
|
720
879
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -750,7 +909,7 @@ var CohereEmbeddingProvider = class {
|
|
|
750
909
|
return result;
|
|
751
910
|
}
|
|
752
911
|
async embedBatch(texts) {
|
|
753
|
-
const response = await
|
|
912
|
+
const response = await fetchWithTimeout(API_URL$1, {
|
|
754
913
|
method: "POST",
|
|
755
914
|
headers: {
|
|
756
915
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -788,7 +947,7 @@ var OpenRouterEmbeddingProvider = class {
|
|
|
788
947
|
return result;
|
|
789
948
|
}
|
|
790
949
|
async embedBatch(texts) {
|
|
791
|
-
const response = await
|
|
950
|
+
const response = await fetchWithTimeout(API_URL, {
|
|
792
951
|
method: "POST",
|
|
793
952
|
headers: {
|
|
794
953
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -983,6 +1142,11 @@ function createBaseProvider(config) {
|
|
|
983
1142
|
return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
|
|
984
1143
|
}
|
|
985
1144
|
case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
|
|
1145
|
+
case "openai": {
|
|
1146
|
+
const openaiKey = getEnvVar("OPENAI_API_KEY");
|
|
1147
|
+
if (!openaiKey) throw new Error("OPENAI_API_KEY is required for the openai provider");
|
|
1148
|
+
return new OpenAIProvider(openaiKey, config.model, config.maxTokens, config.baseURL);
|
|
1149
|
+
}
|
|
986
1150
|
case "noop": return new NoopProvider();
|
|
987
1151
|
default: return new AgentSDKProvider();
|
|
988
1152
|
}
|
|
@@ -4400,7 +4564,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
|
|
|
4400
4564
|
sdk.registerFunction("mem::context", async (data) => {
|
|
4401
4565
|
const budget = data.budget || tokenBudget;
|
|
4402
4566
|
const blocks = [];
|
|
4403
|
-
const [pinnedSlots, profile] = await Promise.all([
|
|
4567
|
+
const [pinnedSlots, profile, lessons] = await Promise.all([
|
|
4568
|
+
isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
|
|
4569
|
+
kv.get(KV.profiles, data.project).catch(() => null),
|
|
4570
|
+
kv.list(KV.lessons).catch(() => [])
|
|
4571
|
+
]);
|
|
4404
4572
|
const slotContent = renderPinnedContext(pinnedSlots);
|
|
4405
4573
|
if (slotContent) blocks.push({
|
|
4406
4574
|
type: "memory",
|
|
@@ -4424,6 +4592,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
|
|
|
4424
4592
|
});
|
|
4425
4593
|
}
|
|
4426
4594
|
}
|
|
4595
|
+
const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
|
|
4596
|
+
const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
|
|
4597
|
+
return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
|
|
4598
|
+
}).slice(0, 10);
|
|
4599
|
+
if (relevantLessons.length > 0) {
|
|
4600
|
+
const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
|
|
4601
|
+
const mostRecent = relevantLessons.reduce((acc, l) => {
|
|
4602
|
+
const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
|
|
4603
|
+
return t > acc ? t : acc;
|
|
4604
|
+
}, 0);
|
|
4605
|
+
blocks.push({
|
|
4606
|
+
type: "memory",
|
|
4607
|
+
content: lessonsContent,
|
|
4608
|
+
tokens: estimateTokens$1(lessonsContent),
|
|
4609
|
+
recency: mostRecent,
|
|
4610
|
+
sourceIds: relevantLessons.map((l) => l.id)
|
|
4611
|
+
});
|
|
4612
|
+
}
|
|
4427
4613
|
const sessions = (await kv.list(KV.sessions)).filter((s) => s.project === data.project && s.id !== data.sessionId).sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()).slice(0, 10);
|
|
4428
4614
|
const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
|
|
4429
4615
|
const sessionsNeedingObs = [];
|
|
@@ -5246,6 +5432,46 @@ const DEFAULTS$1 = {
|
|
|
5246
5432
|
lowImportanceThreshold: 3,
|
|
5247
5433
|
maxObservationsPerProject: 1e4
|
|
5248
5434
|
};
|
|
5435
|
+
function isValidRecoveryResult(result) {
|
|
5436
|
+
if (!result || typeof result !== "object") return false;
|
|
5437
|
+
if (!("success" in result)) return true;
|
|
5438
|
+
return result.success !== false;
|
|
5439
|
+
}
|
|
5440
|
+
function isCompressedObservation(observation) {
|
|
5441
|
+
return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
|
|
5442
|
+
}
|
|
5443
|
+
async function recoverStaleSession(sdk, sessionId) {
|
|
5444
|
+
try {
|
|
5445
|
+
const result = await sdk.trigger({
|
|
5446
|
+
function_id: "event::session::stopped",
|
|
5447
|
+
payload: { sessionId }
|
|
5448
|
+
});
|
|
5449
|
+
if (!isValidRecoveryResult(result)) {
|
|
5450
|
+
logger.warn("Stale session recovery failed", {
|
|
5451
|
+
sessionId,
|
|
5452
|
+
result
|
|
5453
|
+
});
|
|
5454
|
+
return false;
|
|
5455
|
+
}
|
|
5456
|
+
return true;
|
|
5457
|
+
} catch (err) {
|
|
5458
|
+
logger.warn("Stale session recovery failed", {
|
|
5459
|
+
sessionId,
|
|
5460
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5461
|
+
});
|
|
5462
|
+
return false;
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
async function runRecoveredSessionConsolidation(sdk) {
|
|
5466
|
+
try {
|
|
5467
|
+
await sdk.trigger({
|
|
5468
|
+
function_id: "mem::consolidate-pipeline",
|
|
5469
|
+
payload: { tier: "all" }
|
|
5470
|
+
});
|
|
5471
|
+
} catch (err) {
|
|
5472
|
+
logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5249
5475
|
function registerEvictFunction(sdk, kv) {
|
|
5250
5476
|
sdk.registerFunction("mem::evict", async (data) => {
|
|
5251
5477
|
const dryRun = data?.dryRun ?? false;
|
|
@@ -5264,6 +5490,7 @@ function registerEvictFunction(sdk, kv) {
|
|
|
5264
5490
|
nonLatestMemories: 0,
|
|
5265
5491
|
dryRun
|
|
5266
5492
|
};
|
|
5493
|
+
let recoveredStaleSessions = 0;
|
|
5267
5494
|
const sessions = await kv.list(KV.sessions).catch(() => []);
|
|
5268
5495
|
const summaries = await kv.list(KV.summaries).catch(() => []);
|
|
5269
5496
|
const summaryIds = new Set(summaries.map((s) => s.sessionId));
|
|
@@ -5271,6 +5498,23 @@ function registerEvictFunction(sdk, kv) {
|
|
|
5271
5498
|
if (!session.startedAt) continue;
|
|
5272
5499
|
if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
|
|
5273
5500
|
else {
|
|
5501
|
+
const observations = await kv.list(KV.observations(session.id)).catch((err) => {
|
|
5502
|
+
logger.warn("Stale session observation scan failed", {
|
|
5503
|
+
sessionId: session.id,
|
|
5504
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5505
|
+
});
|
|
5506
|
+
return null;
|
|
5507
|
+
});
|
|
5508
|
+
if (!observations) continue;
|
|
5509
|
+
let recovered = false;
|
|
5510
|
+
if (observations.some(isCompressedObservation)) {
|
|
5511
|
+
recovered = await recoverStaleSession(sdk, session.id);
|
|
5512
|
+
if (!recovered) continue;
|
|
5513
|
+
recoveredStaleSessions++;
|
|
5514
|
+
} else if (observations.length > 0) {
|
|
5515
|
+
logger.warn("Stale session has no compressed observations", { sessionId: session.id });
|
|
5516
|
+
continue;
|
|
5517
|
+
}
|
|
5274
5518
|
try {
|
|
5275
5519
|
await kv.delete(KV.sessions, session.id);
|
|
5276
5520
|
stats.staleSessions++;
|
|
@@ -5284,11 +5528,12 @@ function registerEvictFunction(sdk, kv) {
|
|
|
5284
5528
|
}
|
|
5285
5529
|
await recordAudit(kv, "delete", "mem::evict", [session.id], {
|
|
5286
5530
|
resource: "session",
|
|
5287
|
-
reason: "stale_session_without_summary",
|
|
5531
|
+
reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
|
|
5288
5532
|
dryRun
|
|
5289
5533
|
});
|
|
5290
5534
|
}
|
|
5291
5535
|
}
|
|
5536
|
+
if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
|
|
5292
5537
|
const projectObs = /* @__PURE__ */ new Map();
|
|
5293
5538
|
for (const session of sessions) {
|
|
5294
5539
|
const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
|
|
@@ -5967,7 +6212,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
5967
6212
|
|
|
5968
6213
|
//#endregion
|
|
5969
6214
|
//#region src/version.ts
|
|
5970
|
-
const VERSION = "0.9.
|
|
6215
|
+
const VERSION = "0.9.18";
|
|
5971
6216
|
|
|
5972
6217
|
//#endregion
|
|
5973
6218
|
//#region src/functions/export-import.ts
|
|
@@ -6101,7 +6346,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
6101
6346
|
"0.9.13",
|
|
6102
6347
|
"0.9.14",
|
|
6103
6348
|
"0.9.15",
|
|
6104
|
-
"0.9.16"
|
|
6349
|
+
"0.9.16",
|
|
6350
|
+
"0.9.17",
|
|
6351
|
+
"0.9.18"
|
|
6105
6352
|
]).has(importData.version)) return {
|
|
6106
6353
|
success: false,
|
|
6107
6354
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -19368,7 +19615,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
19368
19615
|
|
|
19369
19616
|
//#endregion
|
|
19370
19617
|
//#region src/viewer/server.ts
|
|
19618
|
+
function loadViewerFavicon() {
|
|
19619
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
19620
|
+
const candidates = [
|
|
19621
|
+
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
19622
|
+
join(base, "..", "viewer", "favicon.svg"),
|
|
19623
|
+
join(base, "viewer", "favicon.svg")
|
|
19624
|
+
];
|
|
19625
|
+
for (const path of candidates) try {
|
|
19626
|
+
return readFileSync(path);
|
|
19627
|
+
} catch {}
|
|
19628
|
+
return null;
|
|
19629
|
+
}
|
|
19371
19630
|
const ALLOWED_ORIGINS = (process.env.VIEWER_ALLOWED_ORIGINS || "http://localhost:3111,http://localhost:3113,http://127.0.0.1:3111,http://127.0.0.1:3113").split(",").map((o) => o.trim());
|
|
19631
|
+
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
19632
|
+
function buildAllowedHosts(origins, listenPort) {
|
|
19633
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
19634
|
+
for (const o of origins) try {
|
|
19635
|
+
const parsed = new URL(o);
|
|
19636
|
+
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
19637
|
+
} catch {}
|
|
19638
|
+
hosts.add(`localhost:${listenPort}`);
|
|
19639
|
+
hosts.add(`127.0.0.1:${listenPort}`);
|
|
19640
|
+
hosts.add(`[::1]:${listenPort}`);
|
|
19641
|
+
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
19642
|
+
return hosts;
|
|
19643
|
+
}
|
|
19644
|
+
function isHostAllowed(headerHost, allowed) {
|
|
19645
|
+
if (typeof headerHost !== "string") return false;
|
|
19646
|
+
const lower = headerHost.toLowerCase().trim();
|
|
19647
|
+
if (!lower) return false;
|
|
19648
|
+
return allowed.has(lower);
|
|
19649
|
+
}
|
|
19372
19650
|
function corsHeaders(req) {
|
|
19373
19651
|
const origin = req.headers.origin || "";
|
|
19374
19652
|
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
@@ -19412,7 +19690,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
|
|
|
19412
19690
|
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
19413
19691
|
const resolvedRestPort = restPort ?? port - 2;
|
|
19414
19692
|
const requestedPort = port;
|
|
19693
|
+
let allowedHosts = null;
|
|
19415
19694
|
const server = createServer(async (req, res) => {
|
|
19695
|
+
if (!allowedHosts) {
|
|
19696
|
+
const addr = server.address();
|
|
19697
|
+
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
19698
|
+
}
|
|
19699
|
+
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
19700
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
19701
|
+
res.end("forbidden host");
|
|
19702
|
+
return;
|
|
19703
|
+
}
|
|
19416
19704
|
const raw = req.url || "/";
|
|
19417
19705
|
const qIdx = raw.indexOf("?");
|
|
19418
19706
|
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
@@ -19441,6 +19729,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
|
19441
19729
|
res.end("viewer not found");
|
|
19442
19730
|
return;
|
|
19443
19731
|
}
|
|
19732
|
+
if (method === "GET" && pathname === "/favicon.svg") {
|
|
19733
|
+
const favicon = loadViewerFavicon();
|
|
19734
|
+
if (favicon) {
|
|
19735
|
+
res.writeHead(200, {
|
|
19736
|
+
"Content-Type": "image/svg+xml",
|
|
19737
|
+
"Cache-Control": "public, max-age=3600"
|
|
19738
|
+
});
|
|
19739
|
+
res.end(favicon);
|
|
19740
|
+
return;
|
|
19741
|
+
}
|
|
19742
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
19743
|
+
res.end("favicon not found");
|
|
19744
|
+
return;
|
|
19745
|
+
}
|
|
19444
19746
|
try {
|
|
19445
19747
|
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
19446
19748
|
} catch (err) {
|
|
@@ -19676,6 +19978,11 @@ async function main() {
|
|
|
19676
19978
|
serviceName: OTEL_CONFIG.serviceName,
|
|
19677
19979
|
serviceVersion: OTEL_CONFIG.serviceVersion,
|
|
19678
19980
|
metricsExportIntervalMs: OTEL_CONFIG.metricsExportIntervalMs
|
|
19981
|
+
},
|
|
19982
|
+
telemetry: {
|
|
19983
|
+
project_name: "agentmemory",
|
|
19984
|
+
language: "node",
|
|
19985
|
+
framework: "iii-sdk"
|
|
19679
19986
|
}
|
|
19680
19987
|
});
|
|
19681
19988
|
const kv = new StateKV(sdk);
|
|
@@ -19835,7 +20142,7 @@ async function main() {
|
|
|
19835
20142
|
console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
|
|
19836
20143
|
}
|
|
19837
20144
|
bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
19838
|
-
bootLog(`REST API:
|
|
20145
|
+
bootLog(`REST API: 121 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
|
|
19839
20146
|
bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
|
|
19840
20147
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
19841
20148
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|