@agentmemory/agentmemory 0.9.21 → 0.9.23
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 +7 -2
- package/README.md +288 -33
- package/dist/cli.d.mts +5 -1
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +128 -703
- package/dist/cli.mjs.map +1 -1
- package/dist/connect-Cf9bmBqO.mjs +1020 -0
- package/dist/connect-Cf9bmBqO.mjs.map +1 -0
- package/dist/hooks/notification.mjs +46 -21
- package/dist/hooks/notification.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +47 -21
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +57 -22
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +26 -2
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +19 -12
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/prompt-submit.mjs +39 -16
- package/dist/hooks/prompt-submit.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +26 -33
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +28 -3
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +14 -9
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-start.mjs +31 -4
- package/dist/hooks/subagent-start.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +45 -20
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +44 -21
- package/dist/hooks/task-completed.mjs.map +1 -1
- package/dist/iii-config.docker.yaml +3 -2
- package/dist/iii-config.yaml +11 -2
- package/dist/{image-refs-R3tin9MR.mjs → image-refs-CJS5B9Gq.mjs} +2 -2
- package/dist/{image-refs-R3tin9MR.mjs.map → image-refs-CJS5B9Gq.mjs.map} +1 -1
- package/dist/{image-store-DyrKZKqZ.mjs → image-store-CdE0amb1.mjs} +1 -1
- package/dist/index.mjs +866 -380
- package/dist/index.mjs.map +1 -1
- package/dist/logger-xlVlvCWX.mjs +43 -0
- package/dist/logger-xlVlvCWX.mjs.map +1 -0
- package/dist/schema-BkALl7Z_.mjs +74 -0
- package/dist/schema-BkALl7Z_.mjs.map +1 -0
- package/dist/{src-D5arboxc.mjs → src-DvS3bhMe.mjs} +844 -395
- package/dist/src-DvS3bhMe.mjs.map +1 -0
- package/dist/{standalone-C7BgzzIN.mjs → standalone-DHQcPX_g.mjs} +107 -14
- package/dist/standalone-DHQcPX_g.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +108 -12
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-CRTWUFw9.mjs → tools-registry-DJizX9Az.mjs} +51 -12
- package/dist/tools-registry-DJizX9Az.mjs.map +1 -0
- package/dist/version-BPfyI4Kc.mjs +6 -0
- package/dist/version-BPfyI4Kc.mjs.map +1 -0
- package/dist/viewer/index.html +85 -10
- package/iii-config.docker.yaml +3 -2
- package/iii-config.yaml +11 -2
- package/package.json +6 -4
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/.mcp.copilot.json +15 -0
- package/plugin/.mcp.json +3 -2
- package/plugin/hooks/hooks.copilot.json +72 -0
- package/plugin/opencode/agentmemory-capture.ts +34 -9
- package/plugin/plugin.json +15 -0
- package/plugin/scripts/diagnostics.d.mts +17 -0
- package/plugin/scripts/diagnostics.d.mts.map +1 -0
- package/plugin/scripts/diagnostics.mjs.map +1 -0
- package/plugin/scripts/notification.mjs +46 -21
- package/plugin/scripts/notification.mjs.map +1 -1
- package/plugin/scripts/post-tool-failure.mjs +47 -21
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +57 -22
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +26 -2
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +19 -12
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/prompt-submit.mjs +39 -16
- package/plugin/scripts/prompt-submit.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +26 -33
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +28 -3
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +14 -9
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-start.mjs +31 -4
- package/plugin/scripts/subagent-start.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +45 -20
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +44 -21
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/dist/src-D5arboxc.mjs.map +0 -1
- package/dist/standalone-C7BgzzIN.mjs.map +0 -1
- package/dist/tools-registry-CRTWUFw9.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { TriggerAction, registerWorker } from "iii-sdk";
|
|
4
|
-
import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -40,6 +40,7 @@ function safeParseInt(value, fallback) {
|
|
|
40
40
|
}
|
|
41
41
|
const DATA_DIR = join(homedir(), ".agentmemory");
|
|
42
42
|
const ENV_FILE = join(DATA_DIR, ".env");
|
|
43
|
+
let warnPremiumModelShown = false;
|
|
43
44
|
function loadEnvFile() {
|
|
44
45
|
if (!existsSync(ENV_FILE)) return {};
|
|
45
46
|
const content = readFileSync(ENV_FILE, "utf-8");
|
|
@@ -93,11 +94,18 @@ function detectProvider(env) {
|
|
|
93
94
|
maxTokens
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
|
-
if (hasRealValue(env["OPENROUTER_API_KEY"]))
|
|
97
|
-
|
|
98
|
-
model
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
if (hasRealValue(env["OPENROUTER_API_KEY"])) {
|
|
98
|
+
const model = env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514";
|
|
99
|
+
if (!warnPremiumModelShown && /sonnet|opus|gpt-4o(?!.*mini)|gpt-4-turbo/i.test(model) && env["AGENTMEMORY_SUPPRESS_COST_WARNING"] !== "1" && env["AGENTMEMORY_SUPPRESS_COST_WARNING"] !== "true") {
|
|
100
|
+
warnPremiumModelShown = true;
|
|
101
|
+
process.stderr.write(`[agentmemory] OPENROUTER_MODEL=${model} is in the premium tier. Background compression on this model can cost $5+/day under active use. Cheaper alternatives with comparable quality for memory compression: deepseek/deepseek-v4-pro, deepseek/deepseek-chat, qwen/qwen3-coder. See README "Cost-aware model selection" for the full table. Set AGENTMEMORY_SUPPRESS_COST_WARNING=1 to silence.\n`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
provider: "openrouter",
|
|
105
|
+
model,
|
|
106
|
+
maxTokens
|
|
107
|
+
};
|
|
108
|
+
}
|
|
101
109
|
if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
|
|
102
110
|
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");
|
|
103
111
|
return {
|
|
@@ -175,8 +183,8 @@ function loadClaudeBridgeConfig() {
|
|
|
175
183
|
const lineBudget = safeParseInt(env["CLAUDE_MEMORY_LINE_BUDGET"], 200);
|
|
176
184
|
let memoryFilePath = "";
|
|
177
185
|
if (enabled && projectPath) {
|
|
178
|
-
const safePath = projectPath.replace(/[/\\]/g, "-")
|
|
179
|
-
memoryFilePath = join(homedir(), ".claude", "projects", safePath, "
|
|
186
|
+
const safePath = projectPath.replace(/[/\\]/g, "-");
|
|
187
|
+
memoryFilePath = join(homedir(), ".claude", "projects", safePath, "MEMORY.md");
|
|
180
188
|
}
|
|
181
189
|
return {
|
|
182
190
|
enabled,
|
|
@@ -196,6 +204,23 @@ function loadTeamConfig() {
|
|
|
196
204
|
mode: env["TEAM_MODE"] === "shared" ? "shared" : "private"
|
|
197
205
|
};
|
|
198
206
|
}
|
|
207
|
+
function loadAgentScope() {
|
|
208
|
+
const env = getMergedEnv();
|
|
209
|
+
const raw = env["AGENT_ID"];
|
|
210
|
+
if (!raw) return null;
|
|
211
|
+
const agentId = raw.trim().slice(0, 128);
|
|
212
|
+
if (!agentId) return null;
|
|
213
|
+
return {
|
|
214
|
+
agentId,
|
|
215
|
+
mode: env["AGENTMEMORY_AGENT_SCOPE"] === "isolated" ? "isolated" : "shared"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function getAgentId() {
|
|
219
|
+
return loadAgentScope()?.agentId;
|
|
220
|
+
}
|
|
221
|
+
function isAgentScopeIsolated() {
|
|
222
|
+
return loadAgentScope()?.mode === "isolated";
|
|
223
|
+
}
|
|
199
224
|
function loadSnapshotConfig() {
|
|
200
225
|
const env = getMergedEnv();
|
|
201
226
|
return {
|
|
@@ -208,7 +233,17 @@ function isGraphExtractionEnabled() {
|
|
|
208
233
|
return getMergedEnv()["GRAPH_EXTRACTION_ENABLED"] === "true";
|
|
209
234
|
}
|
|
210
235
|
function isConsolidationEnabled() {
|
|
211
|
-
|
|
236
|
+
const env = getMergedEnv();
|
|
237
|
+
const explicit = env["CONSOLIDATION_ENABLED"];
|
|
238
|
+
if (explicit === "false" || explicit === "0") return false;
|
|
239
|
+
if (explicit === "true" || explicit === "1") return true;
|
|
240
|
+
return hasLLMProviderConfigured(env);
|
|
241
|
+
}
|
|
242
|
+
function hasLLMProviderConfigured(env) {
|
|
243
|
+
const provider = (env["AGENTMEMORY_PROVIDER"] || "").toLowerCase();
|
|
244
|
+
if (provider === "noop") return false;
|
|
245
|
+
const openaiKeyForLlm = env["OPENAI_API_KEY"] && (env["OPENAI_API_KEY_FOR_LLM"] || "").toLowerCase() !== "false";
|
|
246
|
+
return Boolean(env["ANTHROPIC_API_KEY"] || openaiKeyForLlm || env["OPENROUTER_API_KEY"] || env["GEMINI_API_KEY"] || env["GOOGLE_API_KEY"] || env["MINIMAX_API_KEY"] || env["OPENAI_BASE_URL"] || provider === "agent-sdk");
|
|
212
247
|
}
|
|
213
248
|
function isAutoCompressEnabled() {
|
|
214
249
|
return getMergedEnv()["AGENTMEMORY_AUTO_COMPRESS"] === "true";
|
|
@@ -453,13 +488,25 @@ function v1AzureUrl(baseUrl, path) {
|
|
|
453
488
|
url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
|
|
454
489
|
return url.toString();
|
|
455
490
|
}
|
|
491
|
+
function appendOpenAIRoute(baseUrl, route) {
|
|
492
|
+
const trimmedBase = baseUrl.replace(/\/+$/, "");
|
|
493
|
+
const cleanRoute = route.startsWith("/") ? route : `/${route}`;
|
|
494
|
+
let pathname;
|
|
495
|
+
try {
|
|
496
|
+
pathname = new URL(trimmedBase).pathname.replace(/\/+$/, "");
|
|
497
|
+
} catch {
|
|
498
|
+
return `${trimmedBase}/v1${cleanRoute}`;
|
|
499
|
+
}
|
|
500
|
+
if (pathname === "" || pathname === "/") return `${trimmedBase}/v1${cleanRoute}`;
|
|
501
|
+
return `${trimmedBase}${cleanRoute}`;
|
|
502
|
+
}
|
|
456
503
|
function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
|
|
457
504
|
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
|
|
458
|
-
return
|
|
505
|
+
return appendOpenAIRoute(baseUrl, "/chat/completions");
|
|
459
506
|
}
|
|
460
507
|
function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
|
|
461
508
|
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
|
|
462
|
-
return
|
|
509
|
+
return appendOpenAIRoute(baseUrl, "/embeddings");
|
|
463
510
|
}
|
|
464
511
|
function buildAuthHeaders(apiKey, isAzure) {
|
|
465
512
|
if (isAzure) return {
|
|
@@ -541,6 +588,7 @@ var OpenAIProvider = class {
|
|
|
541
588
|
const body = {
|
|
542
589
|
model: this.model,
|
|
543
590
|
max_tokens: this.maxTokens,
|
|
591
|
+
stream: false,
|
|
544
592
|
messages: [{
|
|
545
593
|
role: "system",
|
|
546
594
|
content: systemPrompt
|
|
@@ -569,7 +617,7 @@ var OpenAIProvider = class {
|
|
|
569
617
|
const message = data.choices?.[0]?.message;
|
|
570
618
|
const content = message?.content;
|
|
571
619
|
if (content) return content;
|
|
572
|
-
const reasoning = message?.reasoning;
|
|
620
|
+
const reasoning = message?.reasoning ?? message?.reasoning_content;
|
|
573
621
|
if (reasoning) return reasoning;
|
|
574
622
|
throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
575
623
|
}
|
|
@@ -848,16 +896,30 @@ function resolveDimensions(model, override) {
|
|
|
848
896
|
* `api-key` header instead of `Authorization: Bearer`.
|
|
849
897
|
*
|
|
850
898
|
* Required env vars:
|
|
851
|
-
* OPENAI_API_KEY
|
|
899
|
+
* OPENAI_API_KEY — API key (fallback for OPENAI_EMBEDDING_API_KEY)
|
|
852
900
|
*
|
|
853
901
|
* Optional:
|
|
854
|
-
* OPENAI_BASE_URL
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
902
|
+
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
|
|
903
|
+
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
|
|
904
|
+
* OPENAI_EMBEDDING_BASE_URL — embedding-specific base URL override (defaults
|
|
905
|
+
* to OPENAI_BASE_URL). Lets operators run
|
|
906
|
+
* embeddings on a separate endpoint from chat —
|
|
907
|
+
* e.g. local Ollama / LM Studio / llama.cpp /
|
|
908
|
+
* vLLM at http://localhost:1234 for unlimited
|
|
909
|
+
* free embeddings, while keeping chat
|
|
910
|
+
* completions on a rate-limited but high-quality
|
|
911
|
+
* hosted provider. Azure detection runs on
|
|
912
|
+
* whichever URL ends up selected.
|
|
913
|
+
* OPENAI_EMBEDDING_API_KEY — separate API key for the embedding endpoint
|
|
914
|
+
* (defaults to OPENAI_API_KEY). Useful when the
|
|
915
|
+
* embedding endpoint requires a different key
|
|
916
|
+
* or no key at all (set to e.g. "local" for
|
|
917
|
+
* endpoints that ignore Authorization).
|
|
918
|
+
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
|
|
919
|
+
* OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
|
|
920
|
+
* OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
|
|
921
|
+
* custom / self-hosted models not in the
|
|
922
|
+
* MODEL_DIMENSIONS table above)
|
|
861
923
|
*/
|
|
862
924
|
var OpenAIEmbeddingProvider = class {
|
|
863
925
|
name = "openai";
|
|
@@ -868,9 +930,9 @@ var OpenAIEmbeddingProvider = class {
|
|
|
868
930
|
isAzure;
|
|
869
931
|
azureApiVersion;
|
|
870
932
|
constructor(apiKey) {
|
|
871
|
-
this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
|
|
872
|
-
if (!this.apiKey) throw new Error("
|
|
873
|
-
this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
|
|
933
|
+
this.apiKey = apiKey || getEnvVar("OPENAI_EMBEDDING_API_KEY") || getEnvVar("OPENAI_API_KEY") || "";
|
|
934
|
+
if (!this.apiKey) throw new Error("API key is required (via constructor, OPENAI_EMBEDDING_API_KEY, or OPENAI_API_KEY)");
|
|
935
|
+
this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_EMBEDDING_BASE_URL") || getEnvVar("OPENAI_BASE_URL"));
|
|
874
936
|
this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
|
|
875
937
|
this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
|
|
876
938
|
this.isAzure = detectAzure(this.baseUrl);
|
|
@@ -1317,10 +1379,11 @@ function jaccardSimilarity(a, b) {
|
|
|
1317
1379
|
//#endregion
|
|
1318
1380
|
//#region src/state/vector-index.ts
|
|
1319
1381
|
function float32ToBase64(arr) {
|
|
1320
|
-
return Buffer.from(arr.buffer).toString("base64");
|
|
1382
|
+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString("base64");
|
|
1321
1383
|
}
|
|
1322
1384
|
function base64ToFloat32(b64) {
|
|
1323
|
-
|
|
1385
|
+
const buf = Buffer.from(b64, "base64");
|
|
1386
|
+
return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
1324
1387
|
}
|
|
1325
1388
|
function cosineSimilarity(a, b) {
|
|
1326
1389
|
if (a.length !== b.length) return 0;
|
|
@@ -1441,7 +1504,7 @@ var VectorIndex = class VectorIndex {
|
|
|
1441
1504
|
function memoryToObservation(memory) {
|
|
1442
1505
|
return {
|
|
1443
1506
|
id: memory.id,
|
|
1444
|
-
sessionId: memory.sessionIds[0] ?? "memory",
|
|
1507
|
+
sessionId: memory.sessionIds?.[0] ?? "memory",
|
|
1445
1508
|
timestamp: memory.createdAt,
|
|
1446
1509
|
type: "decision",
|
|
1447
1510
|
title: memory.title,
|
|
@@ -2514,6 +2577,24 @@ var SearchIndex = class SearchIndex {
|
|
|
2514
2577
|
has(id) {
|
|
2515
2578
|
return this.entries.has(id);
|
|
2516
2579
|
}
|
|
2580
|
+
remove(id) {
|
|
2581
|
+
const entry = this.entries.get(id);
|
|
2582
|
+
if (!entry) return;
|
|
2583
|
+
const termFreq = this.docTermCounts.get(id);
|
|
2584
|
+
if (termFreq) {
|
|
2585
|
+
for (const term of termFreq.keys()) {
|
|
2586
|
+
const postingList = this.invertedIndex.get(term);
|
|
2587
|
+
if (postingList) {
|
|
2588
|
+
postingList.delete(id);
|
|
2589
|
+
if (postingList.size === 0) this.invertedIndex.delete(term);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
this.docTermCounts.delete(id);
|
|
2593
|
+
}
|
|
2594
|
+
this.totalDocLength = Math.max(0, this.totalDocLength - entry.termCount);
|
|
2595
|
+
this.entries.delete(id);
|
|
2596
|
+
this.sortedTerms = null;
|
|
2597
|
+
}
|
|
2517
2598
|
search(query, limit = 20) {
|
|
2518
2599
|
const rawTerms = this.tokenize(query.toLowerCase());
|
|
2519
2600
|
if (rawTerms.length === 0) return [];
|
|
@@ -2866,6 +2947,7 @@ function buildSyntheticCompression(raw) {
|
|
|
2866
2947
|
};
|
|
2867
2948
|
if (raw.modality) result.modality = raw.modality;
|
|
2868
2949
|
if (raw.imageData) result.imageData = raw.imageData;
|
|
2950
|
+
if (raw.agentId) result.agentId = raw.agentId;
|
|
2869
2951
|
return result;
|
|
2870
2952
|
}
|
|
2871
2953
|
|
|
@@ -2955,6 +3037,16 @@ function setVectorIndex(idx) {
|
|
|
2955
3037
|
function setEmbeddingProvider(provider) {
|
|
2956
3038
|
currentEmbeddingProvider = provider;
|
|
2957
3039
|
}
|
|
3040
|
+
function vectorIndexRemove(id) {
|
|
3041
|
+
vectorIndex?.remove(id);
|
|
3042
|
+
}
|
|
3043
|
+
let indexPersistence = null;
|
|
3044
|
+
function setIndexPersistence(p) {
|
|
3045
|
+
indexPersistence = p;
|
|
3046
|
+
}
|
|
3047
|
+
async function flushIndexSave() {
|
|
3048
|
+
await indexPersistence?.save();
|
|
3049
|
+
}
|
|
2958
3050
|
const EMBED_MAX_CHARS = 16e3;
|
|
2959
3051
|
function clipEmbedInput(text) {
|
|
2960
3052
|
if (text.length <= EMBED_MAX_CHARS) return text;
|
|
@@ -3084,7 +3176,7 @@ async function rebuildIndex(kv) {
|
|
|
3084
3176
|
idx.add(memoryToObservation(memory));
|
|
3085
3177
|
await enqueue({
|
|
3086
3178
|
id: memory.id,
|
|
3087
|
-
sessionId: memory.sessionIds[0] ?? "memory",
|
|
3179
|
+
sessionId: memory.sessionIds?.[0] ?? "memory",
|
|
3088
3180
|
text: memory.title + " " + memory.content,
|
|
3089
3181
|
context: {
|
|
3090
3182
|
kind: "memory",
|
|
@@ -3143,8 +3235,8 @@ function registerSearchFunction(sdk, kv) {
|
|
|
3143
3235
|
if (!Number.isInteger(data.limit) || data.limit < 1) throw new Error("mem::search: limit must be a positive integer");
|
|
3144
3236
|
effectiveLimit = Math.min(data.limit, MAX_LIMIT);
|
|
3145
3237
|
}
|
|
3146
|
-
const projectFilter = typeof data.project === "string" && data.project.length > 0 ? data.project : void 0;
|
|
3147
|
-
const cwdFilter = typeof data.cwd === "string" && data.cwd.length > 0 ? data.cwd : void 0;
|
|
3238
|
+
const projectFilter = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
|
|
3239
|
+
const cwdFilter = typeof data.cwd === "string" && data.cwd.trim().length > 0 ? data.cwd.trim() : void 0;
|
|
3148
3240
|
const format = typeof data.format === "string" ? data.format : "full";
|
|
3149
3241
|
if (![
|
|
3150
3242
|
"full",
|
|
@@ -3170,14 +3262,25 @@ function registerSearchFunction(sdk, kv) {
|
|
|
3170
3262
|
sessionCache.set(sessionId, s ?? null);
|
|
3171
3263
|
return s ?? null;
|
|
3172
3264
|
};
|
|
3265
|
+
const memoryProjectCache = /* @__PURE__ */ new Map();
|
|
3266
|
+
const loadMemoryProject = async (obsId) => {
|
|
3267
|
+
if (memoryProjectCache.has(obsId)) return memoryProjectCache.get(obsId);
|
|
3268
|
+
const proj = (await kv.get(KV.memories, obsId).catch(() => null))?.project ?? null;
|
|
3269
|
+
memoryProjectCache.set(obsId, proj);
|
|
3270
|
+
return proj;
|
|
3271
|
+
};
|
|
3173
3272
|
const candidates = [];
|
|
3174
3273
|
for (const r of results) {
|
|
3175
3274
|
if (candidates.length >= effectiveLimit) break;
|
|
3176
3275
|
if (filtering) {
|
|
3177
3276
|
const s = await loadSession(r.sessionId);
|
|
3178
|
-
if (
|
|
3179
|
-
|
|
3180
|
-
|
|
3277
|
+
if (s) {
|
|
3278
|
+
if (projectFilter && s.project !== projectFilter) continue;
|
|
3279
|
+
if (cwdFilter && s.cwd !== cwdFilter) continue;
|
|
3280
|
+
} else if (projectFilter) {
|
|
3281
|
+
const memProject = await loadMemoryProject(r.obsId);
|
|
3282
|
+
if (memProject !== null && memProject !== projectFilter) continue;
|
|
3283
|
+
}
|
|
3181
3284
|
}
|
|
3182
3285
|
candidates.push(r);
|
|
3183
3286
|
}
|
|
@@ -3349,6 +3452,9 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
3349
3452
|
error: `Session observation limit reached (${maxObservationsPerSession})`
|
|
3350
3453
|
};
|
|
3351
3454
|
}
|
|
3455
|
+
const existingSession = await kv.get(KV.sessions, payload.sessionId);
|
|
3456
|
+
const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
|
|
3457
|
+
if (inheritedAgentId) raw.agentId = inheritedAgentId;
|
|
3352
3458
|
if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
|
|
3353
3459
|
const { saveImageToDisk } = await Promise.resolve().then(() => image_store_exports);
|
|
3354
3460
|
const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
|
|
@@ -3400,7 +3506,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
3400
3506
|
},
|
|
3401
3507
|
action: TriggerAction.Void()
|
|
3402
3508
|
});
|
|
3403
|
-
const session =
|
|
3509
|
+
const session = existingSession;
|
|
3404
3510
|
if (session) {
|
|
3405
3511
|
const updates = [{
|
|
3406
3512
|
type: "set",
|
|
@@ -3420,6 +3526,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
3420
3526
|
});
|
|
3421
3527
|
}
|
|
3422
3528
|
await kv.update(KV.sessions, payload.sessionId, updates);
|
|
3529
|
+
} else if (typeof payload.project === "string" && payload.project.trim().length > 0 && typeof payload.cwd === "string" && payload.cwd.trim().length > 0) {
|
|
3530
|
+
const trimmedPrompt = typeof raw.userPrompt === "string" ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
|
|
3531
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3532
|
+
await kv.set(KV.sessions, payload.sessionId, {
|
|
3533
|
+
id: payload.sessionId,
|
|
3534
|
+
project: payload.project,
|
|
3535
|
+
cwd: payload.cwd,
|
|
3536
|
+
startedAt: payload.timestamp ?? ts,
|
|
3537
|
+
updatedAt: ts,
|
|
3538
|
+
status: "active",
|
|
3539
|
+
observationCount: 1,
|
|
3540
|
+
...inheritedAgentId ? { agentId: inheritedAgentId } : {},
|
|
3541
|
+
...trimmedPrompt && trimmedPrompt.length > 0 ? { firstPrompt: trimmedPrompt } : {}
|
|
3542
|
+
});
|
|
3423
3543
|
}
|
|
3424
3544
|
if (isAutoCompressEnabled()) await sdk.trigger({
|
|
3425
3545
|
function_id: "mem::compress",
|
|
@@ -3912,10 +4032,10 @@ const DEFAULT_SLOTS = [
|
|
|
3912
4032
|
}
|
|
3913
4033
|
];
|
|
3914
4034
|
function isSlotsEnabled() {
|
|
3915
|
-
return
|
|
4035
|
+
return getEnvVar("AGENTMEMORY_SLOTS") === "true";
|
|
3916
4036
|
}
|
|
3917
4037
|
function isReflectEnabled() {
|
|
3918
|
-
return
|
|
4038
|
+
return getEnvVar("AGENTMEMORY_REFLECT") === "true";
|
|
3919
4039
|
}
|
|
3920
4040
|
function scopeKv(scope) {
|
|
3921
4041
|
return scope === "global" ? KV.globalSlots : KV.slots;
|
|
@@ -4679,7 +4799,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
|
|
|
4679
4799
|
confidence: qualityScore / 100,
|
|
4680
4800
|
...hasImage ? { modality: data.raw.modality } : {},
|
|
4681
4801
|
...imageDescription ? { imageDescription } : {},
|
|
4682
|
-
...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
|
|
4802
|
+
...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
|
|
4803
|
+
...data.raw.agentId ? { agentId: data.raw.agentId } : {}
|
|
4683
4804
|
};
|
|
4684
4805
|
await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
|
|
4685
4806
|
try {
|
|
@@ -5179,8 +5300,78 @@ function isAllowedPath(dbPath) {
|
|
|
5179
5300
|
const resolved = resolve(dbPath);
|
|
5180
5301
|
return ALLOWED_DIRS.some((dir) => resolved.startsWith(dir + "/"));
|
|
5181
5302
|
}
|
|
5303
|
+
async function inferMemoryProjects(kv, dryRun = false) {
|
|
5304
|
+
const memories = await kv.list(KV.memories);
|
|
5305
|
+
const sessionCache = /* @__PURE__ */ new Map();
|
|
5306
|
+
const loadSession = async (sid) => {
|
|
5307
|
+
if (sessionCache.has(sid)) return sessionCache.get(sid);
|
|
5308
|
+
const s = await kv.get(KV.sessions, sid).catch(() => null);
|
|
5309
|
+
sessionCache.set(sid, s);
|
|
5310
|
+
return s;
|
|
5311
|
+
};
|
|
5312
|
+
let updated = 0;
|
|
5313
|
+
let skipped = 0;
|
|
5314
|
+
let ambiguous = 0;
|
|
5315
|
+
for (const memory of memories) {
|
|
5316
|
+
if (memory.project) {
|
|
5317
|
+
skipped++;
|
|
5318
|
+
continue;
|
|
5319
|
+
}
|
|
5320
|
+
const sessionIds = memory.sessionIds ?? [];
|
|
5321
|
+
if (sessionIds.length === 0) {
|
|
5322
|
+
ambiguous++;
|
|
5323
|
+
continue;
|
|
5324
|
+
}
|
|
5325
|
+
const projects = [];
|
|
5326
|
+
for (const sid of sessionIds) {
|
|
5327
|
+
const session = await loadSession(sid);
|
|
5328
|
+
if (session?.project) projects.push(session.project);
|
|
5329
|
+
}
|
|
5330
|
+
if (projects.length === 0) {
|
|
5331
|
+
ambiguous++;
|
|
5332
|
+
continue;
|
|
5333
|
+
}
|
|
5334
|
+
const freq = /* @__PURE__ */ new Map();
|
|
5335
|
+
for (const p of projects) freq.set(p, (freq.get(p) ?? 0) + 1);
|
|
5336
|
+
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
|
|
5337
|
+
const [topProject, topCount] = sorted[0];
|
|
5338
|
+
if (topCount <= projects.length / 2 && sorted.length > 1) {
|
|
5339
|
+
ambiguous++;
|
|
5340
|
+
continue;
|
|
5341
|
+
}
|
|
5342
|
+
if (!dryRun) {
|
|
5343
|
+
memory.project = topProject;
|
|
5344
|
+
await kv.set(KV.memories, memory.id, memory);
|
|
5345
|
+
}
|
|
5346
|
+
updated++;
|
|
5347
|
+
}
|
|
5348
|
+
logger.info("inferMemoryProjects complete", {
|
|
5349
|
+
updated,
|
|
5350
|
+
skipped,
|
|
5351
|
+
ambiguous,
|
|
5352
|
+
dryRun
|
|
5353
|
+
});
|
|
5354
|
+
return {
|
|
5355
|
+
updated,
|
|
5356
|
+
skipped,
|
|
5357
|
+
ambiguous
|
|
5358
|
+
};
|
|
5359
|
+
}
|
|
5182
5360
|
function registerMigrateFunction(sdk, kv) {
|
|
5183
5361
|
sdk.registerFunction("mem::migrate", async (data) => {
|
|
5362
|
+
if (data.step === "infer-memory-projects") {
|
|
5363
|
+
const dryRun = data.dryRun ?? false;
|
|
5364
|
+
logger.info("Migration step: infer-memory-projects", { dryRun });
|
|
5365
|
+
return {
|
|
5366
|
+
success: true,
|
|
5367
|
+
step: "infer-memory-projects",
|
|
5368
|
+
...await inferMemoryProjects(kv, dryRun)
|
|
5369
|
+
};
|
|
5370
|
+
}
|
|
5371
|
+
if (!data.dbPath) return {
|
|
5372
|
+
success: false,
|
|
5373
|
+
error: "Either step or dbPath is required"
|
|
5374
|
+
};
|
|
5184
5375
|
logger.info("Migration started", { dbPath: data.dbPath });
|
|
5185
5376
|
if (!isAllowedPath(data.dbPath)) return {
|
|
5186
5377
|
success: false,
|
|
@@ -5456,9 +5647,10 @@ function registerConsolidateFunction(sdk, kv, provider) {
|
|
|
5456
5647
|
llmCallCount++;
|
|
5457
5648
|
const parsed = parseMemoryXml(response, sessionIds);
|
|
5458
5649
|
if (!parsed) continue;
|
|
5459
|
-
const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase());
|
|
5460
5650
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5461
5651
|
const obsIds = [...new Set(top.map((o) => o.id))];
|
|
5652
|
+
const scopedProject = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
|
|
5653
|
+
const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase() && (!scopedProject || !m.project || m.project === scopedProject));
|
|
5462
5654
|
if (existingMatch) {
|
|
5463
5655
|
existingMatch.isLatest = false;
|
|
5464
5656
|
await kv.set(KV.memories, existingMatch.id, existingMatch);
|
|
@@ -5475,7 +5667,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
|
|
|
5475
5667
|
parentId: existingMatch.id,
|
|
5476
5668
|
supersedes: [existingMatch.id, ...existingMatch.supersedes || []],
|
|
5477
5669
|
sourceObservationIds: obsIds,
|
|
5478
|
-
isLatest: true
|
|
5670
|
+
isLatest: true,
|
|
5671
|
+
...scopedProject !== void 0 && { project: scopedProject }
|
|
5479
5672
|
};
|
|
5480
5673
|
await kv.set(KV.memories, evolved.id, evolved);
|
|
5481
5674
|
await recordAudit(kv, "evolve", "mem::consolidate", [evolved.id], {
|
|
@@ -5494,7 +5687,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
|
|
|
5494
5687
|
...parsed,
|
|
5495
5688
|
sourceObservationIds: obsIds,
|
|
5496
5689
|
version: 1,
|
|
5497
|
-
isLatest: true
|
|
5690
|
+
isLatest: true,
|
|
5691
|
+
...scopedProject !== void 0 && { project: scopedProject }
|
|
5498
5692
|
};
|
|
5499
5693
|
await kv.set(KV.memories, memory.id, memory);
|
|
5500
5694
|
await recordAudit(kv, "remember", "mem::consolidate", [memory.id], {
|
|
@@ -5635,6 +5829,7 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5635
5829
|
"fact"
|
|
5636
5830
|
]).has(data.type || "") ? data.type : "fact";
|
|
5637
5831
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5832
|
+
const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
|
|
5638
5833
|
return withKeyedLock("mem:remember", async () => {
|
|
5639
5834
|
const existingMemories = await kv.list(KV.memories);
|
|
5640
5835
|
let supersededId;
|
|
@@ -5643,6 +5838,7 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5643
5838
|
const lowerContent = data.content.toLowerCase();
|
|
5644
5839
|
for (const existing of existingMemories) {
|
|
5645
5840
|
if (existing.isLatest === false) continue;
|
|
5841
|
+
if (project && existing.project && existing.project !== project) continue;
|
|
5646
5842
|
if (jaccardSimilarity(lowerContent, existing.content.toLowerCase()) > .7) {
|
|
5647
5843
|
supersededId = existing.id;
|
|
5648
5844
|
supersededVersion = existing.version ?? 1;
|
|
@@ -5650,6 +5846,7 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5650
5846
|
break;
|
|
5651
5847
|
}
|
|
5652
5848
|
}
|
|
5849
|
+
const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
|
|
5653
5850
|
const memory = {
|
|
5654
5851
|
id: generateId("mem"),
|
|
5655
5852
|
createdAt: now,
|
|
@@ -5665,7 +5862,9 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5665
5862
|
parentId: supersededId,
|
|
5666
5863
|
supersedes: supersededId ? [supersededId] : [],
|
|
5667
5864
|
sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
|
|
5668
|
-
isLatest: true
|
|
5865
|
+
isLatest: true,
|
|
5866
|
+
...callAgentId ? { agentId: callAgentId } : {},
|
|
5867
|
+
...project !== void 0 && { project }
|
|
5669
5868
|
};
|
|
5670
5869
|
if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
|
|
5671
5870
|
if (supersededMemory) {
|
|
@@ -5681,7 +5880,7 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5681
5880
|
error: err instanceof Error ? err.message : String(err)
|
|
5682
5881
|
});
|
|
5683
5882
|
}
|
|
5684
|
-
await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
|
|
5883
|
+
await vectorIndexAddGuarded(memory.id, memory.sessionIds?.[0] ?? "memory", memory.title + " " + memory.content, {
|
|
5685
5884
|
kind: "memory",
|
|
5686
5885
|
logId: memory.id
|
|
5687
5886
|
});
|
|
@@ -5692,7 +5891,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5692
5891
|
});
|
|
5693
5892
|
logger.info("Memory saved", {
|
|
5694
5893
|
memId: memory.id,
|
|
5695
|
-
type: memory.type
|
|
5894
|
+
type: memory.type,
|
|
5895
|
+
project: memory.project
|
|
5696
5896
|
});
|
|
5697
5897
|
return {
|
|
5698
5898
|
success: true,
|
|
@@ -5711,6 +5911,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5711
5911
|
await kv.delete(KV.memories, data.memoryId);
|
|
5712
5912
|
if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
5713
5913
|
await deleteAccessLog(kv, data.memoryId);
|
|
5914
|
+
getSearchIndex().remove(data.memoryId);
|
|
5915
|
+
vectorIndexRemove(data.memoryId);
|
|
5714
5916
|
deletedMemoryIds.push(data.memoryId);
|
|
5715
5917
|
deleted++;
|
|
5716
5918
|
}
|
|
@@ -5719,6 +5921,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5719
5921
|
await kv.delete(KV.observations(data.sessionId), obsId);
|
|
5720
5922
|
if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5721
5923
|
if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5924
|
+
getSearchIndex().remove(obsId);
|
|
5925
|
+
vectorIndexRemove(obsId);
|
|
5722
5926
|
deletedObservationIds.push(obsId);
|
|
5723
5927
|
deleted++;
|
|
5724
5928
|
}
|
|
@@ -5728,6 +5932,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5728
5932
|
await kv.delete(KV.observations(data.sessionId), obs.id);
|
|
5729
5933
|
if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5730
5934
|
if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5935
|
+
getSearchIndex().remove(obs.id);
|
|
5936
|
+
vectorIndexRemove(obs.id);
|
|
5731
5937
|
deletedObservationIds.push(obs.id);
|
|
5732
5938
|
deleted++;
|
|
5733
5939
|
}
|
|
@@ -5736,14 +5942,17 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5736
5942
|
deletedSession = true;
|
|
5737
5943
|
deleted += 2;
|
|
5738
5944
|
}
|
|
5739
|
-
if (deleted > 0)
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5945
|
+
if (deleted > 0) {
|
|
5946
|
+
await flushIndexSave();
|
|
5947
|
+
await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
|
|
5948
|
+
sessionId: data.sessionId,
|
|
5949
|
+
deleted,
|
|
5950
|
+
memoriesDeleted: deletedMemoryIds.length,
|
|
5951
|
+
observationsDeleted: deletedObservationIds.length,
|
|
5952
|
+
sessionDeleted: deletedSession,
|
|
5953
|
+
reason: "user-initiated forget"
|
|
5954
|
+
});
|
|
5955
|
+
}
|
|
5747
5956
|
logger.info("Memory forgotten", { deleted });
|
|
5748
5957
|
return {
|
|
5749
5958
|
success: true,
|
|
@@ -6258,6 +6467,9 @@ async function findByKeyword(kv, keyword, project) {
|
|
|
6258
6467
|
const LESSON_CONTENT_PREVIEW_CHARS = 240;
|
|
6259
6468
|
function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
6260
6469
|
sdk.registerFunction("mem::smart-search", async (data) => {
|
|
6470
|
+
const isolated = isAgentScopeIsolated();
|
|
6471
|
+
const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
|
|
6472
|
+
const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
|
|
6261
6473
|
if (data.expandIds && data.expandIds.length > 0) {
|
|
6262
6474
|
const raw = data.expandIds.slice(0, 20);
|
|
6263
6475
|
const items = raw.map((entry) => {
|
|
@@ -6278,17 +6490,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
6278
6490
|
observation: obs
|
|
6279
6491
|
} : null)));
|
|
6280
6492
|
for (const r of results) if (r) expanded.push(r);
|
|
6281
|
-
|
|
6493
|
+
const scoped = filterAgentId ? expanded.filter((e) => e.observation.agentId === filterAgentId) : expanded;
|
|
6494
|
+
recordAccessBatch(kv, scoped.map((e) => e.observation.id));
|
|
6282
6495
|
const truncated = data.expandIds.length > raw.length;
|
|
6283
6496
|
logger.info("Smart search expanded", {
|
|
6284
6497
|
requested: data.expandIds.length,
|
|
6285
6498
|
attempted: raw.length,
|
|
6286
|
-
returned:
|
|
6499
|
+
returned: scoped.length,
|
|
6500
|
+
filteredOutOfScope: expanded.length - scoped.length,
|
|
6287
6501
|
truncated
|
|
6288
6502
|
});
|
|
6289
6503
|
return {
|
|
6290
6504
|
mode: "expanded",
|
|
6291
|
-
results:
|
|
6505
|
+
results: scoped,
|
|
6292
6506
|
truncated
|
|
6293
6507
|
};
|
|
6294
6508
|
}
|
|
@@ -6300,8 +6514,9 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
6300
6514
|
const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
|
|
6301
6515
|
const lessonLimit = Math.min(limit, 10);
|
|
6302
6516
|
const includeLessons = data.includeLessons !== false;
|
|
6303
|
-
const
|
|
6304
|
-
const
|
|
6517
|
+
const overFetchLimit = filterAgentId ? Math.min(limit * 3, 300) : limit;
|
|
6518
|
+
const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
|
|
6519
|
+
const compact = (filterAgentId ? hybridResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : hybridResults.slice(0, limit)).map((r) => ({
|
|
6305
6520
|
obsId: r.observation.id,
|
|
6306
6521
|
sessionId: r.sessionId,
|
|
6307
6522
|
title: r.observation.title,
|
|
@@ -6480,6 +6695,8 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
6480
6695
|
timestamp: mem.forgetAfter
|
|
6481
6696
|
});
|
|
6482
6697
|
await deleteAccessLog(kv, mem.id);
|
|
6698
|
+
getSearchIndex().remove(mem.id);
|
|
6699
|
+
vectorIndexRemove(mem.id);
|
|
6483
6700
|
}
|
|
6484
6701
|
}
|
|
6485
6702
|
}
|
|
@@ -6557,10 +6774,13 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
6557
6774
|
sessionId: sessions[i].id,
|
|
6558
6775
|
timestamp: obs.timestamp
|
|
6559
6776
|
});
|
|
6777
|
+
getSearchIndex().remove(obs.id);
|
|
6778
|
+
vectorIndexRemove(obs.id);
|
|
6560
6779
|
}
|
|
6561
6780
|
}
|
|
6562
6781
|
}
|
|
6563
6782
|
}
|
|
6783
|
+
if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
|
|
6564
6784
|
logger.info("Auto-forget complete", {
|
|
6565
6785
|
ttlExpired: result.ttlExpired.length,
|
|
6566
6786
|
contradictions: result.contradictions.length,
|
|
@@ -6573,7 +6793,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
6573
6793
|
|
|
6574
6794
|
//#endregion
|
|
6575
6795
|
//#region src/version.ts
|
|
6576
|
-
const VERSION = "0.9.
|
|
6796
|
+
const VERSION = "0.9.23";
|
|
6577
6797
|
|
|
6578
6798
|
//#endregion
|
|
6579
6799
|
//#region src/functions/export-import.ts
|
|
@@ -6712,7 +6932,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
6712
6932
|
"0.9.18",
|
|
6713
6933
|
"0.9.19",
|
|
6714
6934
|
"0.9.20",
|
|
6715
|
-
"0.9.21"
|
|
6935
|
+
"0.9.21",
|
|
6936
|
+
"0.9.22",
|
|
6937
|
+
"0.9.23"
|
|
6716
6938
|
]).has(importData.version)) return {
|
|
6717
6939
|
success: false,
|
|
6718
6940
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -6835,6 +7057,7 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
6835
7057
|
continue;
|
|
6836
7058
|
}
|
|
6837
7059
|
}
|
|
7060
|
+
if (!Array.isArray(memory.sessionIds)) memory.sessionIds = [];
|
|
6838
7061
|
await kv.set(KV.memories, memory.id, memory);
|
|
6839
7062
|
stats.memories++;
|
|
6840
7063
|
}
|
|
@@ -7038,6 +7261,7 @@ function escapeXml(s) {
|
|
|
7038
7261
|
}
|
|
7039
7262
|
function registerEnrichFunction(sdk, kv) {
|
|
7040
7263
|
sdk.registerFunction("mem::enrich", async (data) => {
|
|
7264
|
+
const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
|
|
7041
7265
|
const parts = [];
|
|
7042
7266
|
const fileContextPromise = sdk.trigger({
|
|
7043
7267
|
function_id: "mem::file-context",
|
|
@@ -7051,10 +7275,11 @@ function registerEnrichFunction(sdk, kv) {
|
|
|
7051
7275
|
function_id: "mem::search",
|
|
7052
7276
|
payload: {
|
|
7053
7277
|
query: searchQueries.join(" "),
|
|
7054
|
-
limit: 5
|
|
7278
|
+
limit: 5,
|
|
7279
|
+
...project !== void 0 && { project }
|
|
7055
7280
|
}
|
|
7056
7281
|
}).catch(() => ({ results: [] })) : Promise.resolve({ results: [] });
|
|
7057
|
-
const bugMemoriesPromise = kv.list(KV.memories).then((memories) => memories.filter((m) => m.type === "bug" && m.isLatest && m.files.some((f) => data.files.some((df) => f.includes(df) || df.includes(f)))).sort((a, b) => new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime())).catch(() => []);
|
|
7282
|
+
const bugMemoriesPromise = kv.list(KV.memories).then((memories) => memories.filter((m) => m.type === "bug" && m.isLatest && (!project || !m.project || m.project === project) && m.files.some((f) => data.files.some((df) => f.includes(df) || df.includes(f)))).sort((a, b) => new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime())).catch(() => []);
|
|
7058
7283
|
const [fileContext, searchResult, bugMemories] = await Promise.all([
|
|
7059
7284
|
fileContextPromise,
|
|
7060
7285
|
searchPromise,
|
|
@@ -7077,6 +7302,7 @@ function registerEnrichFunction(sdk, kv) {
|
|
|
7077
7302
|
}
|
|
7078
7303
|
logger.info("Enrichment completed", {
|
|
7079
7304
|
sessionId: data.sessionId,
|
|
7305
|
+
project,
|
|
7080
7306
|
fileCount: data.files.length,
|
|
7081
7307
|
contextLength: context.length,
|
|
7082
7308
|
truncated
|
|
@@ -7234,16 +7460,24 @@ function buildGraphExtractionPrompt(observations) {
|
|
|
7234
7460
|
|
|
7235
7461
|
//#endregion
|
|
7236
7462
|
//#region src/functions/graph.ts
|
|
7463
|
+
function parseAttrs(raw) {
|
|
7464
|
+
const attrs = {};
|
|
7465
|
+
const attrRegex = /([A-Za-z_][\w:-]*)="([^"]*)"/g;
|
|
7466
|
+
let m;
|
|
7467
|
+
while ((m = attrRegex.exec(raw)) !== null) attrs[m[1]] = m[2];
|
|
7468
|
+
return attrs;
|
|
7469
|
+
}
|
|
7237
7470
|
function parseGraphXml(xml, observationIds) {
|
|
7238
7471
|
const nodes = [];
|
|
7239
7472
|
const edges = [];
|
|
7240
7473
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7241
|
-
const
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
const
|
|
7245
|
-
const
|
|
7246
|
-
const
|
|
7474
|
+
const entitySelfClose = /<entity\b([^>]*?)\/>/g;
|
|
7475
|
+
const entityWithBody = /<entity\b([^>]*[^/])>([\s\S]*?)<\/entity>/g;
|
|
7476
|
+
const addEntity = (rawAttrs, propsBlock = "") => {
|
|
7477
|
+
const attrs = parseAttrs(rawAttrs);
|
|
7478
|
+
const type = attrs["type"];
|
|
7479
|
+
const name = attrs["name"];
|
|
7480
|
+
if (!type || !name) return;
|
|
7247
7481
|
const properties = {};
|
|
7248
7482
|
const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
|
|
7249
7483
|
let propMatch;
|
|
@@ -7256,17 +7490,23 @@ function parseGraphXml(xml, observationIds) {
|
|
|
7256
7490
|
sourceObservationIds: observationIds,
|
|
7257
7491
|
createdAt: now
|
|
7258
7492
|
});
|
|
7259
|
-
}
|
|
7260
|
-
|
|
7493
|
+
};
|
|
7494
|
+
let match;
|
|
7495
|
+
while ((match = entitySelfClose.exec(xml)) !== null) addEntity(match[1]);
|
|
7496
|
+
while ((match = entityWithBody.exec(xml)) !== null) addEntity(match[1], match[2]);
|
|
7497
|
+
const relRegex = /<relationship\b([^>]*?)\/>/g;
|
|
7261
7498
|
while ((match = relRegex.exec(xml)) !== null) {
|
|
7262
|
-
const
|
|
7263
|
-
const
|
|
7264
|
-
const
|
|
7265
|
-
const
|
|
7266
|
-
|
|
7499
|
+
const attrs = parseAttrs(match[1]);
|
|
7500
|
+
const type = attrs["type"];
|
|
7501
|
+
const sourceName = attrs["source"];
|
|
7502
|
+
const targetName = attrs["target"];
|
|
7503
|
+
if (!type || !sourceName || !targetName) continue;
|
|
7504
|
+
const parsedWeight = parseFloat(attrs["weight"] ?? "");
|
|
7505
|
+
const weight = Number.isFinite(parsedWeight) ? parsedWeight : .5;
|
|
7267
7506
|
const sourceNode = nodes.find((n) => n.name === sourceName);
|
|
7268
7507
|
const targetNode = nodes.find((n) => n.name === targetName);
|
|
7269
|
-
if (sourceNode
|
|
7508
|
+
if (!sourceNode || !targetNode) continue;
|
|
7509
|
+
edges.push({
|
|
7270
7510
|
id: generateId("ge"),
|
|
7271
7511
|
type,
|
|
7272
7512
|
sourceNodeId: sourceNode.id,
|
|
@@ -7480,7 +7720,7 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
|
|
|
7480
7720
|
if (!data?.force && !isConsolidationEnabled()) return {
|
|
7481
7721
|
success: false,
|
|
7482
7722
|
skipped: true,
|
|
7483
|
-
reason: "CONSOLIDATION_ENABLED
|
|
7723
|
+
reason: "Consolidation disabled: set CONSOLIDATION_ENABLED=true or configure an LLM provider (ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GEMINI_API_KEY / GOOGLE_API_KEY / MINIMAX_API_KEY / OPENAI_BASE_URL / AGENTMEMORY_PROVIDER=agent-sdk)"
|
|
7484
7724
|
};
|
|
7485
7725
|
const tier = data?.tier || "all";
|
|
7486
7726
|
const decayDays = getConsolidationDecayDays();
|
|
@@ -7771,8 +8011,11 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7771
8011
|
for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
|
|
7772
8012
|
await kv.delete(KV.memories, id);
|
|
7773
8013
|
await deleteAccessLog(kv, id);
|
|
8014
|
+
getSearchIndex().remove(id);
|
|
8015
|
+
vectorIndexRemove(id);
|
|
7774
8016
|
deleted++;
|
|
7775
8017
|
}
|
|
8018
|
+
if (deleted > 0) await flushIndexSave();
|
|
7776
8019
|
await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
|
|
7777
8020
|
reason: data.reason || "manual deletion",
|
|
7778
8021
|
deleted
|
|
@@ -7825,6 +8068,8 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7825
8068
|
(await Promise.allSettled(batch.map(async (mem) => {
|
|
7826
8069
|
await kv.delete(KV.memories, mem.id);
|
|
7827
8070
|
await deleteAccessLog(kv, mem.id);
|
|
8071
|
+
getSearchIndex().remove(mem.id);
|
|
8072
|
+
vectorIndexRemove(mem.id);
|
|
7828
8073
|
}))).forEach((result, j) => {
|
|
7829
8074
|
const mem = batch[j];
|
|
7830
8075
|
if (result.status === "fulfilled") successfulIds.push(mem.id);
|
|
@@ -7840,6 +8085,7 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7840
8085
|
}
|
|
7841
8086
|
});
|
|
7842
8087
|
}
|
|
8088
|
+
if (successfulIds.length > 0) await flushIndexSave();
|
|
7843
8089
|
await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
|
|
7844
8090
|
filter: data,
|
|
7845
8091
|
deleted: successfulIds.length,
|
|
@@ -10644,11 +10890,34 @@ function registerDiagnosticsFunction(sdk, kv) {
|
|
|
10644
10890
|
});
|
|
10645
10891
|
memoryIssues++;
|
|
10646
10892
|
}
|
|
10893
|
+
const latestMemories = memories.filter((m) => m.isLatest);
|
|
10894
|
+
const unscopedCount = latestMemories.filter((m) => !m.project).length;
|
|
10895
|
+
if (unscopedCount === 0) checks.push({
|
|
10896
|
+
name: "memory-project-coverage",
|
|
10897
|
+
category: "memories",
|
|
10898
|
+
status: "pass",
|
|
10899
|
+
message: `All ${latestMemories.length} latest memories have a project scope`,
|
|
10900
|
+
fixable: false
|
|
10901
|
+
});
|
|
10902
|
+
else if (unscopedCount <= 10) checks.push({
|
|
10903
|
+
name: "memory-project-coverage",
|
|
10904
|
+
category: "memories",
|
|
10905
|
+
status: "warn",
|
|
10906
|
+
message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
|
|
10907
|
+
fixable: true
|
|
10908
|
+
});
|
|
10909
|
+
else checks.push({
|
|
10910
|
+
name: "memory-project-coverage",
|
|
10911
|
+
category: "memories",
|
|
10912
|
+
status: "fail",
|
|
10913
|
+
message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
|
|
10914
|
+
fixable: true
|
|
10915
|
+
});
|
|
10647
10916
|
if (memoryIssues === 0) checks.push({
|
|
10648
10917
|
name: "memories-ok",
|
|
10649
10918
|
category: "memories",
|
|
10650
10919
|
status: "pass",
|
|
10651
|
-
message: `All ${memories.length} memories are consistent`,
|
|
10920
|
+
message: `All ${memories.length} memories are structurally consistent`,
|
|
10652
10921
|
fixable: false
|
|
10653
10922
|
});
|
|
10654
10923
|
}
|
|
@@ -13239,6 +13508,8 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
13239
13508
|
await kv.delete(scope, candidate.memoryId);
|
|
13240
13509
|
await kv.delete(KV.retentionScores, candidate.memoryId);
|
|
13241
13510
|
await deleteAccessLog(kv, candidate.memoryId);
|
|
13511
|
+
getSearchIndex().remove(candidate.memoryId);
|
|
13512
|
+
vectorIndexRemove(candidate.memoryId);
|
|
13242
13513
|
evicted++;
|
|
13243
13514
|
evictedIds.push(candidate.memoryId);
|
|
13244
13515
|
if (resolvedSource === "semantic") evictedSemantic++;
|
|
@@ -13246,13 +13517,16 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
13246
13517
|
} catch {
|
|
13247
13518
|
continue;
|
|
13248
13519
|
}
|
|
13249
|
-
if (evicted > 0)
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13520
|
+
if (evicted > 0) {
|
|
13521
|
+
await flushIndexSave();
|
|
13522
|
+
await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
|
|
13523
|
+
threshold,
|
|
13524
|
+
evicted,
|
|
13525
|
+
evictedEpisodic,
|
|
13526
|
+
evictedSemantic,
|
|
13527
|
+
reason: "retention score below threshold"
|
|
13528
|
+
});
|
|
13529
|
+
}
|
|
13256
13530
|
logger.info("Retention-based eviction complete", {
|
|
13257
13531
|
evicted,
|
|
13258
13532
|
evictedEpisodic,
|
|
@@ -14157,107 +14431,340 @@ function renderViewerDocument() {
|
|
|
14157
14431
|
}
|
|
14158
14432
|
|
|
14159
14433
|
//#endregion
|
|
14160
|
-
//#region src/
|
|
14161
|
-
function
|
|
14162
|
-
|
|
14163
|
-
const
|
|
14164
|
-
|
|
14165
|
-
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
const
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
body: { error: "unauthorized" }
|
|
14172
|
-
};
|
|
14434
|
+
//#region src/viewer/server.ts
|
|
14435
|
+
function loadViewerFavicon() {
|
|
14436
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
14437
|
+
const candidates = [
|
|
14438
|
+
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
14439
|
+
join(base, "..", "viewer", "favicon.svg"),
|
|
14440
|
+
join(base, "viewer", "favicon.svg")
|
|
14441
|
+
];
|
|
14442
|
+
for (const path of candidates) try {
|
|
14443
|
+
return readFileSync(path);
|
|
14444
|
+
} catch {}
|
|
14173
14445
|
return null;
|
|
14174
14446
|
}
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14447
|
+
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());
|
|
14448
|
+
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
14449
|
+
function buildAllowedHosts(origins, listenPort) {
|
|
14450
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
14451
|
+
for (const o of origins) try {
|
|
14452
|
+
const parsed = new URL(o);
|
|
14453
|
+
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
14454
|
+
} catch {}
|
|
14455
|
+
hosts.add(`localhost:${listenPort}`);
|
|
14456
|
+
hosts.add(`127.0.0.1:${listenPort}`);
|
|
14457
|
+
hosts.add(`[::1]:${listenPort}`);
|
|
14458
|
+
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
14459
|
+
return hosts;
|
|
14181
14460
|
}
|
|
14182
|
-
function
|
|
14461
|
+
function isHostAllowed(headerHost, allowed) {
|
|
14462
|
+
if (typeof headerHost !== "string") return false;
|
|
14463
|
+
const lower = headerHost.toLowerCase().trim();
|
|
14464
|
+
if (!lower) return false;
|
|
14465
|
+
return allowed.has(lower);
|
|
14466
|
+
}
|
|
14467
|
+
function corsHeaders(req) {
|
|
14468
|
+
const origin = req.headers.origin || "";
|
|
14469
|
+
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
14183
14470
|
return {
|
|
14184
|
-
|
|
14185
|
-
|
|
14471
|
+
"Access-Control-Allow-Origin": allowed,
|
|
14472
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
14473
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
14474
|
+
Vary: "Origin"
|
|
14186
14475
|
};
|
|
14187
14476
|
}
|
|
14188
|
-
function
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
14477
|
+
function json(res, status, data, req) {
|
|
14478
|
+
const body = JSON.stringify(data);
|
|
14479
|
+
const cors = req ? corsHeaders(req) : {
|
|
14480
|
+
"Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
|
|
14481
|
+
Vary: "Origin"
|
|
14482
|
+
};
|
|
14483
|
+
res.writeHead(status, {
|
|
14484
|
+
...cors,
|
|
14485
|
+
"Content-Type": "application/json"
|
|
14194
14486
|
});
|
|
14487
|
+
res.end(body);
|
|
14195
14488
|
}
|
|
14196
|
-
function
|
|
14197
|
-
return
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14489
|
+
function readBody(req) {
|
|
14490
|
+
return new Promise((resolve, reject) => {
|
|
14491
|
+
let data = "";
|
|
14492
|
+
let size = 0;
|
|
14493
|
+
req.on("data", (chunk) => {
|
|
14494
|
+
size += chunk.length;
|
|
14495
|
+
if (size > 1e6) {
|
|
14496
|
+
req.destroy();
|
|
14497
|
+
reject(/* @__PURE__ */ new Error("too large"));
|
|
14498
|
+
return;
|
|
14499
|
+
}
|
|
14500
|
+
data += chunk.toString();
|
|
14501
|
+
});
|
|
14502
|
+
req.on("end", () => resolve(data));
|
|
14503
|
+
req.on("error", reject);
|
|
14202
14504
|
});
|
|
14203
14505
|
}
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
function parseOptionalFiniteNumber(value) {
|
|
14210
|
-
if (value === void 0 || value === null) return void 0;
|
|
14211
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
14212
|
-
if (typeof value === "string") {
|
|
14213
|
-
const trimmed = value.trim();
|
|
14214
|
-
if (!trimmed) return void 0;
|
|
14215
|
-
const parsed = Number(trimmed);
|
|
14216
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
14217
|
-
}
|
|
14218
|
-
return null;
|
|
14506
|
+
const MAX_VIEWER_PORT_RETRIES = 10;
|
|
14507
|
+
let boundViewerPort = null;
|
|
14508
|
+
let viewerSkipped = false;
|
|
14509
|
+
function getBoundViewerPort() {
|
|
14510
|
+
return boundViewerPort;
|
|
14219
14511
|
}
|
|
14220
|
-
function
|
|
14221
|
-
|
|
14222
|
-
if (parsed === void 0 || parsed === null) return parsed;
|
|
14223
|
-
if (!Number.isInteger(parsed) || parsed < 1) return null;
|
|
14224
|
-
return parsed;
|
|
14512
|
+
function getViewerSkipped() {
|
|
14513
|
+
return viewerSkipped;
|
|
14225
14514
|
}
|
|
14226
|
-
function
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
}
|
|
14237
|
-
};
|
|
14238
|
-
return { action: "continue" };
|
|
14239
|
-
});
|
|
14240
|
-
sdk.registerFunction("api::liveness", async () => ({
|
|
14241
|
-
status_code: 200,
|
|
14242
|
-
body: {
|
|
14243
|
-
status: "ok",
|
|
14244
|
-
service: "agentmemory"
|
|
14515
|
+
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
14516
|
+
boundViewerPort = null;
|
|
14517
|
+
viewerSkipped = false;
|
|
14518
|
+
const resolvedRestPort = restPort ?? port - 2;
|
|
14519
|
+
const requestedPort = port;
|
|
14520
|
+
let allowedHosts = null;
|
|
14521
|
+
const server = createServer(async (req, res) => {
|
|
14522
|
+
if (!allowedHosts) {
|
|
14523
|
+
const addr = server.address();
|
|
14524
|
+
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
14245
14525
|
}
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
config: {
|
|
14251
|
-
api_path: "/agentmemory/livez",
|
|
14252
|
-
http_method: "GET"
|
|
14526
|
+
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
14527
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
14528
|
+
res.end("forbidden host");
|
|
14529
|
+
return;
|
|
14253
14530
|
}
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
const
|
|
14257
|
-
|
|
14258
|
-
|
|
14259
|
-
|
|
14260
|
-
|
|
14531
|
+
const raw = req.url || "/";
|
|
14532
|
+
const qIdx = raw.indexOf("?");
|
|
14533
|
+
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
14534
|
+
const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
|
|
14535
|
+
const method = req.method || "GET";
|
|
14536
|
+
if (method === "OPTIONS") {
|
|
14537
|
+
res.writeHead(204, {
|
|
14538
|
+
...corsHeaders(req),
|
|
14539
|
+
"Access-Control-Max-Age": "86400"
|
|
14540
|
+
});
|
|
14541
|
+
res.end();
|
|
14542
|
+
return;
|
|
14543
|
+
}
|
|
14544
|
+
if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
|
|
14545
|
+
const rendered = renderViewerDocument();
|
|
14546
|
+
if (rendered.found) {
|
|
14547
|
+
res.writeHead(200, {
|
|
14548
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
14549
|
+
"Content-Security-Policy": rendered.csp,
|
|
14550
|
+
"Cache-Control": "no-cache"
|
|
14551
|
+
});
|
|
14552
|
+
res.end(rendered.html);
|
|
14553
|
+
return;
|
|
14554
|
+
}
|
|
14555
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
14556
|
+
res.end("viewer not found");
|
|
14557
|
+
return;
|
|
14558
|
+
}
|
|
14559
|
+
if (method === "GET" && pathname === "/favicon.svg") {
|
|
14560
|
+
const favicon = loadViewerFavicon();
|
|
14561
|
+
if (favicon) {
|
|
14562
|
+
res.writeHead(200, {
|
|
14563
|
+
"Content-Type": "image/svg+xml",
|
|
14564
|
+
"Cache-Control": "public, max-age=3600"
|
|
14565
|
+
});
|
|
14566
|
+
res.end(favicon);
|
|
14567
|
+
return;
|
|
14568
|
+
}
|
|
14569
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
14570
|
+
res.end("favicon not found");
|
|
14571
|
+
return;
|
|
14572
|
+
}
|
|
14573
|
+
try {
|
|
14574
|
+
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
14575
|
+
} catch (err) {
|
|
14576
|
+
console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
|
|
14577
|
+
json(res, 502, { error: "upstream error" }, req);
|
|
14578
|
+
}
|
|
14579
|
+
});
|
|
14580
|
+
let attempt = 0;
|
|
14581
|
+
let currentPort = requestedPort;
|
|
14582
|
+
const tryListen = () => {
|
|
14583
|
+
server.listen(currentPort, "127.0.0.1");
|
|
14584
|
+
};
|
|
14585
|
+
server.on("listening", () => {
|
|
14586
|
+
const addr = server.address();
|
|
14587
|
+
boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
|
|
14588
|
+
viewerSkipped = false;
|
|
14589
|
+
if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
|
|
14590
|
+
else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
|
|
14591
|
+
});
|
|
14592
|
+
server.on("error", (err) => {
|
|
14593
|
+
if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
|
|
14594
|
+
attempt++;
|
|
14595
|
+
currentPort = requestedPort + attempt;
|
|
14596
|
+
setImmediate(tryListen);
|
|
14597
|
+
return;
|
|
14598
|
+
}
|
|
14599
|
+
if (err.code === "EADDRINUSE") {
|
|
14600
|
+
boundViewerPort = null;
|
|
14601
|
+
viewerSkipped = true;
|
|
14602
|
+
console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
|
|
14603
|
+
} else {
|
|
14604
|
+
boundViewerPort = null;
|
|
14605
|
+
viewerSkipped = true;
|
|
14606
|
+
console.error(`[agentmemory] Viewer error:`, err.message);
|
|
14607
|
+
}
|
|
14608
|
+
});
|
|
14609
|
+
tryListen();
|
|
14610
|
+
return server;
|
|
14611
|
+
}
|
|
14612
|
+
async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
|
|
14613
|
+
const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
|
|
14614
|
+
const headers = {};
|
|
14615
|
+
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
|
14616
|
+
const ct = req.headers["content-type"];
|
|
14617
|
+
if (ct) headers["Content-Type"] = ct;
|
|
14618
|
+
let body;
|
|
14619
|
+
if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
|
|
14620
|
+
const controller = new AbortController();
|
|
14621
|
+
const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
|
|
14622
|
+
let upstream;
|
|
14623
|
+
try {
|
|
14624
|
+
upstream = await fetch(upstreamUrl, {
|
|
14625
|
+
method,
|
|
14626
|
+
headers,
|
|
14627
|
+
body: body || void 0,
|
|
14628
|
+
signal: controller.signal
|
|
14629
|
+
});
|
|
14630
|
+
clearTimeout(fetchTimeout);
|
|
14631
|
+
} catch (err) {
|
|
14632
|
+
clearTimeout(fetchTimeout);
|
|
14633
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
14634
|
+
json(res, 504, { error: "upstream timeout" }, req);
|
|
14635
|
+
return;
|
|
14636
|
+
}
|
|
14637
|
+
throw err;
|
|
14638
|
+
}
|
|
14639
|
+
const cors = corsHeaders(req);
|
|
14640
|
+
const responseBody = await upstream.text();
|
|
14641
|
+
const responseHeaders = { ...cors };
|
|
14642
|
+
const upstreamCt = upstream.headers.get("content-type");
|
|
14643
|
+
if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
|
|
14644
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
14645
|
+
res.end(responseBody);
|
|
14646
|
+
}
|
|
14647
|
+
|
|
14648
|
+
//#endregion
|
|
14649
|
+
//#region src/triggers/api.ts
|
|
14650
|
+
function parseOptionalInt(raw) {
|
|
14651
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
14652
|
+
const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
|
|
14653
|
+
return Number.isFinite(n) ? n : void 0;
|
|
14654
|
+
}
|
|
14655
|
+
function checkAuth(req, secret) {
|
|
14656
|
+
if (!secret) return null;
|
|
14657
|
+
const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
|
|
14658
|
+
if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
|
|
14659
|
+
status_code: 401,
|
|
14660
|
+
body: { error: "unauthorized" }
|
|
14661
|
+
};
|
|
14662
|
+
return null;
|
|
14663
|
+
}
|
|
14664
|
+
function requireConfiguredSecret(secret, feature) {
|
|
14665
|
+
if (secret) return null;
|
|
14666
|
+
return {
|
|
14667
|
+
status_code: 503,
|
|
14668
|
+
body: { error: `${feature} requires AGENTMEMORY_SECRET` }
|
|
14669
|
+
};
|
|
14670
|
+
}
|
|
14671
|
+
function flagDisabledResponse(opts) {
|
|
14672
|
+
return {
|
|
14673
|
+
status_code: 503,
|
|
14674
|
+
body: opts
|
|
14675
|
+
};
|
|
14676
|
+
}
|
|
14677
|
+
function graphDisabledResponse() {
|
|
14678
|
+
return flagDisabledResponse({
|
|
14679
|
+
error: "Knowledge graph not enabled",
|
|
14680
|
+
flag: "GRAPH_EXTRACTION_ENABLED",
|
|
14681
|
+
enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
|
|
14682
|
+
docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
|
|
14683
|
+
});
|
|
14684
|
+
}
|
|
14685
|
+
function consolidationDisabledResponse() {
|
|
14686
|
+
return flagDisabledResponse({
|
|
14687
|
+
error: "Consolidation pipeline not enabled",
|
|
14688
|
+
flag: "CONSOLIDATION_ENABLED",
|
|
14689
|
+
enableHow: "Set CONSOLIDATION_ENABLED=true and restart. Requires an LLM provider key.",
|
|
14690
|
+
docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
|
|
14691
|
+
});
|
|
14692
|
+
}
|
|
14693
|
+
function slotsDisabledResponse() {
|
|
14694
|
+
return flagDisabledResponse({
|
|
14695
|
+
error: "Memory slots not enabled",
|
|
14696
|
+
flag: "AGENTMEMORY_SLOTS",
|
|
14697
|
+
enableHow: "Set AGENTMEMORY_SLOTS=true (in ~/.agentmemory/.env or the shell) and restart.",
|
|
14698
|
+
docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
|
|
14699
|
+
});
|
|
14700
|
+
}
|
|
14701
|
+
function reflectDisabledResponse() {
|
|
14702
|
+
return flagDisabledResponse({
|
|
14703
|
+
error: "Slot reflection not enabled",
|
|
14704
|
+
flag: "AGENTMEMORY_REFLECT",
|
|
14705
|
+
enableHow: "Set AGENTMEMORY_REFLECT=true (in ~/.agentmemory/.env or the shell) and restart. Requires AGENTMEMORY_SLOTS=true.",
|
|
14706
|
+
docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
|
|
14707
|
+
});
|
|
14708
|
+
}
|
|
14709
|
+
function asNonEmptyString$1(value) {
|
|
14710
|
+
if (typeof value !== "string") return null;
|
|
14711
|
+
const trimmed = value.trim();
|
|
14712
|
+
return trimmed ? trimmed : null;
|
|
14713
|
+
}
|
|
14714
|
+
function parseOptionalFiniteNumber(value) {
|
|
14715
|
+
if (value === void 0 || value === null) return void 0;
|
|
14716
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
14717
|
+
if (typeof value === "string") {
|
|
14718
|
+
const trimmed = value.trim();
|
|
14719
|
+
if (!trimmed) return void 0;
|
|
14720
|
+
const parsed = Number(trimmed);
|
|
14721
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
14722
|
+
}
|
|
14723
|
+
return null;
|
|
14724
|
+
}
|
|
14725
|
+
function parseOptionalPositiveInt(value) {
|
|
14726
|
+
const parsed = parseOptionalFiniteNumber(value);
|
|
14727
|
+
if (parsed === void 0 || parsed === null) return parsed;
|
|
14728
|
+
if (!Number.isInteger(parsed) || parsed < 1) return null;
|
|
14729
|
+
return parsed;
|
|
14730
|
+
}
|
|
14731
|
+
function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
14732
|
+
sdk.registerFunction("middleware::api-auth", async (input) => {
|
|
14733
|
+
if (!secret) return { action: "continue" };
|
|
14734
|
+
const headers = input?.request?.headers || {};
|
|
14735
|
+
const auth = headers["authorization"] || headers["Authorization"];
|
|
14736
|
+
if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
|
|
14737
|
+
action: "respond",
|
|
14738
|
+
response: {
|
|
14739
|
+
status_code: 401,
|
|
14740
|
+
body: { error: "unauthorized" }
|
|
14741
|
+
}
|
|
14742
|
+
};
|
|
14743
|
+
return { action: "continue" };
|
|
14744
|
+
});
|
|
14745
|
+
sdk.registerFunction("api::liveness", async () => ({
|
|
14746
|
+
status_code: 200,
|
|
14747
|
+
body: {
|
|
14748
|
+
status: "ok",
|
|
14749
|
+
service: "agentmemory",
|
|
14750
|
+
viewerPort: getBoundViewerPort(),
|
|
14751
|
+
viewerSkipped: getViewerSkipped()
|
|
14752
|
+
}
|
|
14753
|
+
}));
|
|
14754
|
+
sdk.registerTrigger({
|
|
14755
|
+
type: "http",
|
|
14756
|
+
function_id: "api::liveness",
|
|
14757
|
+
config: {
|
|
14758
|
+
api_path: "/agentmemory/livez",
|
|
14759
|
+
http_method: "GET"
|
|
14760
|
+
}
|
|
14761
|
+
});
|
|
14762
|
+
sdk.registerFunction("api::config-flags", async (req) => {
|
|
14763
|
+
const authErr = checkAuth(req, secret);
|
|
14764
|
+
if (authErr) return authErr;
|
|
14765
|
+
return {
|
|
14766
|
+
status_code: 200,
|
|
14767
|
+
body: {
|
|
14261
14768
|
version: VERSION,
|
|
14262
14769
|
provider: detectLlmProviderKind(),
|
|
14263
14770
|
embeddingProvider: detectEmbeddingProvider() ? "embeddings" : "none",
|
|
@@ -14336,7 +14843,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14336
14843
|
version: VERSION,
|
|
14337
14844
|
health: health || null,
|
|
14338
14845
|
functionMetrics,
|
|
14339
|
-
circuitBreaker
|
|
14846
|
+
circuitBreaker,
|
|
14847
|
+
viewerPort: getBoundViewerPort(),
|
|
14848
|
+
viewerSkipped: getViewerSkipped()
|
|
14340
14849
|
}
|
|
14341
14850
|
};
|
|
14342
14851
|
});
|
|
@@ -14590,6 +15099,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14590
15099
|
body: { error: "sessionId, project, and cwd are required non-empty strings" }
|
|
14591
15100
|
};
|
|
14592
15101
|
const title = typeof body.title === "string" ? body.title.trim() : void 0;
|
|
15102
|
+
const agentId = (typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim().slice(0, 128) : void 0) ?? getAgentId();
|
|
14593
15103
|
const session = {
|
|
14594
15104
|
id: sessionId,
|
|
14595
15105
|
project,
|
|
@@ -14598,7 +15108,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14598
15108
|
status: "active",
|
|
14599
15109
|
observationCount: 0,
|
|
14600
15110
|
...title ? { summary: title.slice(0, 200) } : {},
|
|
14601
|
-
...title ? { firstPrompt: title.slice(0, 200) } : {}
|
|
15111
|
+
...title ? { firstPrompt: title.slice(0, 200) } : {},
|
|
15112
|
+
...agentId ? { agentId } : {}
|
|
14602
15113
|
};
|
|
14603
15114
|
await kv.set(KV.sessions, sessionId, session);
|
|
14604
15115
|
return {
|
|
@@ -14639,6 +15150,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14639
15150
|
path: "status",
|
|
14640
15151
|
value: "completed"
|
|
14641
15152
|
}]);
|
|
15153
|
+
try {
|
|
15154
|
+
sdk.triggerVoid("event::session::stopped", { sessionId });
|
|
15155
|
+
} catch (err) {
|
|
15156
|
+
logger.warn("event::session::stopped triggerVoid failed", {
|
|
15157
|
+
sessionId,
|
|
15158
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15159
|
+
});
|
|
15160
|
+
}
|
|
14642
15161
|
return {
|
|
14643
15162
|
status_code: 200,
|
|
14644
15163
|
body: { success: true }
|
|
@@ -14785,9 +15304,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14785
15304
|
sdk.registerFunction("api::sessions", async (req) => {
|
|
14786
15305
|
const authErr = checkAuth(req, secret);
|
|
14787
15306
|
if (authErr) return authErr;
|
|
15307
|
+
const sessions = await kv.list(KV.sessions);
|
|
15308
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
15309
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
15310
|
+
const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
14788
15311
|
return {
|
|
14789
15312
|
status_code: 200,
|
|
14790
|
-
body: { sessions:
|
|
15313
|
+
body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
|
|
14791
15314
|
};
|
|
14792
15315
|
});
|
|
14793
15316
|
sdk.registerTrigger({
|
|
@@ -14806,9 +15329,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14806
15329
|
status_code: 400,
|
|
14807
15330
|
body: { error: "sessionId required" }
|
|
14808
15331
|
};
|
|
15332
|
+
const observations = await kv.list(KV.observations(sessionId));
|
|
15333
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
15334
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
15335
|
+
const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
14809
15336
|
return {
|
|
14810
15337
|
status_code: 200,
|
|
14811
|
-
body: { observations:
|
|
15338
|
+
body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
|
|
14812
15339
|
};
|
|
14813
15340
|
});
|
|
14814
15341
|
sdk.registerTrigger({
|
|
@@ -14849,11 +15376,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14849
15376
|
status_code: 400,
|
|
14850
15377
|
body: { error: "terms must be an array of strings" }
|
|
14851
15378
|
};
|
|
15379
|
+
if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
|
|
15380
|
+
status_code: 400,
|
|
15381
|
+
body: { error: "project must be a non-empty string" }
|
|
15382
|
+
};
|
|
14852
15383
|
return {
|
|
14853
15384
|
status_code: 200,
|
|
14854
15385
|
body: await sdk.trigger({
|
|
14855
15386
|
function_id: "mem::enrich",
|
|
14856
|
-
payload:
|
|
15387
|
+
payload: {
|
|
15388
|
+
sessionId: req.body.sessionId,
|
|
15389
|
+
files: req.body.files,
|
|
15390
|
+
...req.body.terms !== void 0 && { terms: req.body.terms },
|
|
15391
|
+
...req.body.toolName !== void 0 && { toolName: req.body.toolName },
|
|
15392
|
+
...req.body.project !== void 0 && { project: req.body.project }
|
|
15393
|
+
}
|
|
14857
15394
|
})
|
|
14858
15395
|
};
|
|
14859
15396
|
});
|
|
@@ -14872,11 +15409,23 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14872
15409
|
status_code: 400,
|
|
14873
15410
|
body: { error: "content is required" }
|
|
14874
15411
|
};
|
|
15412
|
+
if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
|
|
15413
|
+
status_code: 400,
|
|
15414
|
+
body: { error: "project must be a non-empty string" }
|
|
15415
|
+
};
|
|
14875
15416
|
return {
|
|
14876
15417
|
status_code: 201,
|
|
14877
15418
|
body: await sdk.trigger({
|
|
14878
15419
|
function_id: "mem::remember",
|
|
14879
|
-
payload:
|
|
15420
|
+
payload: {
|
|
15421
|
+
content: req.body.content,
|
|
15422
|
+
...req.body.type !== void 0 && { type: req.body.type },
|
|
15423
|
+
...req.body.concepts !== void 0 && { concepts: req.body.concepts },
|
|
15424
|
+
...req.body.files !== void 0 && { files: req.body.files },
|
|
15425
|
+
...req.body.ttlDays !== void 0 && { ttlDays: req.body.ttlDays },
|
|
15426
|
+
...req.body.sourceObservationIds !== void 0 && { sourceObservationIds: req.body.sourceObservationIds },
|
|
15427
|
+
...req.body.project !== void 0 && { project: req.body.project }
|
|
15428
|
+
}
|
|
14880
15429
|
})
|
|
14881
15430
|
};
|
|
14882
15431
|
});
|
|
@@ -14971,15 +15520,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14971
15520
|
sdk.registerFunction("api::migrate", async (req) => {
|
|
14972
15521
|
const authErr = checkAuth(req, secret);
|
|
14973
15522
|
if (authErr) return authErr;
|
|
14974
|
-
|
|
15523
|
+
const hasStep = typeof req.body?.step === "string" && req.body.step.trim().length > 0;
|
|
15524
|
+
const hasDbPath = typeof req.body?.dbPath === "string" && req.body.dbPath.trim().length > 0;
|
|
15525
|
+
if (!hasStep && !hasDbPath) return {
|
|
14975
15526
|
status_code: 400,
|
|
14976
|
-
body: { error: "dbPath is required" }
|
|
15527
|
+
body: { error: "Either step (string) or dbPath (string) is required" }
|
|
14977
15528
|
};
|
|
14978
15529
|
return {
|
|
14979
15530
|
status_code: 200,
|
|
14980
15531
|
body: await sdk.trigger({
|
|
14981
15532
|
function_id: "mem::migrate",
|
|
14982
|
-
payload:
|
|
15533
|
+
payload: {
|
|
15534
|
+
...req.body.step !== void 0 && { step: req.body.step },
|
|
15535
|
+
...req.body.dbPath !== void 0 && { dbPath: req.body.dbPath },
|
|
15536
|
+
...req.body.dryRun !== void 0 && { dryRun: req.body.dryRun }
|
|
15537
|
+
}
|
|
14983
15538
|
})
|
|
14984
15539
|
};
|
|
14985
15540
|
});
|
|
@@ -15084,11 +15639,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15084
15639
|
sdk.registerFunction("api::export", async (req) => {
|
|
15085
15640
|
const authErr = checkAuth(req, secret);
|
|
15086
15641
|
if (authErr) return authErr;
|
|
15642
|
+
const rawMax = req.query_params?.["maxSessions"];
|
|
15643
|
+
const rawOffset = req.query_params?.["offset"];
|
|
15644
|
+
const payload = {};
|
|
15645
|
+
if (typeof rawMax === "string") {
|
|
15646
|
+
const n = Number(rawMax);
|
|
15647
|
+
if (Number.isInteger(n) && n > 0) payload.maxSessions = n;
|
|
15648
|
+
}
|
|
15649
|
+
if (typeof rawOffset === "string") {
|
|
15650
|
+
const n = Number(rawOffset);
|
|
15651
|
+
if (Number.isInteger(n) && n >= 0) payload.offset = n;
|
|
15652
|
+
}
|
|
15087
15653
|
return {
|
|
15088
15654
|
status_code: 200,
|
|
15089
15655
|
body: await sdk.trigger({
|
|
15090
15656
|
function_id: "mem::export",
|
|
15091
|
-
payload
|
|
15657
|
+
payload
|
|
15092
15658
|
})
|
|
15093
15659
|
};
|
|
15094
15660
|
});
|
|
@@ -15314,6 +15880,63 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15314
15880
|
http_method: "POST"
|
|
15315
15881
|
}
|
|
15316
15882
|
});
|
|
15883
|
+
sdk.registerFunction("api::graph-build", async (req) => {
|
|
15884
|
+
const authErr = checkAuth(req, secret);
|
|
15885
|
+
if (authErr) return authErr;
|
|
15886
|
+
const batchSize = Math.max(1, Math.min(100, Number(req.body?.batchSize) || 25));
|
|
15887
|
+
try {
|
|
15888
|
+
const sessions = await kv.list(KV.sessions);
|
|
15889
|
+
let totalNodes = 0;
|
|
15890
|
+
let totalEdges = 0;
|
|
15891
|
+
let batchesRun = 0;
|
|
15892
|
+
for (const session of sessions) {
|
|
15893
|
+
const sid = session?.id;
|
|
15894
|
+
if (typeof sid !== "string" || sid.length === 0) continue;
|
|
15895
|
+
const compressed = (await kv.list(KV.observations(sid))).filter((o) => o && typeof o.title === "string" && o.title.length > 0);
|
|
15896
|
+
if (compressed.length === 0) continue;
|
|
15897
|
+
for (let i = 0; i < compressed.length; i += batchSize) {
|
|
15898
|
+
const batch = compressed.slice(i, i + batchSize);
|
|
15899
|
+
try {
|
|
15900
|
+
const result = await sdk.trigger({
|
|
15901
|
+
function_id: "mem::graph-extract",
|
|
15902
|
+
payload: { observations: batch }
|
|
15903
|
+
});
|
|
15904
|
+
if (result?.success) {
|
|
15905
|
+
totalNodes += Number(result.nodesAdded) || 0;
|
|
15906
|
+
totalEdges += Number(result.edgesAdded) || 0;
|
|
15907
|
+
}
|
|
15908
|
+
batchesRun++;
|
|
15909
|
+
} catch (err) {
|
|
15910
|
+
logger.warn("graph-build batch failed", {
|
|
15911
|
+
sessionId: sid,
|
|
15912
|
+
batchIndex: Math.floor(i / batchSize),
|
|
15913
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15914
|
+
});
|
|
15915
|
+
}
|
|
15916
|
+
}
|
|
15917
|
+
}
|
|
15918
|
+
return {
|
|
15919
|
+
status_code: 200,
|
|
15920
|
+
body: {
|
|
15921
|
+
success: true,
|
|
15922
|
+
sessions: sessions.length,
|
|
15923
|
+
batches: batchesRun,
|
|
15924
|
+
nodes: totalNodes,
|
|
15925
|
+
edges: totalEdges
|
|
15926
|
+
}
|
|
15927
|
+
};
|
|
15928
|
+
} catch {
|
|
15929
|
+
return graphDisabledResponse();
|
|
15930
|
+
}
|
|
15931
|
+
});
|
|
15932
|
+
sdk.registerTrigger({
|
|
15933
|
+
type: "http",
|
|
15934
|
+
function_id: "api::graph-build",
|
|
15935
|
+
config: {
|
|
15936
|
+
api_path: "/agentmemory/graph/build",
|
|
15937
|
+
http_method: "POST"
|
|
15938
|
+
}
|
|
15939
|
+
});
|
|
15317
15940
|
sdk.registerFunction("api::consolidate-pipeline", async (req) => {
|
|
15318
15941
|
const authErr = checkAuth(req, secret);
|
|
15319
15942
|
if (authErr) return authErr;
|
|
@@ -15574,9 +16197,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15574
16197
|
const authErr = checkAuth(req, secret);
|
|
15575
16198
|
if (authErr) return authErr;
|
|
15576
16199
|
const memories = await kv.list(KV.memories);
|
|
16200
|
+
const latest = req.query_params?.["latest"] === "true";
|
|
16201
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
16202
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
16203
|
+
const explicitAgentId = normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0;
|
|
16204
|
+
const includeOrphans = req.query_params?.["includeOrphans"] === "true";
|
|
16205
|
+
const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
16206
|
+
let filtered = latest ? memories.filter((m) => m.isLatest) : memories;
|
|
16207
|
+
if (filterAgentId) filtered = filtered.filter((m) => m.agentId === filterAgentId || includeOrphans && m.agentId === void 0);
|
|
16208
|
+
if (req.query_params?.["count"] === "true") return {
|
|
16209
|
+
status_code: 200,
|
|
16210
|
+
body: {
|
|
16211
|
+
total: filtered.length,
|
|
16212
|
+
latestCount: filtered.filter((m) => m.isLatest).length
|
|
16213
|
+
}
|
|
16214
|
+
};
|
|
16215
|
+
const rawLimit = req.query_params?.["limit"];
|
|
16216
|
+
const rawOffset = req.query_params?.["offset"];
|
|
16217
|
+
const parsedLimit = typeof rawLimit === "string" ? Number(rawLimit) : NaN;
|
|
16218
|
+
const parsedOffset = typeof rawOffset === "string" ? Number(rawOffset) : NaN;
|
|
16219
|
+
const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 5e3) : void 0;
|
|
16220
|
+
const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
|
|
15577
16221
|
return {
|
|
15578
16222
|
status_code: 200,
|
|
15579
|
-
body: {
|
|
16223
|
+
body: {
|
|
16224
|
+
memories: limit !== void 0 ? filtered.slice(offset, offset + limit) : filtered,
|
|
16225
|
+
total: filtered.length,
|
|
16226
|
+
offset,
|
|
16227
|
+
limit: limit ?? null
|
|
16228
|
+
}
|
|
15580
16229
|
};
|
|
15581
16230
|
});
|
|
15582
16231
|
sdk.registerTrigger({
|
|
@@ -15745,6 +16394,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15745
16394
|
sdk.registerFunction("api::slot-list", async (req) => {
|
|
15746
16395
|
const authErr = checkAuth(req, secret);
|
|
15747
16396
|
if (authErr) return authErr;
|
|
16397
|
+
if (!isSlotsEnabled()) return slotsDisabledResponse();
|
|
15748
16398
|
return {
|
|
15749
16399
|
status_code: 200,
|
|
15750
16400
|
body: await sdk.trigger({
|
|
@@ -15764,6 +16414,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15764
16414
|
sdk.registerFunction("api::slot-get", async (req) => {
|
|
15765
16415
|
const authErr = checkAuth(req, secret);
|
|
15766
16416
|
if (authErr) return authErr;
|
|
16417
|
+
if (!isSlotsEnabled()) return slotsDisabledResponse();
|
|
15767
16418
|
const label = asNonEmptyString$1(req.query_params?.["label"]);
|
|
15768
16419
|
if (!label) return {
|
|
15769
16420
|
status_code: 400,
|
|
@@ -15794,6 +16445,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15794
16445
|
sdk.registerFunction("api::slot-create", async (req) => {
|
|
15795
16446
|
const authErr = checkAuth(req, secret);
|
|
15796
16447
|
if (authErr) return authErr;
|
|
16448
|
+
if (!isSlotsEnabled()) return slotsDisabledResponse();
|
|
15797
16449
|
const body = req.body ?? {};
|
|
15798
16450
|
const label = asNonEmptyString$1(body["label"]);
|
|
15799
16451
|
if (!label) return {
|
|
@@ -15856,6 +16508,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15856
16508
|
sdk.registerFunction("api::slot-append", async (req) => {
|
|
15857
16509
|
const authErr = checkAuth(req, secret);
|
|
15858
16510
|
if (authErr) return authErr;
|
|
16511
|
+
if (!isSlotsEnabled()) return slotsDisabledResponse();
|
|
15859
16512
|
const body = req.body ?? {};
|
|
15860
16513
|
const label = asNonEmptyString$1(body["label"]);
|
|
15861
16514
|
const text = typeof body["text"] === "string" ? body["text"] : null;
|
|
@@ -15895,6 +16548,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15895
16548
|
sdk.registerFunction("api::slot-replace", async (req) => {
|
|
15896
16549
|
const authErr = checkAuth(req, secret);
|
|
15897
16550
|
if (authErr) return authErr;
|
|
16551
|
+
if (!isSlotsEnabled()) return slotsDisabledResponse();
|
|
15898
16552
|
const body = req.body ?? {};
|
|
15899
16553
|
const label = asNonEmptyString$1(body["label"]);
|
|
15900
16554
|
const content = body["content"];
|
|
@@ -15934,6 +16588,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15934
16588
|
sdk.registerFunction("api::slot-delete", async (req) => {
|
|
15935
16589
|
const authErr = checkAuth(req, secret);
|
|
15936
16590
|
if (authErr) return authErr;
|
|
16591
|
+
if (!isSlotsEnabled()) return slotsDisabledResponse();
|
|
15937
16592
|
const label = asNonEmptyString$1(req.query_params?.["label"]);
|
|
15938
16593
|
if (!label) return {
|
|
15939
16594
|
status_code: 400,
|
|
@@ -15964,6 +16619,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15964
16619
|
sdk.registerFunction("api::slot-reflect", async (req) => {
|
|
15965
16620
|
const authErr = checkAuth(req, secret);
|
|
15966
16621
|
if (authErr) return authErr;
|
|
16622
|
+
if (!isSlotsEnabled()) return slotsDisabledResponse();
|
|
16623
|
+
if (!isReflectEnabled()) return reflectDisabledResponse();
|
|
15967
16624
|
const body = req.body ?? {};
|
|
15968
16625
|
const sessionId = asNonEmptyString$1(body["sessionId"]);
|
|
15969
16626
|
if (!sessionId) return {
|
|
@@ -17616,6 +18273,10 @@ const CORE_TOOLS = [
|
|
|
17616
18273
|
files: {
|
|
17617
18274
|
type: "string",
|
|
17618
18275
|
description: "Comma-separated relevant file paths"
|
|
18276
|
+
},
|
|
18277
|
+
project: {
|
|
18278
|
+
type: "string",
|
|
18279
|
+
description: "Stable canonical project identifier this memory belongs to (e.g. a slug, UUID, or registry key). Must match the value used when the session was started. Do not use filesystem paths or ad-hoc display names — those change across machines and will silently break project scoping."
|
|
17619
18280
|
}
|
|
17620
18281
|
},
|
|
17621
18282
|
required: ["content"]
|
|
@@ -18656,8 +19317,8 @@ function getAllTools() {
|
|
|
18656
19317
|
];
|
|
18657
19318
|
}
|
|
18658
19319
|
function getVisibleTools() {
|
|
18659
|
-
if ((process.env["AGENTMEMORY_TOOLS"] || "
|
|
18660
|
-
return getAllTools()
|
|
19320
|
+
if ((process.env["AGENTMEMORY_TOOLS"] || "all") === "core") return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
|
|
19321
|
+
return getAllTools();
|
|
18661
19322
|
}
|
|
18662
19323
|
|
|
18663
19324
|
//#endregion
|
|
@@ -18772,13 +19433,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
18772
19433
|
const type = args.type || "fact";
|
|
18773
19434
|
const concepts = typeof args.concepts === "string" ? args.concepts.split(",").map((c) => c.trim()).filter(Boolean) : [];
|
|
18774
19435
|
const files = typeof args.files === "string" ? args.files.split(",").map((f) => f.trim()).filter(Boolean) : [];
|
|
19436
|
+
const project = typeof args.project === "string" && args.project.trim().length > 0 ? args.project.trim() : void 0;
|
|
18775
19437
|
const result = await sdk.trigger({
|
|
18776
19438
|
function_id: "mem::remember",
|
|
18777
19439
|
payload: {
|
|
18778
19440
|
content: args.content,
|
|
18779
19441
|
type,
|
|
18780
19442
|
concepts,
|
|
18781
|
-
files
|
|
19443
|
+
files,
|
|
19444
|
+
...project !== void 0 && { project }
|
|
18782
19445
|
}
|
|
18783
19446
|
});
|
|
18784
19447
|
return {
|
|
@@ -20295,201 +20958,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
20295
20958
|
});
|
|
20296
20959
|
}
|
|
20297
20960
|
|
|
20298
|
-
//#endregion
|
|
20299
|
-
//#region src/viewer/server.ts
|
|
20300
|
-
function loadViewerFavicon() {
|
|
20301
|
-
const base = dirname(fileURLToPath(import.meta.url));
|
|
20302
|
-
const candidates = [
|
|
20303
|
-
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
20304
|
-
join(base, "..", "viewer", "favicon.svg"),
|
|
20305
|
-
join(base, "viewer", "favicon.svg")
|
|
20306
|
-
];
|
|
20307
|
-
for (const path of candidates) try {
|
|
20308
|
-
return readFileSync(path);
|
|
20309
|
-
} catch {}
|
|
20310
|
-
return null;
|
|
20311
|
-
}
|
|
20312
|
-
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());
|
|
20313
|
-
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
20314
|
-
function buildAllowedHosts(origins, listenPort) {
|
|
20315
|
-
const hosts = /* @__PURE__ */ new Set();
|
|
20316
|
-
for (const o of origins) try {
|
|
20317
|
-
const parsed = new URL(o);
|
|
20318
|
-
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
20319
|
-
} catch {}
|
|
20320
|
-
hosts.add(`localhost:${listenPort}`);
|
|
20321
|
-
hosts.add(`127.0.0.1:${listenPort}`);
|
|
20322
|
-
hosts.add(`[::1]:${listenPort}`);
|
|
20323
|
-
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
20324
|
-
return hosts;
|
|
20325
|
-
}
|
|
20326
|
-
function isHostAllowed(headerHost, allowed) {
|
|
20327
|
-
if (typeof headerHost !== "string") return false;
|
|
20328
|
-
const lower = headerHost.toLowerCase().trim();
|
|
20329
|
-
if (!lower) return false;
|
|
20330
|
-
return allowed.has(lower);
|
|
20331
|
-
}
|
|
20332
|
-
function corsHeaders(req) {
|
|
20333
|
-
const origin = req.headers.origin || "";
|
|
20334
|
-
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
20335
|
-
return {
|
|
20336
|
-
"Access-Control-Allow-Origin": allowed,
|
|
20337
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
20338
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
20339
|
-
Vary: "Origin"
|
|
20340
|
-
};
|
|
20341
|
-
}
|
|
20342
|
-
function json(res, status, data, req) {
|
|
20343
|
-
const body = JSON.stringify(data);
|
|
20344
|
-
const cors = req ? corsHeaders(req) : {
|
|
20345
|
-
"Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
|
|
20346
|
-
Vary: "Origin"
|
|
20347
|
-
};
|
|
20348
|
-
res.writeHead(status, {
|
|
20349
|
-
...cors,
|
|
20350
|
-
"Content-Type": "application/json"
|
|
20351
|
-
});
|
|
20352
|
-
res.end(body);
|
|
20353
|
-
}
|
|
20354
|
-
function readBody(req) {
|
|
20355
|
-
return new Promise((resolve, reject) => {
|
|
20356
|
-
let data = "";
|
|
20357
|
-
let size = 0;
|
|
20358
|
-
req.on("data", (chunk) => {
|
|
20359
|
-
size += chunk.length;
|
|
20360
|
-
if (size > 1e6) {
|
|
20361
|
-
req.destroy();
|
|
20362
|
-
reject(/* @__PURE__ */ new Error("too large"));
|
|
20363
|
-
return;
|
|
20364
|
-
}
|
|
20365
|
-
data += chunk.toString();
|
|
20366
|
-
});
|
|
20367
|
-
req.on("end", () => resolve(data));
|
|
20368
|
-
req.on("error", reject);
|
|
20369
|
-
});
|
|
20370
|
-
}
|
|
20371
|
-
const MAX_VIEWER_PORT_RETRIES = 10;
|
|
20372
|
-
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
20373
|
-
const resolvedRestPort = restPort ?? port - 2;
|
|
20374
|
-
const requestedPort = port;
|
|
20375
|
-
let allowedHosts = null;
|
|
20376
|
-
const server = createServer(async (req, res) => {
|
|
20377
|
-
if (!allowedHosts) {
|
|
20378
|
-
const addr = server.address();
|
|
20379
|
-
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
20380
|
-
}
|
|
20381
|
-
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
20382
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
20383
|
-
res.end("forbidden host");
|
|
20384
|
-
return;
|
|
20385
|
-
}
|
|
20386
|
-
const raw = req.url || "/";
|
|
20387
|
-
const qIdx = raw.indexOf("?");
|
|
20388
|
-
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
20389
|
-
const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
|
|
20390
|
-
const method = req.method || "GET";
|
|
20391
|
-
if (method === "OPTIONS") {
|
|
20392
|
-
res.writeHead(204, {
|
|
20393
|
-
...corsHeaders(req),
|
|
20394
|
-
"Access-Control-Max-Age": "86400"
|
|
20395
|
-
});
|
|
20396
|
-
res.end();
|
|
20397
|
-
return;
|
|
20398
|
-
}
|
|
20399
|
-
if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
|
|
20400
|
-
const rendered = renderViewerDocument();
|
|
20401
|
-
if (rendered.found) {
|
|
20402
|
-
res.writeHead(200, {
|
|
20403
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
20404
|
-
"Content-Security-Policy": rendered.csp,
|
|
20405
|
-
"Cache-Control": "no-cache"
|
|
20406
|
-
});
|
|
20407
|
-
res.end(rendered.html);
|
|
20408
|
-
return;
|
|
20409
|
-
}
|
|
20410
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
20411
|
-
res.end("viewer not found");
|
|
20412
|
-
return;
|
|
20413
|
-
}
|
|
20414
|
-
if (method === "GET" && pathname === "/favicon.svg") {
|
|
20415
|
-
const favicon = loadViewerFavicon();
|
|
20416
|
-
if (favicon) {
|
|
20417
|
-
res.writeHead(200, {
|
|
20418
|
-
"Content-Type": "image/svg+xml",
|
|
20419
|
-
"Cache-Control": "public, max-age=3600"
|
|
20420
|
-
});
|
|
20421
|
-
res.end(favicon);
|
|
20422
|
-
return;
|
|
20423
|
-
}
|
|
20424
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
20425
|
-
res.end("favicon not found");
|
|
20426
|
-
return;
|
|
20427
|
-
}
|
|
20428
|
-
try {
|
|
20429
|
-
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
20430
|
-
} catch (err) {
|
|
20431
|
-
console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
|
|
20432
|
-
json(res, 502, { error: "upstream error" }, req);
|
|
20433
|
-
}
|
|
20434
|
-
});
|
|
20435
|
-
let attempt = 0;
|
|
20436
|
-
let currentPort = requestedPort;
|
|
20437
|
-
const tryListen = () => {
|
|
20438
|
-
server.listen(currentPort, "127.0.0.1");
|
|
20439
|
-
};
|
|
20440
|
-
server.on("listening", () => {
|
|
20441
|
-
if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
|
|
20442
|
-
else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
|
|
20443
|
-
});
|
|
20444
|
-
server.on("error", (err) => {
|
|
20445
|
-
if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
|
|
20446
|
-
attempt++;
|
|
20447
|
-
currentPort = requestedPort + attempt;
|
|
20448
|
-
setImmediate(tryListen);
|
|
20449
|
-
return;
|
|
20450
|
-
}
|
|
20451
|
-
if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
|
|
20452
|
-
else console.error(`[agentmemory] Viewer error:`, err.message);
|
|
20453
|
-
});
|
|
20454
|
-
tryListen();
|
|
20455
|
-
return server;
|
|
20456
|
-
}
|
|
20457
|
-
async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
|
|
20458
|
-
const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
|
|
20459
|
-
const headers = {};
|
|
20460
|
-
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
|
20461
|
-
const ct = req.headers["content-type"];
|
|
20462
|
-
if (ct) headers["Content-Type"] = ct;
|
|
20463
|
-
let body;
|
|
20464
|
-
if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
|
|
20465
|
-
const controller = new AbortController();
|
|
20466
|
-
const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
|
|
20467
|
-
let upstream;
|
|
20468
|
-
try {
|
|
20469
|
-
upstream = await fetch(upstreamUrl, {
|
|
20470
|
-
method,
|
|
20471
|
-
headers,
|
|
20472
|
-
body: body || void 0,
|
|
20473
|
-
signal: controller.signal
|
|
20474
|
-
});
|
|
20475
|
-
clearTimeout(fetchTimeout);
|
|
20476
|
-
} catch (err) {
|
|
20477
|
-
clearTimeout(fetchTimeout);
|
|
20478
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
20479
|
-
json(res, 504, { error: "upstream timeout" }, req);
|
|
20480
|
-
return;
|
|
20481
|
-
}
|
|
20482
|
-
throw err;
|
|
20483
|
-
}
|
|
20484
|
-
const cors = corsHeaders(req);
|
|
20485
|
-
const responseBody = await upstream.text();
|
|
20486
|
-
const responseHeaders = { ...cors };
|
|
20487
|
-
const upstreamCt = upstream.headers.get("content-type");
|
|
20488
|
-
if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
|
|
20489
|
-
res.writeHead(upstream.status, responseHeaders);
|
|
20490
|
-
res.end(responseBody);
|
|
20491
|
-
}
|
|
20492
|
-
|
|
20493
20961
|
//#endregion
|
|
20494
20962
|
//#region src/eval/metrics-store.ts
|
|
20495
20963
|
var MetricsStore = class {
|
|
@@ -20627,6 +21095,21 @@ function initMetrics(getMeter) {
|
|
|
20627
21095
|
|
|
20628
21096
|
//#endregion
|
|
20629
21097
|
//#region src/index.ts
|
|
21098
|
+
function workerPidfilePath() {
|
|
21099
|
+
return join(homedir(), ".agentmemory", "worker.pid");
|
|
21100
|
+
}
|
|
21101
|
+
function writeWorkerPidfile() {
|
|
21102
|
+
try {
|
|
21103
|
+
const p = workerPidfilePath();
|
|
21104
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
21105
|
+
writeFileSync(p, `${process.pid}\n`, { encoding: "utf-8" });
|
|
21106
|
+
} catch {}
|
|
21107
|
+
}
|
|
21108
|
+
function clearWorkerPidfile() {
|
|
21109
|
+
try {
|
|
21110
|
+
unlinkSync(workerPidfilePath());
|
|
21111
|
+
} catch {}
|
|
21112
|
+
}
|
|
20630
21113
|
function hasGetMeter(sdk) {
|
|
20631
21114
|
return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
|
|
20632
21115
|
}
|
|
@@ -20667,6 +21150,7 @@ async function main() {
|
|
|
20667
21150
|
framework: "iii-sdk"
|
|
20668
21151
|
}
|
|
20669
21152
|
});
|
|
21153
|
+
writeWorkerPidfile();
|
|
20670
21154
|
const kv = new StateKV(sdk);
|
|
20671
21155
|
const secret = getEnvVar("AGENTMEMORY_SECRET");
|
|
20672
21156
|
const metricsStore = new MetricsStore(kv);
|
|
@@ -20762,6 +21246,7 @@ async function main() {
|
|
|
20762
21246
|
registerMcpEndpoints(sdk, kv, secret);
|
|
20763
21247
|
const healthMonitor = registerHealthMonitor(sdk, kv);
|
|
20764
21248
|
const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
|
|
21249
|
+
setIndexPersistence(indexPersistence);
|
|
20765
21250
|
const loaded = await indexPersistence.load().catch((err) => {
|
|
20766
21251
|
console.warn(`[agentmemory] Failed to load persisted index:`, err);
|
|
20767
21252
|
return null;
|
|
@@ -20803,7 +21288,7 @@ async function main() {
|
|
|
20803
21288
|
if (bm25Index.has(memory.id)) continue;
|
|
20804
21289
|
bm25Index.add({
|
|
20805
21290
|
id: memory.id,
|
|
20806
|
-
sessionId: memory.sessionIds[0] ?? "memory",
|
|
21291
|
+
sessionId: memory.sessionIds?.[0] ?? "memory",
|
|
20807
21292
|
timestamp: memory.createdAt,
|
|
20808
21293
|
type: "decision",
|
|
20809
21294
|
title: memory.title,
|
|
@@ -20823,7 +21308,7 @@ async function main() {
|
|
|
20823
21308
|
console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
|
|
20824
21309
|
}
|
|
20825
21310
|
bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
20826
|
-
bootLog(`REST API:
|
|
21311
|
+
bootLog(`REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
|
|
20827
21312
|
bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
|
|
20828
21313
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
20829
21314
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|
|
@@ -20879,6 +21364,7 @@ async function main() {
|
|
|
20879
21364
|
console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
|
|
20880
21365
|
});
|
|
20881
21366
|
await sdk.shutdown();
|
|
21367
|
+
clearWorkerPidfile();
|
|
20882
21368
|
process.exit(0);
|
|
20883
21369
|
};
|
|
20884
21370
|
process.on("SIGINT", shutdown);
|