@agentmemory/agentmemory 0.9.20 → 0.9.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/README.md +166 -12
- package/dist/.env.example +2 -0
- package/dist/cli.d.mts +5 -1
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +122 -693
- package/dist/cli.mjs.map +1 -1
- package/dist/connect-BQQXpyDS.mjs +763 -0
- package/dist/connect-BQQXpyDS.mjs.map +1 -0
- package/dist/hooks/post-tool-use.mjs +1 -1
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/stop.mjs +8 -0
- package/dist/hooks/stop.mjs.map +1 -1
- 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 +881 -281
- 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-DPSaLB5-.mjs → src-gpTAJuBy.mjs} +861 -287
- package/dist/src-gpTAJuBy.mjs.map +1 -0
- package/dist/{standalone-DMLk7YxP.mjs → standalone-C4i7ktpn.mjs} +48 -12
- package/dist/standalone-C4i7ktpn.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +45 -10
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-Dz8ssuMf.mjs → tools-registry-B7Y6nJsr.mjs} +39 -11
- package/dist/tools-registry-B7Y6nJsr.mjs.map +1 -0
- package/dist/version-DvQMNbEH.mjs +6 -0
- package/dist/version-DvQMNbEH.mjs.map +1 -0
- package/dist/viewer/index.html +134 -21
- package/package.json +6 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +3 -2
- package/plugin/hooks/hooks.codex.json +6 -6
- package/plugin/hooks/hooks.json +12 -12
- package/plugin/opencode/README.md +229 -0
- package/plugin/opencode/agentmemory-capture.ts +687 -0
- package/plugin/opencode/commands/recall.md +19 -0
- package/plugin/opencode/commands/remember.md +19 -0
- package/plugin/opencode/plugin.json +12 -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/post-tool-use.mjs +1 -1
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +8 -0
- package/plugin/scripts/stop.mjs.map +1 -1
- package/dist/src-DPSaLB5-.mjs.map +0 -1
- package/dist/standalone-DMLk7YxP.mjs.map +0 -1
- package/dist/tools-registry-Dz8ssuMf.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 {
|
|
@@ -137,6 +145,9 @@ function getMergedEnv(overrides) {
|
|
|
137
145
|
function getEnvVar(key) {
|
|
138
146
|
return getMergedEnv()[key];
|
|
139
147
|
}
|
|
148
|
+
function isDropStaleIndexEnabled() {
|
|
149
|
+
return getMergedEnv()["AGENTMEMORY_DROP_STALE_INDEX"] === "true";
|
|
150
|
+
}
|
|
140
151
|
function detectLlmProviderKind() {
|
|
141
152
|
const env = getMergedEnv();
|
|
142
153
|
if (hasRealValue(env["ANTHROPIC_API_KEY"]) || hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"]) || hasRealValue(env["OPENROUTER_API_KEY"]) || hasRealValue(env["MINIMAX_API_KEY"]) || hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") return "llm";
|
|
@@ -172,8 +183,8 @@ function loadClaudeBridgeConfig() {
|
|
|
172
183
|
const lineBudget = safeParseInt(env["CLAUDE_MEMORY_LINE_BUDGET"], 200);
|
|
173
184
|
let memoryFilePath = "";
|
|
174
185
|
if (enabled && projectPath) {
|
|
175
|
-
const safePath = projectPath.replace(/[/\\]/g, "-")
|
|
176
|
-
memoryFilePath = join(homedir(), ".claude", "projects", safePath, "
|
|
186
|
+
const safePath = projectPath.replace(/[/\\]/g, "-");
|
|
187
|
+
memoryFilePath = join(homedir(), ".claude", "projects", safePath, "MEMORY.md");
|
|
177
188
|
}
|
|
178
189
|
return {
|
|
179
190
|
enabled,
|
|
@@ -193,6 +204,23 @@ function loadTeamConfig() {
|
|
|
193
204
|
mode: env["TEAM_MODE"] === "shared" ? "shared" : "private"
|
|
194
205
|
};
|
|
195
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
|
+
}
|
|
196
224
|
function loadSnapshotConfig() {
|
|
197
225
|
const env = getMergedEnv();
|
|
198
226
|
return {
|
|
@@ -450,13 +478,25 @@ function v1AzureUrl(baseUrl, path) {
|
|
|
450
478
|
url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
|
|
451
479
|
return url.toString();
|
|
452
480
|
}
|
|
481
|
+
function appendOpenAIRoute(baseUrl, route) {
|
|
482
|
+
const trimmedBase = baseUrl.replace(/\/+$/, "");
|
|
483
|
+
const cleanRoute = route.startsWith("/") ? route : `/${route}`;
|
|
484
|
+
let pathname;
|
|
485
|
+
try {
|
|
486
|
+
pathname = new URL(trimmedBase).pathname.replace(/\/+$/, "");
|
|
487
|
+
} catch {
|
|
488
|
+
return `${trimmedBase}/v1${cleanRoute}`;
|
|
489
|
+
}
|
|
490
|
+
if (pathname === "" || pathname === "/") return `${trimmedBase}/v1${cleanRoute}`;
|
|
491
|
+
return `${trimmedBase}${cleanRoute}`;
|
|
492
|
+
}
|
|
453
493
|
function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
|
|
454
494
|
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
|
|
455
|
-
return
|
|
495
|
+
return appendOpenAIRoute(baseUrl, "/chat/completions");
|
|
456
496
|
}
|
|
457
497
|
function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
|
|
458
498
|
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
|
|
459
|
-
return
|
|
499
|
+
return appendOpenAIRoute(baseUrl, "/embeddings");
|
|
460
500
|
}
|
|
461
501
|
function buildAuthHeaders(apiKey, isAzure) {
|
|
462
502
|
if (isAzure) return {
|
|
@@ -538,6 +578,7 @@ var OpenAIProvider = class {
|
|
|
538
578
|
const body = {
|
|
539
579
|
model: this.model,
|
|
540
580
|
max_tokens: this.maxTokens,
|
|
581
|
+
stream: false,
|
|
541
582
|
messages: [{
|
|
542
583
|
role: "system",
|
|
543
584
|
content: systemPrompt
|
|
@@ -566,7 +607,7 @@ var OpenAIProvider = class {
|
|
|
566
607
|
const message = data.choices?.[0]?.message;
|
|
567
608
|
const content = message?.content;
|
|
568
609
|
if (content) return content;
|
|
569
|
-
const reasoning = message?.reasoning;
|
|
610
|
+
const reasoning = message?.reasoning ?? message?.reasoning_content;
|
|
570
611
|
if (reasoning) return reasoning;
|
|
571
612
|
throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
572
613
|
}
|
|
@@ -2511,6 +2552,24 @@ var SearchIndex = class SearchIndex {
|
|
|
2511
2552
|
has(id) {
|
|
2512
2553
|
return this.entries.has(id);
|
|
2513
2554
|
}
|
|
2555
|
+
remove(id) {
|
|
2556
|
+
const entry = this.entries.get(id);
|
|
2557
|
+
if (!entry) return;
|
|
2558
|
+
const termFreq = this.docTermCounts.get(id);
|
|
2559
|
+
if (termFreq) {
|
|
2560
|
+
for (const term of termFreq.keys()) {
|
|
2561
|
+
const postingList = this.invertedIndex.get(term);
|
|
2562
|
+
if (postingList) {
|
|
2563
|
+
postingList.delete(id);
|
|
2564
|
+
if (postingList.size === 0) this.invertedIndex.delete(term);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
this.docTermCounts.delete(id);
|
|
2568
|
+
}
|
|
2569
|
+
this.totalDocLength = Math.max(0, this.totalDocLength - entry.termCount);
|
|
2570
|
+
this.entries.delete(id);
|
|
2571
|
+
this.sortedTerms = null;
|
|
2572
|
+
}
|
|
2514
2573
|
search(query, limit = 20) {
|
|
2515
2574
|
const rawTerms = this.tokenize(query.toLowerCase());
|
|
2516
2575
|
if (rawTerms.length === 0) return [];
|
|
@@ -2863,6 +2922,7 @@ function buildSyntheticCompression(raw) {
|
|
|
2863
2922
|
};
|
|
2864
2923
|
if (raw.modality) result.modality = raw.modality;
|
|
2865
2924
|
if (raw.imageData) result.imageData = raw.imageData;
|
|
2925
|
+
if (raw.agentId) result.agentId = raw.agentId;
|
|
2866
2926
|
return result;
|
|
2867
2927
|
}
|
|
2868
2928
|
|
|
@@ -2952,6 +3012,16 @@ function setVectorIndex(idx) {
|
|
|
2952
3012
|
function setEmbeddingProvider(provider) {
|
|
2953
3013
|
currentEmbeddingProvider = provider;
|
|
2954
3014
|
}
|
|
3015
|
+
function vectorIndexRemove(id) {
|
|
3016
|
+
vectorIndex?.remove(id);
|
|
3017
|
+
}
|
|
3018
|
+
let indexPersistence = null;
|
|
3019
|
+
function setIndexPersistence(p) {
|
|
3020
|
+
indexPersistence = p;
|
|
3021
|
+
}
|
|
3022
|
+
async function flushIndexSave() {
|
|
3023
|
+
await indexPersistence?.save();
|
|
3024
|
+
}
|
|
2955
3025
|
const EMBED_MAX_CHARS = 16e3;
|
|
2956
3026
|
function clipEmbedInput(text) {
|
|
2957
3027
|
if (text.length <= EMBED_MAX_CHARS) return text;
|
|
@@ -2985,20 +3055,108 @@ async function vectorIndexAddGuarded(id, sessionId, text, context) {
|
|
|
2985
3055
|
return false;
|
|
2986
3056
|
}
|
|
2987
3057
|
}
|
|
3058
|
+
async function vectorIndexAddBatchGuarded(items) {
|
|
3059
|
+
const vi = vectorIndex;
|
|
3060
|
+
const ep = currentEmbeddingProvider;
|
|
3061
|
+
if (!vi || !ep || items.length === 0) return {
|
|
3062
|
+
ok: 0,
|
|
3063
|
+
fail: 0
|
|
3064
|
+
};
|
|
3065
|
+
let embeddings;
|
|
3066
|
+
try {
|
|
3067
|
+
embeddings = await ep.embedBatch(items.map((i) => clipEmbedInput(i.text)));
|
|
3068
|
+
} catch (err) {
|
|
3069
|
+
logger.warn("vector-index add batch: embed failed — skipping batch", {
|
|
3070
|
+
batchSize: items.length,
|
|
3071
|
+
provider: ep.name,
|
|
3072
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3073
|
+
});
|
|
3074
|
+
return {
|
|
3075
|
+
ok: 0,
|
|
3076
|
+
fail: items.length
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
if (embeddings.length !== items.length) {
|
|
3080
|
+
logger.warn("vector-index add batch: provider returned wrong length — skipping batch", {
|
|
3081
|
+
batchSize: items.length,
|
|
3082
|
+
returned: embeddings.length,
|
|
3083
|
+
provider: ep.name
|
|
3084
|
+
});
|
|
3085
|
+
return {
|
|
3086
|
+
ok: 0,
|
|
3087
|
+
fail: items.length
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
let ok = 0;
|
|
3091
|
+
let fail = 0;
|
|
3092
|
+
for (let i = 0; i < items.length; i++) {
|
|
3093
|
+
const item = items[i];
|
|
3094
|
+
const embedding = embeddings[i];
|
|
3095
|
+
if (embedding.length !== ep.dimensions) {
|
|
3096
|
+
logger.warn("vector-index add batch: dimension mismatch — skipping item", {
|
|
3097
|
+
kind: item.context.kind,
|
|
3098
|
+
id: item.context.logId,
|
|
3099
|
+
provider: ep.name,
|
|
3100
|
+
expected: ep.dimensions,
|
|
3101
|
+
received: embedding.length
|
|
3102
|
+
});
|
|
3103
|
+
fail++;
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
try {
|
|
3107
|
+
vi.add(item.id, item.sessionId, embedding);
|
|
3108
|
+
ok++;
|
|
3109
|
+
} catch (err) {
|
|
3110
|
+
logger.warn("vector-index add batch: index write failed — skipping item", {
|
|
3111
|
+
kind: item.context.kind,
|
|
3112
|
+
id: item.context.logId,
|
|
3113
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3114
|
+
});
|
|
3115
|
+
fail++;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
return {
|
|
3119
|
+
ok,
|
|
3120
|
+
fail
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
const DEFAULT_REBUILD_EMBED_BATCH = 32;
|
|
3124
|
+
function getRebuildEmbedBatchSize() {
|
|
3125
|
+
const raw = process.env.REBUILD_EMBED_BATCH_SIZE;
|
|
3126
|
+
if (!raw) return DEFAULT_REBUILD_EMBED_BATCH;
|
|
3127
|
+
const n = parseInt(raw, 10);
|
|
3128
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REBUILD_EMBED_BATCH;
|
|
3129
|
+
}
|
|
2988
3130
|
async function rebuildIndex(kv) {
|
|
2989
3131
|
const idx = getSearchIndex();
|
|
2990
3132
|
idx.clear();
|
|
2991
3133
|
vectorIndex?.clear();
|
|
3134
|
+
const batchSize = getRebuildEmbedBatchSize();
|
|
3135
|
+
const pending = [];
|
|
2992
3136
|
let count = 0;
|
|
3137
|
+
const flush = async () => {
|
|
3138
|
+
if (pending.length === 0) return;
|
|
3139
|
+
await vectorIndexAddBatchGuarded(pending);
|
|
3140
|
+
pending.length = 0;
|
|
3141
|
+
};
|
|
3142
|
+
const enqueue = async (job) => {
|
|
3143
|
+
pending.push(job);
|
|
3144
|
+
if (pending.length >= batchSize) await flush();
|
|
3145
|
+
};
|
|
2993
3146
|
try {
|
|
2994
3147
|
const memories = await kv.list(KV.memories);
|
|
2995
3148
|
for (const memory of memories) {
|
|
2996
3149
|
if (memory.isLatest === false) continue;
|
|
2997
3150
|
if (!memory.title || !memory.content) continue;
|
|
2998
3151
|
idx.add(memoryToObservation(memory));
|
|
2999
|
-
await
|
|
3000
|
-
|
|
3001
|
-
|
|
3152
|
+
await enqueue({
|
|
3153
|
+
id: memory.id,
|
|
3154
|
+
sessionId: memory.sessionIds[0] ?? "memory",
|
|
3155
|
+
text: memory.title + " " + memory.content,
|
|
3156
|
+
context: {
|
|
3157
|
+
kind: "memory",
|
|
3158
|
+
logId: memory.id
|
|
3159
|
+
}
|
|
3002
3160
|
});
|
|
3003
3161
|
count++;
|
|
3004
3162
|
}
|
|
@@ -3006,7 +3164,10 @@ async function rebuildIndex(kv) {
|
|
|
3006
3164
|
logger.warn("rebuildIndex: failed to load memories", { error: err instanceof Error ? err.message : String(err) });
|
|
3007
3165
|
}
|
|
3008
3166
|
const sessions = await kv.list(KV.sessions);
|
|
3009
|
-
if (!sessions.length)
|
|
3167
|
+
if (!sessions.length) {
|
|
3168
|
+
await flush();
|
|
3169
|
+
return count;
|
|
3170
|
+
}
|
|
3010
3171
|
const obsPerSession = [];
|
|
3011
3172
|
const failedSessions = [];
|
|
3012
3173
|
for (let batch = 0; batch < sessions.length; batch += 10) {
|
|
@@ -3024,12 +3185,18 @@ async function rebuildIndex(kv) {
|
|
|
3024
3185
|
if (failedSessions.length > 0) logger.warn("rebuildIndex: failed to load observations for sessions", { failedSessions });
|
|
3025
3186
|
for (const observations of obsPerSession) for (const obs of observations) if (obs.title && obs.narrative) {
|
|
3026
3187
|
idx.add(obs);
|
|
3027
|
-
await
|
|
3028
|
-
|
|
3029
|
-
|
|
3188
|
+
await enqueue({
|
|
3189
|
+
id: obs.id,
|
|
3190
|
+
sessionId: obs.sessionId,
|
|
3191
|
+
text: obs.title + " " + obs.narrative,
|
|
3192
|
+
context: {
|
|
3193
|
+
kind: "observation",
|
|
3194
|
+
logId: obs.id
|
|
3195
|
+
}
|
|
3030
3196
|
});
|
|
3031
3197
|
count++;
|
|
3032
3198
|
}
|
|
3199
|
+
await flush();
|
|
3033
3200
|
return count;
|
|
3034
3201
|
}
|
|
3035
3202
|
function registerSearchFunction(sdk, kv) {
|
|
@@ -3249,6 +3416,9 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
3249
3416
|
error: `Session observation limit reached (${maxObservationsPerSession})`
|
|
3250
3417
|
};
|
|
3251
3418
|
}
|
|
3419
|
+
const existingSession = await kv.get(KV.sessions, payload.sessionId);
|
|
3420
|
+
const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
|
|
3421
|
+
if (inheritedAgentId) raw.agentId = inheritedAgentId;
|
|
3252
3422
|
if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
|
|
3253
3423
|
const { saveImageToDisk } = await Promise.resolve().then(() => image_store_exports);
|
|
3254
3424
|
const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
|
|
@@ -3300,7 +3470,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
3300
3470
|
},
|
|
3301
3471
|
action: TriggerAction.Void()
|
|
3302
3472
|
});
|
|
3303
|
-
const session =
|
|
3473
|
+
const session = existingSession;
|
|
3304
3474
|
if (session) {
|
|
3305
3475
|
const updates = [{
|
|
3306
3476
|
type: "set",
|
|
@@ -3320,6 +3490,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
3320
3490
|
});
|
|
3321
3491
|
}
|
|
3322
3492
|
await kv.update(KV.sessions, payload.sessionId, updates);
|
|
3493
|
+
} else if (typeof payload.project === "string" && payload.project.trim().length > 0 && typeof payload.cwd === "string" && payload.cwd.trim().length > 0) {
|
|
3494
|
+
const trimmedPrompt = typeof raw.userPrompt === "string" ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
|
|
3495
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3496
|
+
await kv.set(KV.sessions, payload.sessionId, {
|
|
3497
|
+
id: payload.sessionId,
|
|
3498
|
+
project: payload.project,
|
|
3499
|
+
cwd: payload.cwd,
|
|
3500
|
+
startedAt: payload.timestamp ?? ts,
|
|
3501
|
+
updatedAt: ts,
|
|
3502
|
+
status: "active",
|
|
3503
|
+
observationCount: 1,
|
|
3504
|
+
...inheritedAgentId ? { agentId: inheritedAgentId } : {},
|
|
3505
|
+
...trimmedPrompt && trimmedPrompt.length > 0 ? { firstPrompt: trimmedPrompt } : {}
|
|
3506
|
+
});
|
|
3323
3507
|
}
|
|
3324
3508
|
if (isAutoCompressEnabled()) await sdk.trigger({
|
|
3325
3509
|
function_id: "mem::compress",
|
|
@@ -4579,7 +4763,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
|
|
|
4579
4763
|
confidence: qualityScore / 100,
|
|
4580
4764
|
...hasImage ? { modality: data.raw.modality } : {},
|
|
4581
4765
|
...imageDescription ? { imageDescription } : {},
|
|
4582
|
-
...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
|
|
4766
|
+
...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
|
|
4767
|
+
...data.raw.agentId ? { agentId: data.raw.agentId } : {}
|
|
4583
4768
|
};
|
|
4584
4769
|
await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
|
|
4585
4770
|
try {
|
|
@@ -4814,9 +4999,134 @@ function buildSummaryPrompt(observations) {
|
|
|
4814
4999
|
});
|
|
4815
5000
|
return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
|
|
4816
5001
|
}
|
|
5002
|
+
const REDUCE_SYSTEM = `You are merging multiple partial summaries of the SAME coding session into one final session summary. The partials are chronological chunks of one continuous session — not separate sessions.
|
|
5003
|
+
|
|
5004
|
+
Output EXACTLY this XML format with no additional text:
|
|
5005
|
+
|
|
5006
|
+
<summary>
|
|
5007
|
+
<title>Short session title (max 100 chars)</title>
|
|
5008
|
+
<narrative>3-5 sentence narrative covering the whole session</narrative>
|
|
5009
|
+
<decisions>
|
|
5010
|
+
<decision>Key technical decision made</decision>
|
|
5011
|
+
</decisions>
|
|
5012
|
+
<files>
|
|
5013
|
+
<file>path/to/modified/file</file>
|
|
5014
|
+
</files>
|
|
5015
|
+
<concepts>
|
|
5016
|
+
<concept>key concept from session</concept>
|
|
5017
|
+
</concepts>
|
|
5018
|
+
</summary>
|
|
5019
|
+
|
|
5020
|
+
Rules:
|
|
5021
|
+
- Synthesize a single narrative that reflects the whole arc, not a chunk-by-chunk recap
|
|
5022
|
+
- Preserve every distinct decision across chunks
|
|
5023
|
+
- Union (deduplicate) all files and concepts
|
|
5024
|
+
- Title should capture the session's overall outcome`;
|
|
5025
|
+
function buildReducePrompt(partials) {
|
|
5026
|
+
const sections = partials.map((p, i) => {
|
|
5027
|
+
const decisions = p.keyDecisions.map((d) => ` - ${d}`).join("\n");
|
|
5028
|
+
const files = p.filesModified.map((f) => ` - ${f}`).join("\n");
|
|
5029
|
+
const concepts = p.concepts.join(", ");
|
|
5030
|
+
return `[Chunk ${i + 1} of ${partials.length} — obs ${p.obsRangeStart}-${p.obsRangeEnd}]
|
|
5031
|
+
Title: ${p.title}
|
|
5032
|
+
Narrative: ${p.narrative}
|
|
5033
|
+
Decisions:
|
|
5034
|
+
${decisions}
|
|
5035
|
+
Files:
|
|
5036
|
+
${files}
|
|
5037
|
+
Concepts: ${concepts}`;
|
|
5038
|
+
});
|
|
5039
|
+
return `Partial summaries (${partials.length} chunks of one session, chronological):\n\n${sections.join("\n\n---\n\n")}`;
|
|
5040
|
+
}
|
|
4817
5041
|
|
|
4818
5042
|
//#endregion
|
|
4819
5043
|
//#region src/functions/summarize.ts
|
|
5044
|
+
const CHUNK_SIZE_DEFAULT = 400;
|
|
5045
|
+
const CHUNK_CONCURRENCY_DEFAULT = 6;
|
|
5046
|
+
const MAX_SKIP_RATIO = .5;
|
|
5047
|
+
function getChunkSize() {
|
|
5048
|
+
const raw = process.env.SUMMARIZE_CHUNK_SIZE;
|
|
5049
|
+
if (!raw) return CHUNK_SIZE_DEFAULT;
|
|
5050
|
+
const n = parseInt(raw, 10);
|
|
5051
|
+
return Number.isFinite(n) && n > 0 ? n : CHUNK_SIZE_DEFAULT;
|
|
5052
|
+
}
|
|
5053
|
+
function getChunkConcurrency() {
|
|
5054
|
+
const raw = process.env.SUMMARIZE_CHUNK_CONCURRENCY;
|
|
5055
|
+
if (!raw) return CHUNK_CONCURRENCY_DEFAULT;
|
|
5056
|
+
const n = parseInt(raw, 10);
|
|
5057
|
+
return Number.isFinite(n) && n > 0 ? n : CHUNK_CONCURRENCY_DEFAULT;
|
|
5058
|
+
}
|
|
5059
|
+
async function summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, total) {
|
|
5060
|
+
for (let attempt = 1; attempt <= 2; attempt++) try {
|
|
5061
|
+
const parsed = parseSummaryXml(await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(chunk)), sessionId, project, chunk.length);
|
|
5062
|
+
if (parsed) return parsed;
|
|
5063
|
+
logger.warn("Summarize chunk parse failed", {
|
|
5064
|
+
sessionId,
|
|
5065
|
+
chunk: `${idx + 1}/${total}`,
|
|
5066
|
+
attempt
|
|
5067
|
+
});
|
|
5068
|
+
} catch (err) {
|
|
5069
|
+
logger.warn("Summarize chunk LLM call failed", {
|
|
5070
|
+
sessionId,
|
|
5071
|
+
chunk: `${idx + 1}/${total}`,
|
|
5072
|
+
attempt,
|
|
5073
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5074
|
+
});
|
|
5075
|
+
}
|
|
5076
|
+
return null;
|
|
5077
|
+
}
|
|
5078
|
+
async function produceSummaryXml(provider, compressed, sessionId, project) {
|
|
5079
|
+
const chunkSize = getChunkSize();
|
|
5080
|
+
if (compressed.length <= chunkSize) return {
|
|
5081
|
+
response: await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(compressed)),
|
|
5082
|
+
mode: "single",
|
|
5083
|
+
chunks: 1
|
|
5084
|
+
};
|
|
5085
|
+
const chunks = [];
|
|
5086
|
+
for (let i = 0; i < compressed.length; i += chunkSize) chunks.push(compressed.slice(i, i + chunkSize));
|
|
5087
|
+
const concurrency = getChunkConcurrency();
|
|
5088
|
+
logger.info("Summarize chunking session", {
|
|
5089
|
+
sessionId,
|
|
5090
|
+
chunks: chunks.length,
|
|
5091
|
+
chunkSize,
|
|
5092
|
+
concurrency,
|
|
5093
|
+
totalObservations: compressed.length
|
|
5094
|
+
});
|
|
5095
|
+
const partialByIdx = new Array(chunks.length).fill(null);
|
|
5096
|
+
for (let batchStart = 0; batchStart < chunks.length; batchStart += concurrency) {
|
|
5097
|
+
const batch = chunks.slice(batchStart, batchStart + concurrency);
|
|
5098
|
+
await Promise.all(batch.map(async (chunk, j) => {
|
|
5099
|
+
const idx = batchStart + j;
|
|
5100
|
+
partialByIdx[idx] = await summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, chunks.length);
|
|
5101
|
+
}));
|
|
5102
|
+
}
|
|
5103
|
+
const skipped = partialByIdx.filter((p) => p === null).length;
|
|
5104
|
+
const partials = partialByIdx.filter((p) => p !== null);
|
|
5105
|
+
if (skipped > Math.floor(chunks.length * MAX_SKIP_RATIO)) throw new Error(`too_many_chunks_skipped: ${skipped}/${chunks.length} chunks failed to parse after retry`);
|
|
5106
|
+
if (skipped > 0) logger.warn("Summarize chunks partially skipped", {
|
|
5107
|
+
sessionId,
|
|
5108
|
+
skipped,
|
|
5109
|
+
total: chunks.length
|
|
5110
|
+
});
|
|
5111
|
+
const reduceInput = partials.map((p) => {
|
|
5112
|
+
const originalIdx = partialByIdx.indexOf(p);
|
|
5113
|
+
return {
|
|
5114
|
+
title: p.title,
|
|
5115
|
+
narrative: p.narrative,
|
|
5116
|
+
keyDecisions: p.keyDecisions,
|
|
5117
|
+
filesModified: p.filesModified,
|
|
5118
|
+
concepts: p.concepts,
|
|
5119
|
+
obsRangeStart: originalIdx * chunkSize + 1,
|
|
5120
|
+
obsRangeEnd: Math.min((originalIdx + 1) * chunkSize, compressed.length)
|
|
5121
|
+
};
|
|
5122
|
+
});
|
|
5123
|
+
return {
|
|
5124
|
+
response: await provider.summarize(REDUCE_SYSTEM, buildReducePrompt(reduceInput)),
|
|
5125
|
+
mode: "chunked",
|
|
5126
|
+
chunks: chunks.length,
|
|
5127
|
+
skipped
|
|
5128
|
+
};
|
|
5129
|
+
}
|
|
4820
5130
|
function parseSummaryXml(xml, sessionId, project, obsCount) {
|
|
4821
5131
|
const title = getXmlTag(xml, "title");
|
|
4822
5132
|
if (!title) return null;
|
|
@@ -4865,16 +5175,15 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
|
|
|
4865
5175
|
};
|
|
4866
5176
|
}
|
|
4867
5177
|
try {
|
|
4868
|
-
const
|
|
4869
|
-
const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
|
|
5178
|
+
const { response, mode, chunks } = await produceSummaryXml(provider, compressed, sessionId, session.project);
|
|
4870
5179
|
if (!response || !response.trim()) {
|
|
4871
5180
|
const latencyMs = Date.now() - startMs;
|
|
4872
5181
|
if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
|
|
4873
5182
|
logger.warn("Empty provider response on summarize", {
|
|
4874
5183
|
sessionId,
|
|
4875
5184
|
provider: provider.name,
|
|
4876
|
-
|
|
4877
|
-
|
|
5185
|
+
mode,
|
|
5186
|
+
chunks,
|
|
4878
5187
|
observationCount: compressed.length
|
|
4879
5188
|
});
|
|
4880
5189
|
return {
|
|
@@ -5426,6 +5735,7 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5426
5735
|
break;
|
|
5427
5736
|
}
|
|
5428
5737
|
}
|
|
5738
|
+
const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
|
|
5429
5739
|
const memory = {
|
|
5430
5740
|
id: generateId("mem"),
|
|
5431
5741
|
createdAt: now,
|
|
@@ -5441,7 +5751,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5441
5751
|
parentId: supersededId,
|
|
5442
5752
|
supersedes: supersededId ? [supersededId] : [],
|
|
5443
5753
|
sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
|
|
5444
|
-
isLatest: true
|
|
5754
|
+
isLatest: true,
|
|
5755
|
+
...callAgentId ? { agentId: callAgentId } : {}
|
|
5445
5756
|
};
|
|
5446
5757
|
if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
|
|
5447
5758
|
if (supersededMemory) {
|
|
@@ -5487,6 +5798,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5487
5798
|
await kv.delete(KV.memories, data.memoryId);
|
|
5488
5799
|
if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
5489
5800
|
await deleteAccessLog(kv, data.memoryId);
|
|
5801
|
+
getSearchIndex().remove(data.memoryId);
|
|
5802
|
+
vectorIndexRemove(data.memoryId);
|
|
5490
5803
|
deletedMemoryIds.push(data.memoryId);
|
|
5491
5804
|
deleted++;
|
|
5492
5805
|
}
|
|
@@ -5495,6 +5808,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5495
5808
|
await kv.delete(KV.observations(data.sessionId), obsId);
|
|
5496
5809
|
if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5497
5810
|
if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5811
|
+
getSearchIndex().remove(obsId);
|
|
5812
|
+
vectorIndexRemove(obsId);
|
|
5498
5813
|
deletedObservationIds.push(obsId);
|
|
5499
5814
|
deleted++;
|
|
5500
5815
|
}
|
|
@@ -5504,6 +5819,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5504
5819
|
await kv.delete(KV.observations(data.sessionId), obs.id);
|
|
5505
5820
|
if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5506
5821
|
if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5822
|
+
getSearchIndex().remove(obs.id);
|
|
5823
|
+
vectorIndexRemove(obs.id);
|
|
5507
5824
|
deletedObservationIds.push(obs.id);
|
|
5508
5825
|
deleted++;
|
|
5509
5826
|
}
|
|
@@ -5512,14 +5829,17 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5512
5829
|
deletedSession = true;
|
|
5513
5830
|
deleted += 2;
|
|
5514
5831
|
}
|
|
5515
|
-
if (deleted > 0)
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5832
|
+
if (deleted > 0) {
|
|
5833
|
+
await flushIndexSave();
|
|
5834
|
+
await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
|
|
5835
|
+
sessionId: data.sessionId,
|
|
5836
|
+
deleted,
|
|
5837
|
+
memoriesDeleted: deletedMemoryIds.length,
|
|
5838
|
+
observationsDeleted: deletedObservationIds.length,
|
|
5839
|
+
sessionDeleted: deletedSession,
|
|
5840
|
+
reason: "user-initiated forget"
|
|
5841
|
+
});
|
|
5842
|
+
}
|
|
5523
5843
|
logger.info("Memory forgotten", { deleted });
|
|
5524
5844
|
return {
|
|
5525
5845
|
success: true,
|
|
@@ -6031,8 +6351,12 @@ async function findByKeyword(kv, keyword, project) {
|
|
|
6031
6351
|
|
|
6032
6352
|
//#endregion
|
|
6033
6353
|
//#region src/functions/smart-search.ts
|
|
6354
|
+
const LESSON_CONTENT_PREVIEW_CHARS = 240;
|
|
6034
6355
|
function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
6035
6356
|
sdk.registerFunction("mem::smart-search", async (data) => {
|
|
6357
|
+
const isolated = isAgentScopeIsolated();
|
|
6358
|
+
const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
|
|
6359
|
+
const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
|
|
6036
6360
|
if (data.expandIds && data.expandIds.length > 0) {
|
|
6037
6361
|
const raw = data.expandIds.slice(0, 20);
|
|
6038
6362
|
const items = raw.map((entry) => {
|
|
@@ -6053,17 +6377,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
6053
6377
|
observation: obs
|
|
6054
6378
|
} : null)));
|
|
6055
6379
|
for (const r of results) if (r) expanded.push(r);
|
|
6056
|
-
|
|
6380
|
+
const scoped = filterAgentId ? expanded.filter((e) => e.observation.agentId === filterAgentId) : expanded;
|
|
6381
|
+
recordAccessBatch(kv, scoped.map((e) => e.observation.id));
|
|
6057
6382
|
const truncated = data.expandIds.length > raw.length;
|
|
6058
6383
|
logger.info("Smart search expanded", {
|
|
6059
6384
|
requested: data.expandIds.length,
|
|
6060
6385
|
attempted: raw.length,
|
|
6061
|
-
returned:
|
|
6386
|
+
returned: scoped.length,
|
|
6387
|
+
filteredOutOfScope: expanded.length - scoped.length,
|
|
6062
6388
|
truncated
|
|
6063
6389
|
});
|
|
6064
6390
|
return {
|
|
6065
6391
|
mode: "expanded",
|
|
6066
|
-
results:
|
|
6392
|
+
results: scoped,
|
|
6067
6393
|
truncated
|
|
6068
6394
|
};
|
|
6069
6395
|
}
|
|
@@ -6073,7 +6399,11 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
6073
6399
|
error: "query is required"
|
|
6074
6400
|
};
|
|
6075
6401
|
const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
|
|
6076
|
-
const
|
|
6402
|
+
const lessonLimit = Math.min(limit, 10);
|
|
6403
|
+
const includeLessons = data.includeLessons !== false;
|
|
6404
|
+
const overFetchLimit = filterAgentId ? Math.min(limit * 3, 300) : limit;
|
|
6405
|
+
const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
|
|
6406
|
+
const compact = (filterAgentId ? hybridResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : hybridResults.slice(0, limit)).map((r) => ({
|
|
6077
6407
|
obsId: r.observation.id,
|
|
6078
6408
|
sessionId: r.sessionId,
|
|
6079
6409
|
title: r.observation.title,
|
|
@@ -6084,14 +6414,42 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
6084
6414
|
recordAccessBatch(kv, compact.map((r) => r.obsId));
|
|
6085
6415
|
logger.info("Smart search compact", {
|
|
6086
6416
|
query: data.query,
|
|
6087
|
-
results: compact.length
|
|
6417
|
+
results: compact.length,
|
|
6418
|
+
lessons: lessons.length
|
|
6088
6419
|
});
|
|
6089
|
-
|
|
6420
|
+
const response = {
|
|
6090
6421
|
mode: "compact",
|
|
6091
6422
|
results: compact
|
|
6092
6423
|
};
|
|
6424
|
+
if (includeLessons) response.lessons = lessons;
|
|
6425
|
+
return response;
|
|
6093
6426
|
});
|
|
6094
6427
|
}
|
|
6428
|
+
async function recallLessons(sdk, query, limit, project) {
|
|
6429
|
+
try {
|
|
6430
|
+
const result = await sdk.trigger({
|
|
6431
|
+
function_id: "mem::lesson-recall",
|
|
6432
|
+
payload: {
|
|
6433
|
+
query,
|
|
6434
|
+
limit,
|
|
6435
|
+
project
|
|
6436
|
+
}
|
|
6437
|
+
});
|
|
6438
|
+
if (!result?.success || !Array.isArray(result.lessons)) return [];
|
|
6439
|
+
return result.lessons.map((l) => ({
|
|
6440
|
+
lessonId: l.id,
|
|
6441
|
+
content: l.content.length > LESSON_CONTENT_PREVIEW_CHARS ? l.content.slice(0, LESSON_CONTENT_PREVIEW_CHARS) + "…" : l.content,
|
|
6442
|
+
confidence: l.confidence,
|
|
6443
|
+
score: l.score ?? l.confidence,
|
|
6444
|
+
createdAt: l.createdAt,
|
|
6445
|
+
project: l.project,
|
|
6446
|
+
tags: l.tags ?? []
|
|
6447
|
+
}));
|
|
6448
|
+
} catch (err) {
|
|
6449
|
+
logger.warn("Smart search: mem::lesson-recall failed; returning empty lesson list", { error: err instanceof Error ? err.message : String(err) });
|
|
6450
|
+
return [];
|
|
6451
|
+
}
|
|
6452
|
+
}
|
|
6095
6453
|
async function findObservation$1(kv, obsId, sessionIdHint) {
|
|
6096
6454
|
if (sessionIdHint) {
|
|
6097
6455
|
const obs = await kv.get(KV.observations(sessionIdHint), obsId).catch(() => null);
|
|
@@ -6224,6 +6582,8 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
6224
6582
|
timestamp: mem.forgetAfter
|
|
6225
6583
|
});
|
|
6226
6584
|
await deleteAccessLog(kv, mem.id);
|
|
6585
|
+
getSearchIndex().remove(mem.id);
|
|
6586
|
+
vectorIndexRemove(mem.id);
|
|
6227
6587
|
}
|
|
6228
6588
|
}
|
|
6229
6589
|
}
|
|
@@ -6301,10 +6661,13 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
6301
6661
|
sessionId: sessions[i].id,
|
|
6302
6662
|
timestamp: obs.timestamp
|
|
6303
6663
|
});
|
|
6664
|
+
getSearchIndex().remove(obs.id);
|
|
6665
|
+
vectorIndexRemove(obs.id);
|
|
6304
6666
|
}
|
|
6305
6667
|
}
|
|
6306
6668
|
}
|
|
6307
6669
|
}
|
|
6670
|
+
if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
|
|
6308
6671
|
logger.info("Auto-forget complete", {
|
|
6309
6672
|
ttlExpired: result.ttlExpired.length,
|
|
6310
6673
|
contradictions: result.contradictions.length,
|
|
@@ -6317,7 +6680,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
6317
6680
|
|
|
6318
6681
|
//#endregion
|
|
6319
6682
|
//#region src/version.ts
|
|
6320
|
-
const VERSION = "0.9.
|
|
6683
|
+
const VERSION = "0.9.22";
|
|
6321
6684
|
|
|
6322
6685
|
//#endregion
|
|
6323
6686
|
//#region src/functions/export-import.ts
|
|
@@ -6455,7 +6818,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
6455
6818
|
"0.9.17",
|
|
6456
6819
|
"0.9.18",
|
|
6457
6820
|
"0.9.19",
|
|
6458
|
-
"0.9.20"
|
|
6821
|
+
"0.9.20",
|
|
6822
|
+
"0.9.21",
|
|
6823
|
+
"0.9.22"
|
|
6459
6824
|
]).has(importData.version)) return {
|
|
6460
6825
|
success: false,
|
|
6461
6826
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -6981,12 +7346,12 @@ function parseGraphXml(xml, observationIds) {
|
|
|
6981
7346
|
const nodes = [];
|
|
6982
7347
|
const edges = [];
|
|
6983
7348
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6984
|
-
const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]
|
|
7349
|
+
const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*?(?:\/>|>([\s\S]*?)<\/entity>)/g;
|
|
6985
7350
|
let match;
|
|
6986
7351
|
while ((match = entityRegex.exec(xml)) !== null) {
|
|
6987
7352
|
const type = match[1];
|
|
6988
7353
|
const name = match[2];
|
|
6989
|
-
const propsBlock = match[3];
|
|
7354
|
+
const propsBlock = match[3] ?? "";
|
|
6990
7355
|
const properties = {};
|
|
6991
7356
|
const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
|
|
6992
7357
|
let propMatch;
|
|
@@ -7514,8 +7879,11 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7514
7879
|
for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
|
|
7515
7880
|
await kv.delete(KV.memories, id);
|
|
7516
7881
|
await deleteAccessLog(kv, id);
|
|
7882
|
+
getSearchIndex().remove(id);
|
|
7883
|
+
vectorIndexRemove(id);
|
|
7517
7884
|
deleted++;
|
|
7518
7885
|
}
|
|
7886
|
+
if (deleted > 0) await flushIndexSave();
|
|
7519
7887
|
await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
|
|
7520
7888
|
reason: data.reason || "manual deletion",
|
|
7521
7889
|
deleted
|
|
@@ -7568,6 +7936,8 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7568
7936
|
(await Promise.allSettled(batch.map(async (mem) => {
|
|
7569
7937
|
await kv.delete(KV.memories, mem.id);
|
|
7570
7938
|
await deleteAccessLog(kv, mem.id);
|
|
7939
|
+
getSearchIndex().remove(mem.id);
|
|
7940
|
+
vectorIndexRemove(mem.id);
|
|
7571
7941
|
}))).forEach((result, j) => {
|
|
7572
7942
|
const mem = batch[j];
|
|
7573
7943
|
if (result.status === "fulfilled") successfulIds.push(mem.id);
|
|
@@ -7583,6 +7953,7 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7583
7953
|
}
|
|
7584
7954
|
});
|
|
7585
7955
|
}
|
|
7956
|
+
if (successfulIds.length > 0) await flushIndexSave();
|
|
7586
7957
|
await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
|
|
7587
7958
|
filter: data,
|
|
7588
7959
|
deleted: successfulIds.length,
|
|
@@ -10157,6 +10528,12 @@ const ALL_CATEGORIES = [
|
|
|
10157
10528
|
"signals",
|
|
10158
10529
|
"sessions",
|
|
10159
10530
|
"memories",
|
|
10531
|
+
"lessons",
|
|
10532
|
+
"summaries",
|
|
10533
|
+
"semantic",
|
|
10534
|
+
"procedural",
|
|
10535
|
+
"crystals",
|
|
10536
|
+
"insights",
|
|
10160
10537
|
"mesh"
|
|
10161
10538
|
];
|
|
10162
10539
|
const TWENTY_FOUR_HOURS_MS = 1440 * 60 * 1e3;
|
|
@@ -10389,6 +10766,133 @@ function registerDiagnosticsFunction(sdk, kv) {
|
|
|
10389
10766
|
fixable: false
|
|
10390
10767
|
});
|
|
10391
10768
|
}
|
|
10769
|
+
if (categories.includes("lessons")) {
|
|
10770
|
+
const lessons = await kv.list(KV.lessons);
|
|
10771
|
+
const live = lessons.filter((l) => !l.deleted);
|
|
10772
|
+
let lessonIssues = 0;
|
|
10773
|
+
for (const l of live) if (!Number.isFinite(l.confidence) || l.confidence < 0 || l.confidence > 1) {
|
|
10774
|
+
checks.push({
|
|
10775
|
+
name: `lesson-bad-confidence:${l.id}`,
|
|
10776
|
+
category: "lessons",
|
|
10777
|
+
status: "warn",
|
|
10778
|
+
message: `Lesson ${l.id} has confidence ${l.confidence} (expected finite number in 0..1)`,
|
|
10779
|
+
fixable: false
|
|
10780
|
+
});
|
|
10781
|
+
lessonIssues++;
|
|
10782
|
+
}
|
|
10783
|
+
if (lessonIssues === 0) checks.push({
|
|
10784
|
+
name: "lessons-ok",
|
|
10785
|
+
category: "lessons",
|
|
10786
|
+
status: "pass",
|
|
10787
|
+
message: `All ${live.length} lessons are healthy (${lessons.length - live.length} tombstoned)`,
|
|
10788
|
+
fixable: false
|
|
10789
|
+
});
|
|
10790
|
+
}
|
|
10791
|
+
if (categories.includes("summaries")) {
|
|
10792
|
+
const summaries = await kv.list(KV.summaries);
|
|
10793
|
+
let summaryIssues = 0;
|
|
10794
|
+
for (const s of summaries) if (typeof s.title !== "string" || s.title.trim().length === 0) {
|
|
10795
|
+
checks.push({
|
|
10796
|
+
name: `summary-missing-title:${s.sessionId}`,
|
|
10797
|
+
category: "summaries",
|
|
10798
|
+
status: "warn",
|
|
10799
|
+
message: `Summary for session ${s.sessionId} has no title`,
|
|
10800
|
+
fixable: false
|
|
10801
|
+
});
|
|
10802
|
+
summaryIssues++;
|
|
10803
|
+
}
|
|
10804
|
+
if (summaryIssues === 0) checks.push({
|
|
10805
|
+
name: "summaries-ok",
|
|
10806
|
+
category: "summaries",
|
|
10807
|
+
status: "pass",
|
|
10808
|
+
message: `All ${summaries.length} session summaries are consistent`,
|
|
10809
|
+
fixable: false
|
|
10810
|
+
});
|
|
10811
|
+
}
|
|
10812
|
+
if (categories.includes("semantic")) {
|
|
10813
|
+
const semantic = await kv.list(KV.semantic);
|
|
10814
|
+
let semanticIssues = 0;
|
|
10815
|
+
for (const s of semantic) if (!Number.isFinite(s.confidence) || s.confidence < 0 || s.confidence > 1) {
|
|
10816
|
+
checks.push({
|
|
10817
|
+
name: `semantic-bad-confidence:${s.id}`,
|
|
10818
|
+
category: "semantic",
|
|
10819
|
+
status: "warn",
|
|
10820
|
+
message: `Semantic fact ${s.id} has confidence ${s.confidence} (expected finite number in 0..1)`,
|
|
10821
|
+
fixable: false
|
|
10822
|
+
});
|
|
10823
|
+
semanticIssues++;
|
|
10824
|
+
}
|
|
10825
|
+
if (semanticIssues === 0) checks.push({
|
|
10826
|
+
name: "semantic-ok",
|
|
10827
|
+
category: "semantic",
|
|
10828
|
+
status: "pass",
|
|
10829
|
+
message: `All ${semantic.length} semantic memories are consistent`,
|
|
10830
|
+
fixable: false
|
|
10831
|
+
});
|
|
10832
|
+
}
|
|
10833
|
+
if (categories.includes("procedural")) {
|
|
10834
|
+
const procedural = await kv.list(KV.procedural);
|
|
10835
|
+
let proceduralIssues = 0;
|
|
10836
|
+
for (const p of procedural) if (!Array.isArray(p.steps) || p.steps.length === 0) {
|
|
10837
|
+
checks.push({
|
|
10838
|
+
name: `procedural-empty-steps:${p.id}`,
|
|
10839
|
+
category: "procedural",
|
|
10840
|
+
status: "warn",
|
|
10841
|
+
message: `Procedural memory "${p.name}" (${p.id}) has no steps`,
|
|
10842
|
+
fixable: false
|
|
10843
|
+
});
|
|
10844
|
+
proceduralIssues++;
|
|
10845
|
+
}
|
|
10846
|
+
if (proceduralIssues === 0) checks.push({
|
|
10847
|
+
name: "procedural-ok",
|
|
10848
|
+
category: "procedural",
|
|
10849
|
+
status: "pass",
|
|
10850
|
+
message: `All ${procedural.length} procedural memories are consistent`,
|
|
10851
|
+
fixable: false
|
|
10852
|
+
});
|
|
10853
|
+
}
|
|
10854
|
+
if (categories.includes("crystals")) {
|
|
10855
|
+
const crystals = await kv.list(KV.crystals);
|
|
10856
|
+
let crystalIssues = 0;
|
|
10857
|
+
for (const c of crystals) if (typeof c.narrative !== "string" || c.narrative.trim().length === 0) {
|
|
10858
|
+
checks.push({
|
|
10859
|
+
name: `crystal-empty-narrative:${c.id}`,
|
|
10860
|
+
category: "crystals",
|
|
10861
|
+
status: "warn",
|
|
10862
|
+
message: `Crystal ${c.id} has empty narrative`,
|
|
10863
|
+
fixable: false
|
|
10864
|
+
});
|
|
10865
|
+
crystalIssues++;
|
|
10866
|
+
}
|
|
10867
|
+
if (crystalIssues === 0) checks.push({
|
|
10868
|
+
name: "crystals-ok",
|
|
10869
|
+
category: "crystals",
|
|
10870
|
+
status: "pass",
|
|
10871
|
+
message: `All ${crystals.length} crystals are consistent`,
|
|
10872
|
+
fixable: false
|
|
10873
|
+
});
|
|
10874
|
+
}
|
|
10875
|
+
if (categories.includes("insights")) {
|
|
10876
|
+
const insights = await kv.list(KV.insights);
|
|
10877
|
+
let insightIssues = 0;
|
|
10878
|
+
for (const i of insights) if (!Number.isFinite(i.confidence) || i.confidence < 0 || i.confidence > 1) {
|
|
10879
|
+
checks.push({
|
|
10880
|
+
name: `insight-bad-confidence:${i.id}`,
|
|
10881
|
+
category: "insights",
|
|
10882
|
+
status: "warn",
|
|
10883
|
+
message: `Insight ${i.id} has confidence ${i.confidence} (expected finite number in 0..1)`,
|
|
10884
|
+
fixable: false
|
|
10885
|
+
});
|
|
10886
|
+
insightIssues++;
|
|
10887
|
+
}
|
|
10888
|
+
if (insightIssues === 0) checks.push({
|
|
10889
|
+
name: "insights-ok",
|
|
10890
|
+
category: "insights",
|
|
10891
|
+
status: "pass",
|
|
10892
|
+
message: `All ${insights.length} insights are consistent`,
|
|
10893
|
+
fixable: false
|
|
10894
|
+
});
|
|
10895
|
+
}
|
|
10392
10896
|
if (categories.includes("mesh")) {
|
|
10393
10897
|
const peers = await kv.list(KV.mesh);
|
|
10394
10898
|
let meshIssues = 0;
|
|
@@ -12849,6 +13353,8 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
12849
13353
|
await kv.delete(scope, candidate.memoryId);
|
|
12850
13354
|
await kv.delete(KV.retentionScores, candidate.memoryId);
|
|
12851
13355
|
await deleteAccessLog(kv, candidate.memoryId);
|
|
13356
|
+
getSearchIndex().remove(candidate.memoryId);
|
|
13357
|
+
vectorIndexRemove(candidate.memoryId);
|
|
12852
13358
|
evicted++;
|
|
12853
13359
|
evictedIds.push(candidate.memoryId);
|
|
12854
13360
|
if (resolvedSource === "semantic") evictedSemantic++;
|
|
@@ -12856,13 +13362,16 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
12856
13362
|
} catch {
|
|
12857
13363
|
continue;
|
|
12858
13364
|
}
|
|
12859
|
-
if (evicted > 0)
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12865
|
-
|
|
13365
|
+
if (evicted > 0) {
|
|
13366
|
+
await flushIndexSave();
|
|
13367
|
+
await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
|
|
13368
|
+
threshold,
|
|
13369
|
+
evicted,
|
|
13370
|
+
evictedEpisodic,
|
|
13371
|
+
evictedSemantic,
|
|
13372
|
+
reason: "retention score below threshold"
|
|
13373
|
+
});
|
|
13374
|
+
}
|
|
12866
13375
|
logger.info("Retention-based eviction complete", {
|
|
12867
13376
|
evicted,
|
|
12868
13377
|
evictedEpisodic,
|
|
@@ -13767,31 +14276,246 @@ function renderViewerDocument() {
|
|
|
13767
14276
|
}
|
|
13768
14277
|
|
|
13769
14278
|
//#endregion
|
|
13770
|
-
//#region src/
|
|
13771
|
-
function
|
|
13772
|
-
|
|
13773
|
-
const
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
const
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
body: { error: "unauthorized" }
|
|
13782
|
-
};
|
|
14279
|
+
//#region src/viewer/server.ts
|
|
14280
|
+
function loadViewerFavicon() {
|
|
14281
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
14282
|
+
const candidates = [
|
|
14283
|
+
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
14284
|
+
join(base, "..", "viewer", "favicon.svg"),
|
|
14285
|
+
join(base, "viewer", "favicon.svg")
|
|
14286
|
+
];
|
|
14287
|
+
for (const path of candidates) try {
|
|
14288
|
+
return readFileSync(path);
|
|
14289
|
+
} catch {}
|
|
13783
14290
|
return null;
|
|
13784
14291
|
}
|
|
13785
|
-
|
|
13786
|
-
|
|
14292
|
+
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());
|
|
14293
|
+
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
14294
|
+
function buildAllowedHosts(origins, listenPort) {
|
|
14295
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
14296
|
+
for (const o of origins) try {
|
|
14297
|
+
const parsed = new URL(o);
|
|
14298
|
+
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
14299
|
+
} catch {}
|
|
14300
|
+
hosts.add(`localhost:${listenPort}`);
|
|
14301
|
+
hosts.add(`127.0.0.1:${listenPort}`);
|
|
14302
|
+
hosts.add(`[::1]:${listenPort}`);
|
|
14303
|
+
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
14304
|
+
return hosts;
|
|
14305
|
+
}
|
|
14306
|
+
function isHostAllowed(headerHost, allowed) {
|
|
14307
|
+
if (typeof headerHost !== "string") return false;
|
|
14308
|
+
const lower = headerHost.toLowerCase().trim();
|
|
14309
|
+
if (!lower) return false;
|
|
14310
|
+
return allowed.has(lower);
|
|
14311
|
+
}
|
|
14312
|
+
function corsHeaders(req) {
|
|
14313
|
+
const origin = req.headers.origin || "";
|
|
14314
|
+
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
13787
14315
|
return {
|
|
13788
|
-
|
|
13789
|
-
|
|
14316
|
+
"Access-Control-Allow-Origin": allowed,
|
|
14317
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
14318
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
14319
|
+
Vary: "Origin"
|
|
13790
14320
|
};
|
|
13791
14321
|
}
|
|
13792
|
-
function
|
|
13793
|
-
|
|
13794
|
-
|
|
14322
|
+
function json(res, status, data, req) {
|
|
14323
|
+
const body = JSON.stringify(data);
|
|
14324
|
+
const cors = req ? corsHeaders(req) : {
|
|
14325
|
+
"Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
|
|
14326
|
+
Vary: "Origin"
|
|
14327
|
+
};
|
|
14328
|
+
res.writeHead(status, {
|
|
14329
|
+
...cors,
|
|
14330
|
+
"Content-Type": "application/json"
|
|
14331
|
+
});
|
|
14332
|
+
res.end(body);
|
|
14333
|
+
}
|
|
14334
|
+
function readBody(req) {
|
|
14335
|
+
return new Promise((resolve, reject) => {
|
|
14336
|
+
let data = "";
|
|
14337
|
+
let size = 0;
|
|
14338
|
+
req.on("data", (chunk) => {
|
|
14339
|
+
size += chunk.length;
|
|
14340
|
+
if (size > 1e6) {
|
|
14341
|
+
req.destroy();
|
|
14342
|
+
reject(/* @__PURE__ */ new Error("too large"));
|
|
14343
|
+
return;
|
|
14344
|
+
}
|
|
14345
|
+
data += chunk.toString();
|
|
14346
|
+
});
|
|
14347
|
+
req.on("end", () => resolve(data));
|
|
14348
|
+
req.on("error", reject);
|
|
14349
|
+
});
|
|
14350
|
+
}
|
|
14351
|
+
const MAX_VIEWER_PORT_RETRIES = 10;
|
|
14352
|
+
let boundViewerPort = null;
|
|
14353
|
+
let viewerSkipped = false;
|
|
14354
|
+
function getBoundViewerPort() {
|
|
14355
|
+
return boundViewerPort;
|
|
14356
|
+
}
|
|
14357
|
+
function getViewerSkipped() {
|
|
14358
|
+
return viewerSkipped;
|
|
14359
|
+
}
|
|
14360
|
+
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
14361
|
+
boundViewerPort = null;
|
|
14362
|
+
viewerSkipped = false;
|
|
14363
|
+
const resolvedRestPort = restPort ?? port - 2;
|
|
14364
|
+
const requestedPort = port;
|
|
14365
|
+
let allowedHosts = null;
|
|
14366
|
+
const server = createServer(async (req, res) => {
|
|
14367
|
+
if (!allowedHosts) {
|
|
14368
|
+
const addr = server.address();
|
|
14369
|
+
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
14370
|
+
}
|
|
14371
|
+
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
14372
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
14373
|
+
res.end("forbidden host");
|
|
14374
|
+
return;
|
|
14375
|
+
}
|
|
14376
|
+
const raw = req.url || "/";
|
|
14377
|
+
const qIdx = raw.indexOf("?");
|
|
14378
|
+
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
14379
|
+
const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
|
|
14380
|
+
const method = req.method || "GET";
|
|
14381
|
+
if (method === "OPTIONS") {
|
|
14382
|
+
res.writeHead(204, {
|
|
14383
|
+
...corsHeaders(req),
|
|
14384
|
+
"Access-Control-Max-Age": "86400"
|
|
14385
|
+
});
|
|
14386
|
+
res.end();
|
|
14387
|
+
return;
|
|
14388
|
+
}
|
|
14389
|
+
if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
|
|
14390
|
+
const rendered = renderViewerDocument();
|
|
14391
|
+
if (rendered.found) {
|
|
14392
|
+
res.writeHead(200, {
|
|
14393
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
14394
|
+
"Content-Security-Policy": rendered.csp,
|
|
14395
|
+
"Cache-Control": "no-cache"
|
|
14396
|
+
});
|
|
14397
|
+
res.end(rendered.html);
|
|
14398
|
+
return;
|
|
14399
|
+
}
|
|
14400
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
14401
|
+
res.end("viewer not found");
|
|
14402
|
+
return;
|
|
14403
|
+
}
|
|
14404
|
+
if (method === "GET" && pathname === "/favicon.svg") {
|
|
14405
|
+
const favicon = loadViewerFavicon();
|
|
14406
|
+
if (favicon) {
|
|
14407
|
+
res.writeHead(200, {
|
|
14408
|
+
"Content-Type": "image/svg+xml",
|
|
14409
|
+
"Cache-Control": "public, max-age=3600"
|
|
14410
|
+
});
|
|
14411
|
+
res.end(favicon);
|
|
14412
|
+
return;
|
|
14413
|
+
}
|
|
14414
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
14415
|
+
res.end("favicon not found");
|
|
14416
|
+
return;
|
|
14417
|
+
}
|
|
14418
|
+
try {
|
|
14419
|
+
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
14420
|
+
} catch (err) {
|
|
14421
|
+
console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
|
|
14422
|
+
json(res, 502, { error: "upstream error" }, req);
|
|
14423
|
+
}
|
|
14424
|
+
});
|
|
14425
|
+
let attempt = 0;
|
|
14426
|
+
let currentPort = requestedPort;
|
|
14427
|
+
const tryListen = () => {
|
|
14428
|
+
server.listen(currentPort, "127.0.0.1");
|
|
14429
|
+
};
|
|
14430
|
+
server.on("listening", () => {
|
|
14431
|
+
const addr = server.address();
|
|
14432
|
+
boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
|
|
14433
|
+
viewerSkipped = false;
|
|
14434
|
+
if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
|
|
14435
|
+
else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
|
|
14436
|
+
});
|
|
14437
|
+
server.on("error", (err) => {
|
|
14438
|
+
if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
|
|
14439
|
+
attempt++;
|
|
14440
|
+
currentPort = requestedPort + attempt;
|
|
14441
|
+
setImmediate(tryListen);
|
|
14442
|
+
return;
|
|
14443
|
+
}
|
|
14444
|
+
if (err.code === "EADDRINUSE") {
|
|
14445
|
+
boundViewerPort = null;
|
|
14446
|
+
viewerSkipped = true;
|
|
14447
|
+
console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
|
|
14448
|
+
} else {
|
|
14449
|
+
boundViewerPort = null;
|
|
14450
|
+
viewerSkipped = true;
|
|
14451
|
+
console.error(`[agentmemory] Viewer error:`, err.message);
|
|
14452
|
+
}
|
|
14453
|
+
});
|
|
14454
|
+
tryListen();
|
|
14455
|
+
return server;
|
|
14456
|
+
}
|
|
14457
|
+
async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
|
|
14458
|
+
const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
|
|
14459
|
+
const headers = {};
|
|
14460
|
+
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
|
14461
|
+
const ct = req.headers["content-type"];
|
|
14462
|
+
if (ct) headers["Content-Type"] = ct;
|
|
14463
|
+
let body;
|
|
14464
|
+
if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
|
|
14465
|
+
const controller = new AbortController();
|
|
14466
|
+
const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
|
|
14467
|
+
let upstream;
|
|
14468
|
+
try {
|
|
14469
|
+
upstream = await fetch(upstreamUrl, {
|
|
14470
|
+
method,
|
|
14471
|
+
headers,
|
|
14472
|
+
body: body || void 0,
|
|
14473
|
+
signal: controller.signal
|
|
14474
|
+
});
|
|
14475
|
+
clearTimeout(fetchTimeout);
|
|
14476
|
+
} catch (err) {
|
|
14477
|
+
clearTimeout(fetchTimeout);
|
|
14478
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
14479
|
+
json(res, 504, { error: "upstream timeout" }, req);
|
|
14480
|
+
return;
|
|
14481
|
+
}
|
|
14482
|
+
throw err;
|
|
14483
|
+
}
|
|
14484
|
+
const cors = corsHeaders(req);
|
|
14485
|
+
const responseBody = await upstream.text();
|
|
14486
|
+
const responseHeaders = { ...cors };
|
|
14487
|
+
const upstreamCt = upstream.headers.get("content-type");
|
|
14488
|
+
if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
|
|
14489
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
14490
|
+
res.end(responseBody);
|
|
14491
|
+
}
|
|
14492
|
+
|
|
14493
|
+
//#endregion
|
|
14494
|
+
//#region src/triggers/api.ts
|
|
14495
|
+
function parseOptionalInt(raw) {
|
|
14496
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
14497
|
+
const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
|
|
14498
|
+
return Number.isFinite(n) ? n : void 0;
|
|
14499
|
+
}
|
|
14500
|
+
function checkAuth(req, secret) {
|
|
14501
|
+
if (!secret) return null;
|
|
14502
|
+
const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
|
|
14503
|
+
if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
|
|
14504
|
+
status_code: 401,
|
|
14505
|
+
body: { error: "unauthorized" }
|
|
14506
|
+
};
|
|
14507
|
+
return null;
|
|
14508
|
+
}
|
|
14509
|
+
function requireConfiguredSecret(secret, feature) {
|
|
14510
|
+
if (secret) return null;
|
|
14511
|
+
return {
|
|
14512
|
+
status_code: 503,
|
|
14513
|
+
body: { error: `${feature} requires AGENTMEMORY_SECRET` }
|
|
14514
|
+
};
|
|
14515
|
+
}
|
|
14516
|
+
function flagDisabledResponse(opts) {
|
|
14517
|
+
return {
|
|
14518
|
+
status_code: 503,
|
|
13795
14519
|
body: opts
|
|
13796
14520
|
};
|
|
13797
14521
|
}
|
|
@@ -13851,7 +14575,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13851
14575
|
status_code: 200,
|
|
13852
14576
|
body: {
|
|
13853
14577
|
status: "ok",
|
|
13854
|
-
service: "agentmemory"
|
|
14578
|
+
service: "agentmemory",
|
|
14579
|
+
viewerPort: getBoundViewerPort(),
|
|
14580
|
+
viewerSkipped: getViewerSkipped()
|
|
13855
14581
|
}
|
|
13856
14582
|
}));
|
|
13857
14583
|
sdk.registerTrigger({
|
|
@@ -13946,7 +14672,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13946
14672
|
version: VERSION,
|
|
13947
14673
|
health: health || null,
|
|
13948
14674
|
functionMetrics,
|
|
13949
|
-
circuitBreaker
|
|
14675
|
+
circuitBreaker,
|
|
14676
|
+
viewerPort: getBoundViewerPort(),
|
|
14677
|
+
viewerSkipped: getViewerSkipped()
|
|
13950
14678
|
}
|
|
13951
14679
|
};
|
|
13952
14680
|
});
|
|
@@ -14199,13 +14927,18 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14199
14927
|
status_code: 400,
|
|
14200
14928
|
body: { error: "sessionId, project, and cwd are required non-empty strings" }
|
|
14201
14929
|
};
|
|
14930
|
+
const title = typeof body.title === "string" ? body.title.trim() : void 0;
|
|
14931
|
+
const agentId = (typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim().slice(0, 128) : void 0) ?? getAgentId();
|
|
14202
14932
|
const session = {
|
|
14203
14933
|
id: sessionId,
|
|
14204
14934
|
project,
|
|
14205
14935
|
cwd,
|
|
14206
14936
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14207
14937
|
status: "active",
|
|
14208
|
-
observationCount: 0
|
|
14938
|
+
observationCount: 0,
|
|
14939
|
+
...title ? { summary: title.slice(0, 200) } : {},
|
|
14940
|
+
...title ? { firstPrompt: title.slice(0, 200) } : {},
|
|
14941
|
+
...agentId ? { agentId } : {}
|
|
14209
14942
|
};
|
|
14210
14943
|
await kv.set(KV.sessions, sessionId, session);
|
|
14211
14944
|
return {
|
|
@@ -14392,9 +15125,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14392
15125
|
sdk.registerFunction("api::sessions", async (req) => {
|
|
14393
15126
|
const authErr = checkAuth(req, secret);
|
|
14394
15127
|
if (authErr) return authErr;
|
|
15128
|
+
const sessions = await kv.list(KV.sessions);
|
|
15129
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
15130
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
15131
|
+
const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
14395
15132
|
return {
|
|
14396
15133
|
status_code: 200,
|
|
14397
|
-
body: { sessions:
|
|
15134
|
+
body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
|
|
14398
15135
|
};
|
|
14399
15136
|
});
|
|
14400
15137
|
sdk.registerTrigger({
|
|
@@ -14413,9 +15150,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14413
15150
|
status_code: 400,
|
|
14414
15151
|
body: { error: "sessionId required" }
|
|
14415
15152
|
};
|
|
15153
|
+
const observations = await kv.list(KV.observations(sessionId));
|
|
15154
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
15155
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
15156
|
+
const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
14416
15157
|
return {
|
|
14417
15158
|
status_code: 200,
|
|
14418
|
-
body: { observations:
|
|
15159
|
+
body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
|
|
14419
15160
|
};
|
|
14420
15161
|
});
|
|
14421
15162
|
sdk.registerTrigger({
|
|
@@ -14691,11 +15432,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14691
15432
|
sdk.registerFunction("api::export", async (req) => {
|
|
14692
15433
|
const authErr = checkAuth(req, secret);
|
|
14693
15434
|
if (authErr) return authErr;
|
|
15435
|
+
const rawMax = req.query_params?.["maxSessions"];
|
|
15436
|
+
const rawOffset = req.query_params?.["offset"];
|
|
15437
|
+
const payload = {};
|
|
15438
|
+
if (typeof rawMax === "string") {
|
|
15439
|
+
const n = Number(rawMax);
|
|
15440
|
+
if (Number.isInteger(n) && n > 0) payload.maxSessions = n;
|
|
15441
|
+
}
|
|
15442
|
+
if (typeof rawOffset === "string") {
|
|
15443
|
+
const n = Number(rawOffset);
|
|
15444
|
+
if (Number.isInteger(n) && n >= 0) payload.offset = n;
|
|
15445
|
+
}
|
|
14694
15446
|
return {
|
|
14695
15447
|
status_code: 200,
|
|
14696
15448
|
body: await sdk.trigger({
|
|
14697
15449
|
function_id: "mem::export",
|
|
14698
|
-
payload
|
|
15450
|
+
payload
|
|
14699
15451
|
})
|
|
14700
15452
|
};
|
|
14701
15453
|
});
|
|
@@ -15181,9 +15933,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
15181
15933
|
const authErr = checkAuth(req, secret);
|
|
15182
15934
|
if (authErr) return authErr;
|
|
15183
15935
|
const memories = await kv.list(KV.memories);
|
|
15936
|
+
const latest = req.query_params?.["latest"] === "true";
|
|
15937
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
15938
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
15939
|
+
const explicitAgentId = normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0;
|
|
15940
|
+
const includeOrphans = req.query_params?.["includeOrphans"] === "true";
|
|
15941
|
+
const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
15942
|
+
let filtered = latest ? memories.filter((m) => m.isLatest) : memories;
|
|
15943
|
+
if (filterAgentId) filtered = filtered.filter((m) => m.agentId === filterAgentId || includeOrphans && m.agentId === void 0);
|
|
15944
|
+
if (req.query_params?.["count"] === "true") return {
|
|
15945
|
+
status_code: 200,
|
|
15946
|
+
body: {
|
|
15947
|
+
total: filtered.length,
|
|
15948
|
+
latestCount: filtered.filter((m) => m.isLatest).length
|
|
15949
|
+
}
|
|
15950
|
+
};
|
|
15951
|
+
const rawLimit = req.query_params?.["limit"];
|
|
15952
|
+
const rawOffset = req.query_params?.["offset"];
|
|
15953
|
+
const parsedLimit = typeof rawLimit === "string" ? Number(rawLimit) : NaN;
|
|
15954
|
+
const parsedOffset = typeof rawOffset === "string" ? Number(rawOffset) : NaN;
|
|
15955
|
+
const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 5e3) : void 0;
|
|
15956
|
+
const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
|
|
15184
15957
|
return {
|
|
15185
15958
|
status_code: 200,
|
|
15186
|
-
body: {
|
|
15959
|
+
body: {
|
|
15960
|
+
memories: limit !== void 0 ? filtered.slice(offset, offset + limit) : filtered,
|
|
15961
|
+
total: filtered.length,
|
|
15962
|
+
offset,
|
|
15963
|
+
limit: limit ?? null
|
|
15964
|
+
}
|
|
15187
15965
|
};
|
|
15188
15966
|
});
|
|
15189
15967
|
sdk.registerTrigger({
|
|
@@ -18263,8 +19041,8 @@ function getAllTools() {
|
|
|
18263
19041
|
];
|
|
18264
19042
|
}
|
|
18265
19043
|
function getVisibleTools() {
|
|
18266
|
-
if ((process.env["AGENTMEMORY_TOOLS"] || "
|
|
18267
|
-
return getAllTools()
|
|
19044
|
+
if ((process.env["AGENTMEMORY_TOOLS"] || "all") === "core") return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
|
|
19045
|
+
return getAllTools();
|
|
18268
19046
|
}
|
|
18269
19047
|
|
|
18270
19048
|
//#endregion
|
|
@@ -19902,201 +20680,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
19902
20680
|
});
|
|
19903
20681
|
}
|
|
19904
20682
|
|
|
19905
|
-
//#endregion
|
|
19906
|
-
//#region src/viewer/server.ts
|
|
19907
|
-
function loadViewerFavicon() {
|
|
19908
|
-
const base = dirname(fileURLToPath(import.meta.url));
|
|
19909
|
-
const candidates = [
|
|
19910
|
-
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
19911
|
-
join(base, "..", "viewer", "favicon.svg"),
|
|
19912
|
-
join(base, "viewer", "favicon.svg")
|
|
19913
|
-
];
|
|
19914
|
-
for (const path of candidates) try {
|
|
19915
|
-
return readFileSync(path);
|
|
19916
|
-
} catch {}
|
|
19917
|
-
return null;
|
|
19918
|
-
}
|
|
19919
|
-
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());
|
|
19920
|
-
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
19921
|
-
function buildAllowedHosts(origins, listenPort) {
|
|
19922
|
-
const hosts = /* @__PURE__ */ new Set();
|
|
19923
|
-
for (const o of origins) try {
|
|
19924
|
-
const parsed = new URL(o);
|
|
19925
|
-
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
19926
|
-
} catch {}
|
|
19927
|
-
hosts.add(`localhost:${listenPort}`);
|
|
19928
|
-
hosts.add(`127.0.0.1:${listenPort}`);
|
|
19929
|
-
hosts.add(`[::1]:${listenPort}`);
|
|
19930
|
-
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
19931
|
-
return hosts;
|
|
19932
|
-
}
|
|
19933
|
-
function isHostAllowed(headerHost, allowed) {
|
|
19934
|
-
if (typeof headerHost !== "string") return false;
|
|
19935
|
-
const lower = headerHost.toLowerCase().trim();
|
|
19936
|
-
if (!lower) return false;
|
|
19937
|
-
return allowed.has(lower);
|
|
19938
|
-
}
|
|
19939
|
-
function corsHeaders(req) {
|
|
19940
|
-
const origin = req.headers.origin || "";
|
|
19941
|
-
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
19942
|
-
return {
|
|
19943
|
-
"Access-Control-Allow-Origin": allowed,
|
|
19944
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
19945
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
19946
|
-
Vary: "Origin"
|
|
19947
|
-
};
|
|
19948
|
-
}
|
|
19949
|
-
function json(res, status, data, req) {
|
|
19950
|
-
const body = JSON.stringify(data);
|
|
19951
|
-
const cors = req ? corsHeaders(req) : {
|
|
19952
|
-
"Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
|
|
19953
|
-
Vary: "Origin"
|
|
19954
|
-
};
|
|
19955
|
-
res.writeHead(status, {
|
|
19956
|
-
...cors,
|
|
19957
|
-
"Content-Type": "application/json"
|
|
19958
|
-
});
|
|
19959
|
-
res.end(body);
|
|
19960
|
-
}
|
|
19961
|
-
function readBody(req) {
|
|
19962
|
-
return new Promise((resolve, reject) => {
|
|
19963
|
-
let data = "";
|
|
19964
|
-
let size = 0;
|
|
19965
|
-
req.on("data", (chunk) => {
|
|
19966
|
-
size += chunk.length;
|
|
19967
|
-
if (size > 1e6) {
|
|
19968
|
-
req.destroy();
|
|
19969
|
-
reject(/* @__PURE__ */ new Error("too large"));
|
|
19970
|
-
return;
|
|
19971
|
-
}
|
|
19972
|
-
data += chunk.toString();
|
|
19973
|
-
});
|
|
19974
|
-
req.on("end", () => resolve(data));
|
|
19975
|
-
req.on("error", reject);
|
|
19976
|
-
});
|
|
19977
|
-
}
|
|
19978
|
-
const MAX_VIEWER_PORT_RETRIES = 10;
|
|
19979
|
-
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
19980
|
-
const resolvedRestPort = restPort ?? port - 2;
|
|
19981
|
-
const requestedPort = port;
|
|
19982
|
-
let allowedHosts = null;
|
|
19983
|
-
const server = createServer(async (req, res) => {
|
|
19984
|
-
if (!allowedHosts) {
|
|
19985
|
-
const addr = server.address();
|
|
19986
|
-
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
19987
|
-
}
|
|
19988
|
-
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
19989
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
19990
|
-
res.end("forbidden host");
|
|
19991
|
-
return;
|
|
19992
|
-
}
|
|
19993
|
-
const raw = req.url || "/";
|
|
19994
|
-
const qIdx = raw.indexOf("?");
|
|
19995
|
-
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
19996
|
-
const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
|
|
19997
|
-
const method = req.method || "GET";
|
|
19998
|
-
if (method === "OPTIONS") {
|
|
19999
|
-
res.writeHead(204, {
|
|
20000
|
-
...corsHeaders(req),
|
|
20001
|
-
"Access-Control-Max-Age": "86400"
|
|
20002
|
-
});
|
|
20003
|
-
res.end();
|
|
20004
|
-
return;
|
|
20005
|
-
}
|
|
20006
|
-
if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
|
|
20007
|
-
const rendered = renderViewerDocument();
|
|
20008
|
-
if (rendered.found) {
|
|
20009
|
-
res.writeHead(200, {
|
|
20010
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
20011
|
-
"Content-Security-Policy": rendered.csp,
|
|
20012
|
-
"Cache-Control": "no-cache"
|
|
20013
|
-
});
|
|
20014
|
-
res.end(rendered.html);
|
|
20015
|
-
return;
|
|
20016
|
-
}
|
|
20017
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
20018
|
-
res.end("viewer not found");
|
|
20019
|
-
return;
|
|
20020
|
-
}
|
|
20021
|
-
if (method === "GET" && pathname === "/favicon.svg") {
|
|
20022
|
-
const favicon = loadViewerFavicon();
|
|
20023
|
-
if (favicon) {
|
|
20024
|
-
res.writeHead(200, {
|
|
20025
|
-
"Content-Type": "image/svg+xml",
|
|
20026
|
-
"Cache-Control": "public, max-age=3600"
|
|
20027
|
-
});
|
|
20028
|
-
res.end(favicon);
|
|
20029
|
-
return;
|
|
20030
|
-
}
|
|
20031
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
20032
|
-
res.end("favicon not found");
|
|
20033
|
-
return;
|
|
20034
|
-
}
|
|
20035
|
-
try {
|
|
20036
|
-
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
20037
|
-
} catch (err) {
|
|
20038
|
-
console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
|
|
20039
|
-
json(res, 502, { error: "upstream error" }, req);
|
|
20040
|
-
}
|
|
20041
|
-
});
|
|
20042
|
-
let attempt = 0;
|
|
20043
|
-
let currentPort = requestedPort;
|
|
20044
|
-
const tryListen = () => {
|
|
20045
|
-
server.listen(currentPort, "127.0.0.1");
|
|
20046
|
-
};
|
|
20047
|
-
server.on("listening", () => {
|
|
20048
|
-
if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
|
|
20049
|
-
else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
|
|
20050
|
-
});
|
|
20051
|
-
server.on("error", (err) => {
|
|
20052
|
-
if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
|
|
20053
|
-
attempt++;
|
|
20054
|
-
currentPort = requestedPort + attempt;
|
|
20055
|
-
setImmediate(tryListen);
|
|
20056
|
-
return;
|
|
20057
|
-
}
|
|
20058
|
-
if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
|
|
20059
|
-
else console.error(`[agentmemory] Viewer error:`, err.message);
|
|
20060
|
-
});
|
|
20061
|
-
tryListen();
|
|
20062
|
-
return server;
|
|
20063
|
-
}
|
|
20064
|
-
async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
|
|
20065
|
-
const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
|
|
20066
|
-
const headers = {};
|
|
20067
|
-
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
|
20068
|
-
const ct = req.headers["content-type"];
|
|
20069
|
-
if (ct) headers["Content-Type"] = ct;
|
|
20070
|
-
let body;
|
|
20071
|
-
if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
|
|
20072
|
-
const controller = new AbortController();
|
|
20073
|
-
const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
|
|
20074
|
-
let upstream;
|
|
20075
|
-
try {
|
|
20076
|
-
upstream = await fetch(upstreamUrl, {
|
|
20077
|
-
method,
|
|
20078
|
-
headers,
|
|
20079
|
-
body: body || void 0,
|
|
20080
|
-
signal: controller.signal
|
|
20081
|
-
});
|
|
20082
|
-
clearTimeout(fetchTimeout);
|
|
20083
|
-
} catch (err) {
|
|
20084
|
-
clearTimeout(fetchTimeout);
|
|
20085
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
20086
|
-
json(res, 504, { error: "upstream timeout" }, req);
|
|
20087
|
-
return;
|
|
20088
|
-
}
|
|
20089
|
-
throw err;
|
|
20090
|
-
}
|
|
20091
|
-
const cors = corsHeaders(req);
|
|
20092
|
-
const responseBody = await upstream.text();
|
|
20093
|
-
const responseHeaders = { ...cors };
|
|
20094
|
-
const upstreamCt = upstream.headers.get("content-type");
|
|
20095
|
-
if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
|
|
20096
|
-
res.writeHead(upstream.status, responseHeaders);
|
|
20097
|
-
res.end(responseBody);
|
|
20098
|
-
}
|
|
20099
|
-
|
|
20100
20683
|
//#endregion
|
|
20101
20684
|
//#region src/eval/metrics-store.ts
|
|
20102
20685
|
var MetricsStore = class {
|
|
@@ -20234,6 +20817,21 @@ function initMetrics(getMeter) {
|
|
|
20234
20817
|
|
|
20235
20818
|
//#endregion
|
|
20236
20819
|
//#region src/index.ts
|
|
20820
|
+
function workerPidfilePath() {
|
|
20821
|
+
return join(homedir(), ".agentmemory", "worker.pid");
|
|
20822
|
+
}
|
|
20823
|
+
function writeWorkerPidfile() {
|
|
20824
|
+
try {
|
|
20825
|
+
const p = workerPidfilePath();
|
|
20826
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
20827
|
+
writeFileSync(p, `${process.pid}\n`, { encoding: "utf-8" });
|
|
20828
|
+
} catch {}
|
|
20829
|
+
}
|
|
20830
|
+
function clearWorkerPidfile() {
|
|
20831
|
+
try {
|
|
20832
|
+
unlinkSync(workerPidfilePath());
|
|
20833
|
+
} catch {}
|
|
20834
|
+
}
|
|
20237
20835
|
function hasGetMeter(sdk) {
|
|
20238
20836
|
return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
|
|
20239
20837
|
}
|
|
@@ -20274,6 +20872,7 @@ async function main() {
|
|
|
20274
20872
|
framework: "iii-sdk"
|
|
20275
20873
|
}
|
|
20276
20874
|
});
|
|
20875
|
+
writeWorkerPidfile();
|
|
20277
20876
|
const kv = new StateKV(sdk);
|
|
20278
20877
|
const secret = getEnvVar("AGENTMEMORY_SECRET");
|
|
20279
20878
|
const metricsStore = new MetricsStore(kv);
|
|
@@ -20369,6 +20968,7 @@ async function main() {
|
|
|
20369
20968
|
registerMcpEndpoints(sdk, kv, secret);
|
|
20370
20969
|
const healthMonitor = registerHealthMonitor(sdk, kv);
|
|
20371
20970
|
const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
|
|
20971
|
+
setIndexPersistence(indexPersistence);
|
|
20372
20972
|
const loaded = await indexPersistence.load().catch((err) => {
|
|
20373
20973
|
console.warn(`[agentmemory] Failed to load persisted index:`, err);
|
|
20374
20974
|
return null;
|
|
@@ -20386,23 +20986,22 @@ async function main() {
|
|
|
20386
20986
|
if (mismatches.length > 0) {
|
|
20387
20987
|
const sample = mismatches.slice(0, 5).map((m) => `${m.obsId} (dim=${m.dim})`).join(", ");
|
|
20388
20988
|
const distinct = Array.from(seenDimensions).sort((a, b) => a - b).join(", ");
|
|
20389
|
-
if (
|
|
20989
|
+
if (isDropStaleIndexEnabled()) console.warn(`[agentmemory] Persisted vector index has ${mismatches.length} of ${loaded.vector.size} vectors with the wrong dimension. Active provider (${embeddingProvider?.name}) declares ${activeDim}; dimensions seen on disk: ${distinct}. AGENTMEMORY_DROP_STALE_INDEX=true is set — discarding the persisted vectors. Live observations will rebuild the index over time.`);
|
|
20390
20990
|
else throw new Error(`[agentmemory] Refusing to start: persisted vector index has ${mismatches.length} of ${loaded.vector.size} vectors with the wrong dimension. Active provider (${embeddingProvider?.name}) declares ${activeDim}; dimensions seen on disk: ${distinct}. First mismatched obsIds: ${sample}. Loading would silently corrupt search (cross-dimension cosine returns 0). Choose one:\n - Re-embed the existing index against the new provider, then start.\n - Set AGENTMEMORY_DROP_STALE_INDEX=true to discard the persisted vectors and rebuild from live observations.\n - Switch the embedding provider back to the one that wrote the index.`);
|
|
20391
20991
|
} else {
|
|
20392
20992
|
vectorIndex.restoreFrom(loaded.vector);
|
|
20393
20993
|
bootLog(`Loaded persisted vector index (${vectorIndex.size} vectors)`);
|
|
20394
20994
|
}
|
|
20395
20995
|
}
|
|
20396
|
-
if (bm25Index.size === 0) {
|
|
20397
|
-
const indexCount = await rebuildIndex(kv).catch((err) => {
|
|
20398
|
-
console.warn(`[agentmemory] Failed to rebuild search index:`, err);
|
|
20399
|
-
return 0;
|
|
20400
|
-
});
|
|
20996
|
+
if (bm25Index.size === 0) rebuildIndex(kv).then((indexCount) => {
|
|
20401
20997
|
if (indexCount > 0) {
|
|
20402
20998
|
bootLog(`Search index rebuilt: ${indexCount} entries`);
|
|
20403
20999
|
indexPersistence.scheduleSave();
|
|
20404
21000
|
}
|
|
20405
|
-
}
|
|
21001
|
+
}).catch((err) => {
|
|
21002
|
+
console.warn(`[agentmemory] Failed to rebuild search index:`, err);
|
|
21003
|
+
});
|
|
21004
|
+
else try {
|
|
20406
21005
|
const memories = await kv.list(KV.memories);
|
|
20407
21006
|
let backfilled = 0;
|
|
20408
21007
|
for (const memory of memories) {
|
|
@@ -20487,6 +21086,7 @@ async function main() {
|
|
|
20487
21086
|
console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
|
|
20488
21087
|
});
|
|
20489
21088
|
await sdk.shutdown();
|
|
21089
|
+
clearWorkerPidfile();
|
|
20490
21090
|
process.exit(0);
|
|
20491
21091
|
};
|
|
20492
21092
|
process.on("SIGINT", shutdown);
|