@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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./schema-BkALl7Z_.mjs";
|
|
2
|
+
import { n as logger, t as bootLog } from "./logger-xlVlvCWX.mjs";
|
|
3
|
+
import { t as VERSION } from "./version-DvQMNbEH.mjs";
|
|
4
|
+
import { a as isManagedImagePath, getImageRefCount, i as getMaxBytes, n as IMAGES_DIR, r as deleteImage, t as withKeyedLock } from "./image-refs-CJS5B9Gq.mjs";
|
|
5
|
+
import { _ as loadEmbeddingConfig, a as getAgentId, b as loadTeamConfig, d as isConsolidationEnabled, f as isContextInjectionEnabled, g as loadConfig, h as loadClaudeBridgeConfig, i as detectLlmProviderKind, l as isAgentScopeIsolated, m as isGraphExtractionEnabled, n as getVisibleTools, o as getConsolidationDecayDays, p as isDropStaleIndexEnabled, r as detectEmbeddingProvider, s as getEnvVar, t as getAllTools, u as isAutoCompressEnabled, v as loadFallbackConfig, y as loadSnapshotConfig } from "./tools-registry-B7Y6nJsr.mjs";
|
|
4
6
|
import { createRequire } from "node:module";
|
|
5
7
|
import { execFile } from "node:child_process";
|
|
6
|
-
import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
9
|
import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
9
11
|
import { homedir } from "node:os";
|
|
@@ -229,13 +231,25 @@ function v1AzureUrl(baseUrl, path) {
|
|
|
229
231
|
url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
|
|
230
232
|
return url.toString();
|
|
231
233
|
}
|
|
234
|
+
function appendOpenAIRoute(baseUrl, route) {
|
|
235
|
+
const trimmedBase = baseUrl.replace(/\/+$/, "");
|
|
236
|
+
const cleanRoute = route.startsWith("/") ? route : `/${route}`;
|
|
237
|
+
let pathname;
|
|
238
|
+
try {
|
|
239
|
+
pathname = new URL(trimmedBase).pathname.replace(/\/+$/, "");
|
|
240
|
+
} catch {
|
|
241
|
+
return `${trimmedBase}/v1${cleanRoute}`;
|
|
242
|
+
}
|
|
243
|
+
if (pathname === "" || pathname === "/") return `${trimmedBase}/v1${cleanRoute}`;
|
|
244
|
+
return `${trimmedBase}${cleanRoute}`;
|
|
245
|
+
}
|
|
232
246
|
function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
|
|
233
247
|
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
|
|
234
|
-
return
|
|
248
|
+
return appendOpenAIRoute(baseUrl, "/chat/completions");
|
|
235
249
|
}
|
|
236
250
|
function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
|
|
237
251
|
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
|
|
238
|
-
return
|
|
252
|
+
return appendOpenAIRoute(baseUrl, "/embeddings");
|
|
239
253
|
}
|
|
240
254
|
function buildAuthHeaders(apiKey, isAzure) {
|
|
241
255
|
if (isAzure) return {
|
|
@@ -317,6 +331,7 @@ var OpenAIProvider = class {
|
|
|
317
331
|
const body = {
|
|
318
332
|
model: this.model,
|
|
319
333
|
max_tokens: this.maxTokens,
|
|
334
|
+
stream: false,
|
|
320
335
|
messages: [{
|
|
321
336
|
role: "system",
|
|
322
337
|
content: systemPrompt
|
|
@@ -345,7 +360,7 @@ var OpenAIProvider = class {
|
|
|
345
360
|
const message = data.choices?.[0]?.message;
|
|
346
361
|
const content = message?.content;
|
|
347
362
|
if (content) return content;
|
|
348
|
-
const reasoning = message?.reasoning;
|
|
363
|
+
const reasoning = message?.reasoning ?? message?.reasoning_content;
|
|
349
364
|
if (reasoning) return reasoning;
|
|
350
365
|
throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
351
366
|
}
|
|
@@ -2182,6 +2197,24 @@ var SearchIndex = class SearchIndex {
|
|
|
2182
2197
|
has(id) {
|
|
2183
2198
|
return this.entries.has(id);
|
|
2184
2199
|
}
|
|
2200
|
+
remove(id) {
|
|
2201
|
+
const entry = this.entries.get(id);
|
|
2202
|
+
if (!entry) return;
|
|
2203
|
+
const termFreq = this.docTermCounts.get(id);
|
|
2204
|
+
if (termFreq) {
|
|
2205
|
+
for (const term of termFreq.keys()) {
|
|
2206
|
+
const postingList = this.invertedIndex.get(term);
|
|
2207
|
+
if (postingList) {
|
|
2208
|
+
postingList.delete(id);
|
|
2209
|
+
if (postingList.size === 0) this.invertedIndex.delete(term);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
this.docTermCounts.delete(id);
|
|
2213
|
+
}
|
|
2214
|
+
this.totalDocLength = Math.max(0, this.totalDocLength - entry.termCount);
|
|
2215
|
+
this.entries.delete(id);
|
|
2216
|
+
this.sortedTerms = null;
|
|
2217
|
+
}
|
|
2185
2218
|
search(query, limit = 20) {
|
|
2186
2219
|
const rawTerms = this.tokenize(query.toLowerCase());
|
|
2187
2220
|
if (rawTerms.length === 0) return [];
|
|
@@ -2521,6 +2554,7 @@ function buildSyntheticCompression(raw) {
|
|
|
2521
2554
|
};
|
|
2522
2555
|
if (raw.modality) result.modality = raw.modality;
|
|
2523
2556
|
if (raw.imageData) result.imageData = raw.imageData;
|
|
2557
|
+
if (raw.agentId) result.agentId = raw.agentId;
|
|
2524
2558
|
return result;
|
|
2525
2559
|
}
|
|
2526
2560
|
|
|
@@ -2610,6 +2644,16 @@ function setVectorIndex(idx) {
|
|
|
2610
2644
|
function setEmbeddingProvider(provider) {
|
|
2611
2645
|
currentEmbeddingProvider = provider;
|
|
2612
2646
|
}
|
|
2647
|
+
function vectorIndexRemove(id) {
|
|
2648
|
+
vectorIndex?.remove(id);
|
|
2649
|
+
}
|
|
2650
|
+
let indexPersistence = null;
|
|
2651
|
+
function setIndexPersistence(p) {
|
|
2652
|
+
indexPersistence = p;
|
|
2653
|
+
}
|
|
2654
|
+
async function flushIndexSave() {
|
|
2655
|
+
await indexPersistence?.save();
|
|
2656
|
+
}
|
|
2613
2657
|
const EMBED_MAX_CHARS = 16e3;
|
|
2614
2658
|
function clipEmbedInput(text) {
|
|
2615
2659
|
if (text.length <= EMBED_MAX_CHARS) return text;
|
|
@@ -2643,20 +2687,108 @@ async function vectorIndexAddGuarded(id, sessionId, text, context) {
|
|
|
2643
2687
|
return false;
|
|
2644
2688
|
}
|
|
2645
2689
|
}
|
|
2690
|
+
async function vectorIndexAddBatchGuarded(items) {
|
|
2691
|
+
const vi = vectorIndex;
|
|
2692
|
+
const ep = currentEmbeddingProvider;
|
|
2693
|
+
if (!vi || !ep || items.length === 0) return {
|
|
2694
|
+
ok: 0,
|
|
2695
|
+
fail: 0
|
|
2696
|
+
};
|
|
2697
|
+
let embeddings;
|
|
2698
|
+
try {
|
|
2699
|
+
embeddings = await ep.embedBatch(items.map((i) => clipEmbedInput(i.text)));
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
logger.warn("vector-index add batch: embed failed — skipping batch", {
|
|
2702
|
+
batchSize: items.length,
|
|
2703
|
+
provider: ep.name,
|
|
2704
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2705
|
+
});
|
|
2706
|
+
return {
|
|
2707
|
+
ok: 0,
|
|
2708
|
+
fail: items.length
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
if (embeddings.length !== items.length) {
|
|
2712
|
+
logger.warn("vector-index add batch: provider returned wrong length — skipping batch", {
|
|
2713
|
+
batchSize: items.length,
|
|
2714
|
+
returned: embeddings.length,
|
|
2715
|
+
provider: ep.name
|
|
2716
|
+
});
|
|
2717
|
+
return {
|
|
2718
|
+
ok: 0,
|
|
2719
|
+
fail: items.length
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
let ok = 0;
|
|
2723
|
+
let fail = 0;
|
|
2724
|
+
for (let i = 0; i < items.length; i++) {
|
|
2725
|
+
const item = items[i];
|
|
2726
|
+
const embedding = embeddings[i];
|
|
2727
|
+
if (embedding.length !== ep.dimensions) {
|
|
2728
|
+
logger.warn("vector-index add batch: dimension mismatch — skipping item", {
|
|
2729
|
+
kind: item.context.kind,
|
|
2730
|
+
id: item.context.logId,
|
|
2731
|
+
provider: ep.name,
|
|
2732
|
+
expected: ep.dimensions,
|
|
2733
|
+
received: embedding.length
|
|
2734
|
+
});
|
|
2735
|
+
fail++;
|
|
2736
|
+
continue;
|
|
2737
|
+
}
|
|
2738
|
+
try {
|
|
2739
|
+
vi.add(item.id, item.sessionId, embedding);
|
|
2740
|
+
ok++;
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
logger.warn("vector-index add batch: index write failed — skipping item", {
|
|
2743
|
+
kind: item.context.kind,
|
|
2744
|
+
id: item.context.logId,
|
|
2745
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2746
|
+
});
|
|
2747
|
+
fail++;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return {
|
|
2751
|
+
ok,
|
|
2752
|
+
fail
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
const DEFAULT_REBUILD_EMBED_BATCH = 32;
|
|
2756
|
+
function getRebuildEmbedBatchSize() {
|
|
2757
|
+
const raw = process.env.REBUILD_EMBED_BATCH_SIZE;
|
|
2758
|
+
if (!raw) return DEFAULT_REBUILD_EMBED_BATCH;
|
|
2759
|
+
const n = parseInt(raw, 10);
|
|
2760
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REBUILD_EMBED_BATCH;
|
|
2761
|
+
}
|
|
2646
2762
|
async function rebuildIndex(kv) {
|
|
2647
2763
|
const idx = getSearchIndex();
|
|
2648
2764
|
idx.clear();
|
|
2649
2765
|
vectorIndex?.clear();
|
|
2766
|
+
const batchSize = getRebuildEmbedBatchSize();
|
|
2767
|
+
const pending = [];
|
|
2650
2768
|
let count = 0;
|
|
2769
|
+
const flush = async () => {
|
|
2770
|
+
if (pending.length === 0) return;
|
|
2771
|
+
await vectorIndexAddBatchGuarded(pending);
|
|
2772
|
+
pending.length = 0;
|
|
2773
|
+
};
|
|
2774
|
+
const enqueue = async (job) => {
|
|
2775
|
+
pending.push(job);
|
|
2776
|
+
if (pending.length >= batchSize) await flush();
|
|
2777
|
+
};
|
|
2651
2778
|
try {
|
|
2652
2779
|
const memories = await kv.list(KV.memories);
|
|
2653
2780
|
for (const memory of memories) {
|
|
2654
2781
|
if (memory.isLatest === false) continue;
|
|
2655
2782
|
if (!memory.title || !memory.content) continue;
|
|
2656
2783
|
idx.add(memoryToObservation(memory));
|
|
2657
|
-
await
|
|
2658
|
-
|
|
2659
|
-
|
|
2784
|
+
await enqueue({
|
|
2785
|
+
id: memory.id,
|
|
2786
|
+
sessionId: memory.sessionIds[0] ?? "memory",
|
|
2787
|
+
text: memory.title + " " + memory.content,
|
|
2788
|
+
context: {
|
|
2789
|
+
kind: "memory",
|
|
2790
|
+
logId: memory.id
|
|
2791
|
+
}
|
|
2660
2792
|
});
|
|
2661
2793
|
count++;
|
|
2662
2794
|
}
|
|
@@ -2664,7 +2796,10 @@ async function rebuildIndex(kv) {
|
|
|
2664
2796
|
logger.warn("rebuildIndex: failed to load memories", { error: err instanceof Error ? err.message : String(err) });
|
|
2665
2797
|
}
|
|
2666
2798
|
const sessions = await kv.list(KV.sessions);
|
|
2667
|
-
if (!sessions.length)
|
|
2799
|
+
if (!sessions.length) {
|
|
2800
|
+
await flush();
|
|
2801
|
+
return count;
|
|
2802
|
+
}
|
|
2668
2803
|
const obsPerSession = [];
|
|
2669
2804
|
const failedSessions = [];
|
|
2670
2805
|
for (let batch = 0; batch < sessions.length; batch += 10) {
|
|
@@ -2682,12 +2817,18 @@ async function rebuildIndex(kv) {
|
|
|
2682
2817
|
if (failedSessions.length > 0) logger.warn("rebuildIndex: failed to load observations for sessions", { failedSessions });
|
|
2683
2818
|
for (const observations of obsPerSession) for (const obs of observations) if (obs.title && obs.narrative) {
|
|
2684
2819
|
idx.add(obs);
|
|
2685
|
-
await
|
|
2686
|
-
|
|
2687
|
-
|
|
2820
|
+
await enqueue({
|
|
2821
|
+
id: obs.id,
|
|
2822
|
+
sessionId: obs.sessionId,
|
|
2823
|
+
text: obs.title + " " + obs.narrative,
|
|
2824
|
+
context: {
|
|
2825
|
+
kind: "observation",
|
|
2826
|
+
logId: obs.id
|
|
2827
|
+
}
|
|
2688
2828
|
});
|
|
2689
2829
|
count++;
|
|
2690
2830
|
}
|
|
2831
|
+
await flush();
|
|
2691
2832
|
return count;
|
|
2692
2833
|
}
|
|
2693
2834
|
function registerSearchFunction(sdk, kv) {
|
|
@@ -2907,11 +3048,14 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2907
3048
|
error: `Session observation limit reached (${maxObservationsPerSession})`
|
|
2908
3049
|
};
|
|
2909
3050
|
}
|
|
3051
|
+
const existingSession = await kv.get(KV.sessions, payload.sessionId);
|
|
3052
|
+
const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
|
|
3053
|
+
if (inheritedAgentId) raw.agentId = inheritedAgentId;
|
|
2910
3054
|
if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
|
|
2911
|
-
const { saveImageToDisk } = await import("./image-store-
|
|
3055
|
+
const { saveImageToDisk } = await import("./image-store-CdE0amb1.mjs");
|
|
2912
3056
|
const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
|
|
2913
3057
|
raw.imageData = filePath;
|
|
2914
|
-
const { incrementImageRef } = await import("./image-refs-
|
|
3058
|
+
const { incrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
|
|
2915
3059
|
await incrementImageRef(kv, filePath);
|
|
2916
3060
|
sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
|
|
2917
3061
|
if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
|
|
@@ -2924,7 +3068,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2924
3068
|
await kv.set(KV.observations(payload.sessionId), obsId, raw);
|
|
2925
3069
|
} catch (error) {
|
|
2926
3070
|
if (raw.imageData) {
|
|
2927
|
-
const { deleteImage } = await import("./image-store-
|
|
3071
|
+
const { deleteImage } = await import("./image-store-CdE0amb1.mjs");
|
|
2928
3072
|
const { deletedBytes } = await deleteImage(raw.imageData);
|
|
2929
3073
|
if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
|
|
2930
3074
|
}
|
|
@@ -2958,7 +3102,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2958
3102
|
},
|
|
2959
3103
|
action: TriggerAction.Void()
|
|
2960
3104
|
});
|
|
2961
|
-
const session =
|
|
3105
|
+
const session = existingSession;
|
|
2962
3106
|
if (session) {
|
|
2963
3107
|
const updates = [{
|
|
2964
3108
|
type: "set",
|
|
@@ -2978,6 +3122,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2978
3122
|
});
|
|
2979
3123
|
}
|
|
2980
3124
|
await kv.update(KV.sessions, payload.sessionId, updates);
|
|
3125
|
+
} else if (typeof payload.project === "string" && payload.project.trim().length > 0 && typeof payload.cwd === "string" && payload.cwd.trim().length > 0) {
|
|
3126
|
+
const trimmedPrompt = typeof raw.userPrompt === "string" ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
|
|
3127
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3128
|
+
await kv.set(KV.sessions, payload.sessionId, {
|
|
3129
|
+
id: payload.sessionId,
|
|
3130
|
+
project: payload.project,
|
|
3131
|
+
cwd: payload.cwd,
|
|
3132
|
+
startedAt: payload.timestamp ?? ts,
|
|
3133
|
+
updatedAt: ts,
|
|
3134
|
+
status: "active",
|
|
3135
|
+
observationCount: 1,
|
|
3136
|
+
...inheritedAgentId ? { agentId: inheritedAgentId } : {},
|
|
3137
|
+
...trimmedPrompt && trimmedPrompt.length > 0 ? { firstPrompt: trimmedPrompt } : {}
|
|
3138
|
+
});
|
|
2981
3139
|
}
|
|
2982
3140
|
if (isAutoCompressEnabled()) await sdk.trigger({
|
|
2983
3141
|
function_id: "mem::compress",
|
|
@@ -4130,7 +4288,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
|
|
|
4130
4288
|
confidence: qualityScore / 100,
|
|
4131
4289
|
...hasImage ? { modality: data.raw.modality } : {},
|
|
4132
4290
|
...imageDescription ? { imageDescription } : {},
|
|
4133
|
-
...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
|
|
4291
|
+
...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
|
|
4292
|
+
...data.raw.agentId ? { agentId: data.raw.agentId } : {}
|
|
4134
4293
|
};
|
|
4135
4294
|
await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
|
|
4136
4295
|
try {
|
|
@@ -4365,9 +4524,134 @@ function buildSummaryPrompt(observations) {
|
|
|
4365
4524
|
});
|
|
4366
4525
|
return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
|
|
4367
4526
|
}
|
|
4527
|
+
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.
|
|
4528
|
+
|
|
4529
|
+
Output EXACTLY this XML format with no additional text:
|
|
4530
|
+
|
|
4531
|
+
<summary>
|
|
4532
|
+
<title>Short session title (max 100 chars)</title>
|
|
4533
|
+
<narrative>3-5 sentence narrative covering the whole session</narrative>
|
|
4534
|
+
<decisions>
|
|
4535
|
+
<decision>Key technical decision made</decision>
|
|
4536
|
+
</decisions>
|
|
4537
|
+
<files>
|
|
4538
|
+
<file>path/to/modified/file</file>
|
|
4539
|
+
</files>
|
|
4540
|
+
<concepts>
|
|
4541
|
+
<concept>key concept from session</concept>
|
|
4542
|
+
</concepts>
|
|
4543
|
+
</summary>
|
|
4544
|
+
|
|
4545
|
+
Rules:
|
|
4546
|
+
- Synthesize a single narrative that reflects the whole arc, not a chunk-by-chunk recap
|
|
4547
|
+
- Preserve every distinct decision across chunks
|
|
4548
|
+
- Union (deduplicate) all files and concepts
|
|
4549
|
+
- Title should capture the session's overall outcome`;
|
|
4550
|
+
function buildReducePrompt(partials) {
|
|
4551
|
+
const sections = partials.map((p, i) => {
|
|
4552
|
+
const decisions = p.keyDecisions.map((d) => ` - ${d}`).join("\n");
|
|
4553
|
+
const files = p.filesModified.map((f) => ` - ${f}`).join("\n");
|
|
4554
|
+
const concepts = p.concepts.join(", ");
|
|
4555
|
+
return `[Chunk ${i + 1} of ${partials.length} — obs ${p.obsRangeStart}-${p.obsRangeEnd}]
|
|
4556
|
+
Title: ${p.title}
|
|
4557
|
+
Narrative: ${p.narrative}
|
|
4558
|
+
Decisions:
|
|
4559
|
+
${decisions}
|
|
4560
|
+
Files:
|
|
4561
|
+
${files}
|
|
4562
|
+
Concepts: ${concepts}`;
|
|
4563
|
+
});
|
|
4564
|
+
return `Partial summaries (${partials.length} chunks of one session, chronological):\n\n${sections.join("\n\n---\n\n")}`;
|
|
4565
|
+
}
|
|
4368
4566
|
|
|
4369
4567
|
//#endregion
|
|
4370
4568
|
//#region src/functions/summarize.ts
|
|
4569
|
+
const CHUNK_SIZE_DEFAULT = 400;
|
|
4570
|
+
const CHUNK_CONCURRENCY_DEFAULT = 6;
|
|
4571
|
+
const MAX_SKIP_RATIO = .5;
|
|
4572
|
+
function getChunkSize() {
|
|
4573
|
+
const raw = process.env.SUMMARIZE_CHUNK_SIZE;
|
|
4574
|
+
if (!raw) return CHUNK_SIZE_DEFAULT;
|
|
4575
|
+
const n = parseInt(raw, 10);
|
|
4576
|
+
return Number.isFinite(n) && n > 0 ? n : CHUNK_SIZE_DEFAULT;
|
|
4577
|
+
}
|
|
4578
|
+
function getChunkConcurrency() {
|
|
4579
|
+
const raw = process.env.SUMMARIZE_CHUNK_CONCURRENCY;
|
|
4580
|
+
if (!raw) return CHUNK_CONCURRENCY_DEFAULT;
|
|
4581
|
+
const n = parseInt(raw, 10);
|
|
4582
|
+
return Number.isFinite(n) && n > 0 ? n : CHUNK_CONCURRENCY_DEFAULT;
|
|
4583
|
+
}
|
|
4584
|
+
async function summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, total) {
|
|
4585
|
+
for (let attempt = 1; attempt <= 2; attempt++) try {
|
|
4586
|
+
const parsed = parseSummaryXml(await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(chunk)), sessionId, project, chunk.length);
|
|
4587
|
+
if (parsed) return parsed;
|
|
4588
|
+
logger.warn("Summarize chunk parse failed", {
|
|
4589
|
+
sessionId,
|
|
4590
|
+
chunk: `${idx + 1}/${total}`,
|
|
4591
|
+
attempt
|
|
4592
|
+
});
|
|
4593
|
+
} catch (err) {
|
|
4594
|
+
logger.warn("Summarize chunk LLM call failed", {
|
|
4595
|
+
sessionId,
|
|
4596
|
+
chunk: `${idx + 1}/${total}`,
|
|
4597
|
+
attempt,
|
|
4598
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4599
|
+
});
|
|
4600
|
+
}
|
|
4601
|
+
return null;
|
|
4602
|
+
}
|
|
4603
|
+
async function produceSummaryXml(provider, compressed, sessionId, project) {
|
|
4604
|
+
const chunkSize = getChunkSize();
|
|
4605
|
+
if (compressed.length <= chunkSize) return {
|
|
4606
|
+
response: await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(compressed)),
|
|
4607
|
+
mode: "single",
|
|
4608
|
+
chunks: 1
|
|
4609
|
+
};
|
|
4610
|
+
const chunks = [];
|
|
4611
|
+
for (let i = 0; i < compressed.length; i += chunkSize) chunks.push(compressed.slice(i, i + chunkSize));
|
|
4612
|
+
const concurrency = getChunkConcurrency();
|
|
4613
|
+
logger.info("Summarize chunking session", {
|
|
4614
|
+
sessionId,
|
|
4615
|
+
chunks: chunks.length,
|
|
4616
|
+
chunkSize,
|
|
4617
|
+
concurrency,
|
|
4618
|
+
totalObservations: compressed.length
|
|
4619
|
+
});
|
|
4620
|
+
const partialByIdx = new Array(chunks.length).fill(null);
|
|
4621
|
+
for (let batchStart = 0; batchStart < chunks.length; batchStart += concurrency) {
|
|
4622
|
+
const batch = chunks.slice(batchStart, batchStart + concurrency);
|
|
4623
|
+
await Promise.all(batch.map(async (chunk, j) => {
|
|
4624
|
+
const idx = batchStart + j;
|
|
4625
|
+
partialByIdx[idx] = await summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, chunks.length);
|
|
4626
|
+
}));
|
|
4627
|
+
}
|
|
4628
|
+
const skipped = partialByIdx.filter((p) => p === null).length;
|
|
4629
|
+
const partials = partialByIdx.filter((p) => p !== null);
|
|
4630
|
+
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`);
|
|
4631
|
+
if (skipped > 0) logger.warn("Summarize chunks partially skipped", {
|
|
4632
|
+
sessionId,
|
|
4633
|
+
skipped,
|
|
4634
|
+
total: chunks.length
|
|
4635
|
+
});
|
|
4636
|
+
const reduceInput = partials.map((p) => {
|
|
4637
|
+
const originalIdx = partialByIdx.indexOf(p);
|
|
4638
|
+
return {
|
|
4639
|
+
title: p.title,
|
|
4640
|
+
narrative: p.narrative,
|
|
4641
|
+
keyDecisions: p.keyDecisions,
|
|
4642
|
+
filesModified: p.filesModified,
|
|
4643
|
+
concepts: p.concepts,
|
|
4644
|
+
obsRangeStart: originalIdx * chunkSize + 1,
|
|
4645
|
+
obsRangeEnd: Math.min((originalIdx + 1) * chunkSize, compressed.length)
|
|
4646
|
+
};
|
|
4647
|
+
});
|
|
4648
|
+
return {
|
|
4649
|
+
response: await provider.summarize(REDUCE_SYSTEM, buildReducePrompt(reduceInput)),
|
|
4650
|
+
mode: "chunked",
|
|
4651
|
+
chunks: chunks.length,
|
|
4652
|
+
skipped
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4371
4655
|
function parseSummaryXml(xml, sessionId, project, obsCount) {
|
|
4372
4656
|
const title = getXmlTag(xml, "title");
|
|
4373
4657
|
if (!title) return null;
|
|
@@ -4416,16 +4700,15 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
|
|
|
4416
4700
|
};
|
|
4417
4701
|
}
|
|
4418
4702
|
try {
|
|
4419
|
-
const
|
|
4420
|
-
const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
|
|
4703
|
+
const { response, mode, chunks } = await produceSummaryXml(provider, compressed, sessionId, session.project);
|
|
4421
4704
|
if (!response || !response.trim()) {
|
|
4422
4705
|
const latencyMs = Date.now() - startMs;
|
|
4423
4706
|
if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
|
|
4424
4707
|
logger.warn("Empty provider response on summarize", {
|
|
4425
4708
|
sessionId,
|
|
4426
4709
|
provider: provider.name,
|
|
4427
|
-
|
|
4428
|
-
|
|
4710
|
+
mode,
|
|
4711
|
+
chunks,
|
|
4429
4712
|
observationCount: compressed.length
|
|
4430
4713
|
});
|
|
4431
4714
|
return {
|
|
@@ -4977,6 +5260,7 @@ function registerRememberFunction(sdk, kv) {
|
|
|
4977
5260
|
break;
|
|
4978
5261
|
}
|
|
4979
5262
|
}
|
|
5263
|
+
const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
|
|
4980
5264
|
const memory = {
|
|
4981
5265
|
id: generateId("mem"),
|
|
4982
5266
|
createdAt: now,
|
|
@@ -4992,7 +5276,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
4992
5276
|
parentId: supersededId,
|
|
4993
5277
|
supersedes: supersededId ? [supersededId] : [],
|
|
4994
5278
|
sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
|
|
4995
|
-
isLatest: true
|
|
5279
|
+
isLatest: true,
|
|
5280
|
+
...callAgentId ? { agentId: callAgentId } : {}
|
|
4996
5281
|
};
|
|
4997
5282
|
if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
|
|
4998
5283
|
if (supersededMemory) {
|
|
@@ -5032,12 +5317,14 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5032
5317
|
const deletedMemoryIds = [];
|
|
5033
5318
|
const deletedObservationIds = [];
|
|
5034
5319
|
let deletedSession = false;
|
|
5035
|
-
const { decrementImageRef } = await import("./image-refs-
|
|
5320
|
+
const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
|
|
5036
5321
|
if (data.memoryId) {
|
|
5037
5322
|
const mem = await kv.get(KV.memories, data.memoryId);
|
|
5038
5323
|
await kv.delete(KV.memories, data.memoryId);
|
|
5039
5324
|
if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
5040
5325
|
await deleteAccessLog(kv, data.memoryId);
|
|
5326
|
+
getSearchIndex().remove(data.memoryId);
|
|
5327
|
+
vectorIndexRemove(data.memoryId);
|
|
5041
5328
|
deletedMemoryIds.push(data.memoryId);
|
|
5042
5329
|
deleted++;
|
|
5043
5330
|
}
|
|
@@ -5046,6 +5333,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5046
5333
|
await kv.delete(KV.observations(data.sessionId), obsId);
|
|
5047
5334
|
if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5048
5335
|
if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5336
|
+
getSearchIndex().remove(obsId);
|
|
5337
|
+
vectorIndexRemove(obsId);
|
|
5049
5338
|
deletedObservationIds.push(obsId);
|
|
5050
5339
|
deleted++;
|
|
5051
5340
|
}
|
|
@@ -5055,6 +5344,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5055
5344
|
await kv.delete(KV.observations(data.sessionId), obs.id);
|
|
5056
5345
|
if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5057
5346
|
if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5347
|
+
getSearchIndex().remove(obs.id);
|
|
5348
|
+
vectorIndexRemove(obs.id);
|
|
5058
5349
|
deletedObservationIds.push(obs.id);
|
|
5059
5350
|
deleted++;
|
|
5060
5351
|
}
|
|
@@ -5063,14 +5354,17 @@ function registerRememberFunction(sdk, kv) {
|
|
|
5063
5354
|
deletedSession = true;
|
|
5064
5355
|
deleted += 2;
|
|
5065
5356
|
}
|
|
5066
|
-
if (deleted > 0)
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5357
|
+
if (deleted > 0) {
|
|
5358
|
+
await flushIndexSave();
|
|
5359
|
+
await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
|
|
5360
|
+
sessionId: data.sessionId,
|
|
5361
|
+
deleted,
|
|
5362
|
+
memoriesDeleted: deletedMemoryIds.length,
|
|
5363
|
+
observationsDeleted: deletedObservationIds.length,
|
|
5364
|
+
sessionDeleted: deletedSession,
|
|
5365
|
+
reason: "user-initiated forget"
|
|
5366
|
+
});
|
|
5367
|
+
}
|
|
5074
5368
|
logger.info("Memory forgotten", { deleted });
|
|
5075
5369
|
return {
|
|
5076
5370
|
success: true,
|
|
@@ -5131,7 +5425,7 @@ async function runRecoveredSessionConsolidation(sdk) {
|
|
|
5131
5425
|
function registerEvictFunction(sdk, kv) {
|
|
5132
5426
|
sdk.registerFunction("mem::evict", async (data) => {
|
|
5133
5427
|
const dryRun = data?.dryRun ?? false;
|
|
5134
|
-
const { decrementImageRef } = await import("./image-refs-
|
|
5428
|
+
const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
|
|
5135
5429
|
const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
|
|
5136
5430
|
const cfg = {
|
|
5137
5431
|
...DEFAULTS$1,
|
|
@@ -5582,8 +5876,12 @@ async function findByKeyword(kv, keyword, project) {
|
|
|
5582
5876
|
|
|
5583
5877
|
//#endregion
|
|
5584
5878
|
//#region src/functions/smart-search.ts
|
|
5879
|
+
const LESSON_CONTENT_PREVIEW_CHARS = 240;
|
|
5585
5880
|
function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
5586
5881
|
sdk.registerFunction("mem::smart-search", async (data) => {
|
|
5882
|
+
const isolated = isAgentScopeIsolated();
|
|
5883
|
+
const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
|
|
5884
|
+
const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
|
|
5587
5885
|
if (data.expandIds && data.expandIds.length > 0) {
|
|
5588
5886
|
const raw = data.expandIds.slice(0, 20);
|
|
5589
5887
|
const items = raw.map((entry) => {
|
|
@@ -5604,17 +5902,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
5604
5902
|
observation: obs
|
|
5605
5903
|
} : null)));
|
|
5606
5904
|
for (const r of results) if (r) expanded.push(r);
|
|
5607
|
-
|
|
5905
|
+
const scoped = filterAgentId ? expanded.filter((e) => e.observation.agentId === filterAgentId) : expanded;
|
|
5906
|
+
recordAccessBatch(kv, scoped.map((e) => e.observation.id));
|
|
5608
5907
|
const truncated = data.expandIds.length > raw.length;
|
|
5609
5908
|
logger.info("Smart search expanded", {
|
|
5610
5909
|
requested: data.expandIds.length,
|
|
5611
5910
|
attempted: raw.length,
|
|
5612
|
-
returned:
|
|
5911
|
+
returned: scoped.length,
|
|
5912
|
+
filteredOutOfScope: expanded.length - scoped.length,
|
|
5613
5913
|
truncated
|
|
5614
5914
|
});
|
|
5615
5915
|
return {
|
|
5616
5916
|
mode: "expanded",
|
|
5617
|
-
results:
|
|
5917
|
+
results: scoped,
|
|
5618
5918
|
truncated
|
|
5619
5919
|
};
|
|
5620
5920
|
}
|
|
@@ -5624,7 +5924,11 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
5624
5924
|
error: "query is required"
|
|
5625
5925
|
};
|
|
5626
5926
|
const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
|
|
5627
|
-
const
|
|
5927
|
+
const lessonLimit = Math.min(limit, 10);
|
|
5928
|
+
const includeLessons = data.includeLessons !== false;
|
|
5929
|
+
const overFetchLimit = filterAgentId ? Math.min(limit * 3, 300) : limit;
|
|
5930
|
+
const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
|
|
5931
|
+
const compact = (filterAgentId ? hybridResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : hybridResults.slice(0, limit)).map((r) => ({
|
|
5628
5932
|
obsId: r.observation.id,
|
|
5629
5933
|
sessionId: r.sessionId,
|
|
5630
5934
|
title: r.observation.title,
|
|
@@ -5635,14 +5939,42 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
|
|
|
5635
5939
|
recordAccessBatch(kv, compact.map((r) => r.obsId));
|
|
5636
5940
|
logger.info("Smart search compact", {
|
|
5637
5941
|
query: data.query,
|
|
5638
|
-
results: compact.length
|
|
5942
|
+
results: compact.length,
|
|
5943
|
+
lessons: lessons.length
|
|
5639
5944
|
});
|
|
5640
|
-
|
|
5945
|
+
const response = {
|
|
5641
5946
|
mode: "compact",
|
|
5642
5947
|
results: compact
|
|
5643
5948
|
};
|
|
5949
|
+
if (includeLessons) response.lessons = lessons;
|
|
5950
|
+
return response;
|
|
5644
5951
|
});
|
|
5645
5952
|
}
|
|
5953
|
+
async function recallLessons(sdk, query, limit, project) {
|
|
5954
|
+
try {
|
|
5955
|
+
const result = await sdk.trigger({
|
|
5956
|
+
function_id: "mem::lesson-recall",
|
|
5957
|
+
payload: {
|
|
5958
|
+
query,
|
|
5959
|
+
limit,
|
|
5960
|
+
project
|
|
5961
|
+
}
|
|
5962
|
+
});
|
|
5963
|
+
if (!result?.success || !Array.isArray(result.lessons)) return [];
|
|
5964
|
+
return result.lessons.map((l) => ({
|
|
5965
|
+
lessonId: l.id,
|
|
5966
|
+
content: l.content.length > LESSON_CONTENT_PREVIEW_CHARS ? l.content.slice(0, LESSON_CONTENT_PREVIEW_CHARS) + "…" : l.content,
|
|
5967
|
+
confidence: l.confidence,
|
|
5968
|
+
score: l.score ?? l.confidence,
|
|
5969
|
+
createdAt: l.createdAt,
|
|
5970
|
+
project: l.project,
|
|
5971
|
+
tags: l.tags ?? []
|
|
5972
|
+
}));
|
|
5973
|
+
} catch (err) {
|
|
5974
|
+
logger.warn("Smart search: mem::lesson-recall failed; returning empty lesson list", { error: err instanceof Error ? err.message : String(err) });
|
|
5975
|
+
return [];
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5646
5978
|
async function findObservation$1(kv, obsId, sessionIdHint) {
|
|
5647
5979
|
if (sessionIdHint) {
|
|
5648
5980
|
const obs = await kv.get(KV.observations(sessionIdHint), obsId).catch(() => null);
|
|
@@ -5753,7 +6085,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
5753
6085
|
sdk.registerFunction("mem::auto-forget", async (data) => {
|
|
5754
6086
|
const dryRun = data?.dryRun ?? false;
|
|
5755
6087
|
const now = Date.now();
|
|
5756
|
-
const { decrementImageRef } = await import("./image-refs-
|
|
6088
|
+
const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
|
|
5757
6089
|
const result = {
|
|
5758
6090
|
ttlExpired: [],
|
|
5759
6091
|
contradictions: [],
|
|
@@ -5775,6 +6107,8 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
5775
6107
|
timestamp: mem.forgetAfter
|
|
5776
6108
|
});
|
|
5777
6109
|
await deleteAccessLog(kv, mem.id);
|
|
6110
|
+
getSearchIndex().remove(mem.id);
|
|
6111
|
+
vectorIndexRemove(mem.id);
|
|
5778
6112
|
}
|
|
5779
6113
|
}
|
|
5780
6114
|
}
|
|
@@ -5852,10 +6186,13 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
5852
6186
|
sessionId: sessions[i].id,
|
|
5853
6187
|
timestamp: obs.timestamp
|
|
5854
6188
|
});
|
|
6189
|
+
getSearchIndex().remove(obs.id);
|
|
6190
|
+
vectorIndexRemove(obs.id);
|
|
5855
6191
|
}
|
|
5856
6192
|
}
|
|
5857
6193
|
}
|
|
5858
6194
|
}
|
|
6195
|
+
if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
|
|
5859
6196
|
logger.info("Auto-forget complete", {
|
|
5860
6197
|
ttlExpired: result.ttlExpired.length,
|
|
5861
6198
|
contradictions: result.contradictions.length,
|
|
@@ -6002,7 +6339,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
6002
6339
|
"0.9.17",
|
|
6003
6340
|
"0.9.18",
|
|
6004
6341
|
"0.9.19",
|
|
6005
|
-
"0.9.20"
|
|
6342
|
+
"0.9.20",
|
|
6343
|
+
"0.9.21",
|
|
6344
|
+
"0.9.22"
|
|
6006
6345
|
]).has(importData.version)) return {
|
|
6007
6346
|
success: false,
|
|
6008
6347
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -6528,12 +6867,12 @@ function parseGraphXml(xml, observationIds) {
|
|
|
6528
6867
|
const nodes = [];
|
|
6529
6868
|
const edges = [];
|
|
6530
6869
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6531
|
-
const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]
|
|
6870
|
+
const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*?(?:\/>|>([\s\S]*?)<\/entity>)/g;
|
|
6532
6871
|
let match;
|
|
6533
6872
|
while ((match = entityRegex.exec(xml)) !== null) {
|
|
6534
6873
|
const type = match[1];
|
|
6535
6874
|
const name = match[2];
|
|
6536
|
-
const propsBlock = match[3];
|
|
6875
|
+
const propsBlock = match[3] ?? "";
|
|
6537
6876
|
const properties = {};
|
|
6538
6877
|
const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
|
|
6539
6878
|
let propMatch;
|
|
@@ -7061,8 +7400,11 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7061
7400
|
for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
|
|
7062
7401
|
await kv.delete(KV.memories, id);
|
|
7063
7402
|
await deleteAccessLog(kv, id);
|
|
7403
|
+
getSearchIndex().remove(id);
|
|
7404
|
+
vectorIndexRemove(id);
|
|
7064
7405
|
deleted++;
|
|
7065
7406
|
}
|
|
7407
|
+
if (deleted > 0) await flushIndexSave();
|
|
7066
7408
|
await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
|
|
7067
7409
|
reason: data.reason || "manual deletion",
|
|
7068
7410
|
deleted
|
|
@@ -7115,6 +7457,8 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7115
7457
|
(await Promise.allSettled(batch.map(async (mem) => {
|
|
7116
7458
|
await kv.delete(KV.memories, mem.id);
|
|
7117
7459
|
await deleteAccessLog(kv, mem.id);
|
|
7460
|
+
getSearchIndex().remove(mem.id);
|
|
7461
|
+
vectorIndexRemove(mem.id);
|
|
7118
7462
|
}))).forEach((result, j) => {
|
|
7119
7463
|
const mem = batch[j];
|
|
7120
7464
|
if (result.status === "fulfilled") successfulIds.push(mem.id);
|
|
@@ -7130,6 +7474,7 @@ function registerGovernanceFunction(sdk, kv) {
|
|
|
7130
7474
|
}
|
|
7131
7475
|
});
|
|
7132
7476
|
}
|
|
7477
|
+
if (successfulIds.length > 0) await flushIndexSave();
|
|
7133
7478
|
await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
|
|
7134
7479
|
filter: data,
|
|
7135
7480
|
deleted: successfulIds.length,
|
|
@@ -9704,6 +10049,12 @@ const ALL_CATEGORIES = [
|
|
|
9704
10049
|
"signals",
|
|
9705
10050
|
"sessions",
|
|
9706
10051
|
"memories",
|
|
10052
|
+
"lessons",
|
|
10053
|
+
"summaries",
|
|
10054
|
+
"semantic",
|
|
10055
|
+
"procedural",
|
|
10056
|
+
"crystals",
|
|
10057
|
+
"insights",
|
|
9707
10058
|
"mesh"
|
|
9708
10059
|
];
|
|
9709
10060
|
const TWENTY_FOUR_HOURS_MS = 1440 * 60 * 1e3;
|
|
@@ -9936,6 +10287,133 @@ function registerDiagnosticsFunction(sdk, kv) {
|
|
|
9936
10287
|
fixable: false
|
|
9937
10288
|
});
|
|
9938
10289
|
}
|
|
10290
|
+
if (categories.includes("lessons")) {
|
|
10291
|
+
const lessons = await kv.list(KV.lessons);
|
|
10292
|
+
const live = lessons.filter((l) => !l.deleted);
|
|
10293
|
+
let lessonIssues = 0;
|
|
10294
|
+
for (const l of live) if (!Number.isFinite(l.confidence) || l.confidence < 0 || l.confidence > 1) {
|
|
10295
|
+
checks.push({
|
|
10296
|
+
name: `lesson-bad-confidence:${l.id}`,
|
|
10297
|
+
category: "lessons",
|
|
10298
|
+
status: "warn",
|
|
10299
|
+
message: `Lesson ${l.id} has confidence ${l.confidence} (expected finite number in 0..1)`,
|
|
10300
|
+
fixable: false
|
|
10301
|
+
});
|
|
10302
|
+
lessonIssues++;
|
|
10303
|
+
}
|
|
10304
|
+
if (lessonIssues === 0) checks.push({
|
|
10305
|
+
name: "lessons-ok",
|
|
10306
|
+
category: "lessons",
|
|
10307
|
+
status: "pass",
|
|
10308
|
+
message: `All ${live.length} lessons are healthy (${lessons.length - live.length} tombstoned)`,
|
|
10309
|
+
fixable: false
|
|
10310
|
+
});
|
|
10311
|
+
}
|
|
10312
|
+
if (categories.includes("summaries")) {
|
|
10313
|
+
const summaries = await kv.list(KV.summaries);
|
|
10314
|
+
let summaryIssues = 0;
|
|
10315
|
+
for (const s of summaries) if (typeof s.title !== "string" || s.title.trim().length === 0) {
|
|
10316
|
+
checks.push({
|
|
10317
|
+
name: `summary-missing-title:${s.sessionId}`,
|
|
10318
|
+
category: "summaries",
|
|
10319
|
+
status: "warn",
|
|
10320
|
+
message: `Summary for session ${s.sessionId} has no title`,
|
|
10321
|
+
fixable: false
|
|
10322
|
+
});
|
|
10323
|
+
summaryIssues++;
|
|
10324
|
+
}
|
|
10325
|
+
if (summaryIssues === 0) checks.push({
|
|
10326
|
+
name: "summaries-ok",
|
|
10327
|
+
category: "summaries",
|
|
10328
|
+
status: "pass",
|
|
10329
|
+
message: `All ${summaries.length} session summaries are consistent`,
|
|
10330
|
+
fixable: false
|
|
10331
|
+
});
|
|
10332
|
+
}
|
|
10333
|
+
if (categories.includes("semantic")) {
|
|
10334
|
+
const semantic = await kv.list(KV.semantic);
|
|
10335
|
+
let semanticIssues = 0;
|
|
10336
|
+
for (const s of semantic) if (!Number.isFinite(s.confidence) || s.confidence < 0 || s.confidence > 1) {
|
|
10337
|
+
checks.push({
|
|
10338
|
+
name: `semantic-bad-confidence:${s.id}`,
|
|
10339
|
+
category: "semantic",
|
|
10340
|
+
status: "warn",
|
|
10341
|
+
message: `Semantic fact ${s.id} has confidence ${s.confidence} (expected finite number in 0..1)`,
|
|
10342
|
+
fixable: false
|
|
10343
|
+
});
|
|
10344
|
+
semanticIssues++;
|
|
10345
|
+
}
|
|
10346
|
+
if (semanticIssues === 0) checks.push({
|
|
10347
|
+
name: "semantic-ok",
|
|
10348
|
+
category: "semantic",
|
|
10349
|
+
status: "pass",
|
|
10350
|
+
message: `All ${semantic.length} semantic memories are consistent`,
|
|
10351
|
+
fixable: false
|
|
10352
|
+
});
|
|
10353
|
+
}
|
|
10354
|
+
if (categories.includes("procedural")) {
|
|
10355
|
+
const procedural = await kv.list(KV.procedural);
|
|
10356
|
+
let proceduralIssues = 0;
|
|
10357
|
+
for (const p of procedural) if (!Array.isArray(p.steps) || p.steps.length === 0) {
|
|
10358
|
+
checks.push({
|
|
10359
|
+
name: `procedural-empty-steps:${p.id}`,
|
|
10360
|
+
category: "procedural",
|
|
10361
|
+
status: "warn",
|
|
10362
|
+
message: `Procedural memory "${p.name}" (${p.id}) has no steps`,
|
|
10363
|
+
fixable: false
|
|
10364
|
+
});
|
|
10365
|
+
proceduralIssues++;
|
|
10366
|
+
}
|
|
10367
|
+
if (proceduralIssues === 0) checks.push({
|
|
10368
|
+
name: "procedural-ok",
|
|
10369
|
+
category: "procedural",
|
|
10370
|
+
status: "pass",
|
|
10371
|
+
message: `All ${procedural.length} procedural memories are consistent`,
|
|
10372
|
+
fixable: false
|
|
10373
|
+
});
|
|
10374
|
+
}
|
|
10375
|
+
if (categories.includes("crystals")) {
|
|
10376
|
+
const crystals = await kv.list(KV.crystals);
|
|
10377
|
+
let crystalIssues = 0;
|
|
10378
|
+
for (const c of crystals) if (typeof c.narrative !== "string" || c.narrative.trim().length === 0) {
|
|
10379
|
+
checks.push({
|
|
10380
|
+
name: `crystal-empty-narrative:${c.id}`,
|
|
10381
|
+
category: "crystals",
|
|
10382
|
+
status: "warn",
|
|
10383
|
+
message: `Crystal ${c.id} has empty narrative`,
|
|
10384
|
+
fixable: false
|
|
10385
|
+
});
|
|
10386
|
+
crystalIssues++;
|
|
10387
|
+
}
|
|
10388
|
+
if (crystalIssues === 0) checks.push({
|
|
10389
|
+
name: "crystals-ok",
|
|
10390
|
+
category: "crystals",
|
|
10391
|
+
status: "pass",
|
|
10392
|
+
message: `All ${crystals.length} crystals are consistent`,
|
|
10393
|
+
fixable: false
|
|
10394
|
+
});
|
|
10395
|
+
}
|
|
10396
|
+
if (categories.includes("insights")) {
|
|
10397
|
+
const insights = await kv.list(KV.insights);
|
|
10398
|
+
let insightIssues = 0;
|
|
10399
|
+
for (const i of insights) if (!Number.isFinite(i.confidence) || i.confidence < 0 || i.confidence > 1) {
|
|
10400
|
+
checks.push({
|
|
10401
|
+
name: `insight-bad-confidence:${i.id}`,
|
|
10402
|
+
category: "insights",
|
|
10403
|
+
status: "warn",
|
|
10404
|
+
message: `Insight ${i.id} has confidence ${i.confidence} (expected finite number in 0..1)`,
|
|
10405
|
+
fixable: false
|
|
10406
|
+
});
|
|
10407
|
+
insightIssues++;
|
|
10408
|
+
}
|
|
10409
|
+
if (insightIssues === 0) checks.push({
|
|
10410
|
+
name: "insights-ok",
|
|
10411
|
+
category: "insights",
|
|
10412
|
+
status: "pass",
|
|
10413
|
+
message: `All ${insights.length} insights are consistent`,
|
|
10414
|
+
fixable: false
|
|
10415
|
+
});
|
|
10416
|
+
}
|
|
9939
10417
|
if (categories.includes("mesh")) {
|
|
9940
10418
|
const peers = await kv.list(KV.mesh);
|
|
9941
10419
|
let meshIssues = 0;
|
|
@@ -12359,7 +12837,7 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
12359
12837
|
const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
|
|
12360
12838
|
const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
|
|
12361
12839
|
const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
|
|
12362
|
-
const { decrementImageRef } = await import("./image-refs-
|
|
12840
|
+
const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
|
|
12363
12841
|
const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
|
|
12364
12842
|
if (data?.dryRun) return {
|
|
12365
12843
|
success: true,
|
|
@@ -12396,6 +12874,8 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
12396
12874
|
await kv.delete(scope, candidate.memoryId);
|
|
12397
12875
|
await kv.delete(KV.retentionScores, candidate.memoryId);
|
|
12398
12876
|
await deleteAccessLog(kv, candidate.memoryId);
|
|
12877
|
+
getSearchIndex().remove(candidate.memoryId);
|
|
12878
|
+
vectorIndexRemove(candidate.memoryId);
|
|
12399
12879
|
evicted++;
|
|
12400
12880
|
evictedIds.push(candidate.memoryId);
|
|
12401
12881
|
if (resolvedSource === "semantic") evictedSemantic++;
|
|
@@ -12403,13 +12883,16 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
12403
12883
|
} catch {
|
|
12404
12884
|
continue;
|
|
12405
12885
|
}
|
|
12406
|
-
if (evicted > 0)
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12886
|
+
if (evicted > 0) {
|
|
12887
|
+
await flushIndexSave();
|
|
12888
|
+
await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
|
|
12889
|
+
threshold,
|
|
12890
|
+
evicted,
|
|
12891
|
+
evictedEpisodic,
|
|
12892
|
+
evictedSemantic,
|
|
12893
|
+
reason: "retention score below threshold"
|
|
12894
|
+
});
|
|
12895
|
+
}
|
|
12413
12896
|
logger.info("Retention-based eviction complete", {
|
|
12414
12897
|
evicted,
|
|
12415
12898
|
evictedEpisodic,
|
|
@@ -13314,38 +13797,253 @@ function renderViewerDocument() {
|
|
|
13314
13797
|
}
|
|
13315
13798
|
|
|
13316
13799
|
//#endregion
|
|
13317
|
-
//#region src/
|
|
13318
|
-
function
|
|
13319
|
-
|
|
13320
|
-
const
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
const
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
body: { error: "unauthorized" }
|
|
13329
|
-
};
|
|
13800
|
+
//#region src/viewer/server.ts
|
|
13801
|
+
function loadViewerFavicon() {
|
|
13802
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
13803
|
+
const candidates = [
|
|
13804
|
+
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
13805
|
+
join(base, "..", "viewer", "favicon.svg"),
|
|
13806
|
+
join(base, "viewer", "favicon.svg")
|
|
13807
|
+
];
|
|
13808
|
+
for (const path of candidates) try {
|
|
13809
|
+
return readFileSync(path);
|
|
13810
|
+
} catch {}
|
|
13330
13811
|
return null;
|
|
13331
13812
|
}
|
|
13332
|
-
|
|
13333
|
-
|
|
13813
|
+
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());
|
|
13814
|
+
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
13815
|
+
function buildAllowedHosts(origins, listenPort) {
|
|
13816
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
13817
|
+
for (const o of origins) try {
|
|
13818
|
+
const parsed = new URL(o);
|
|
13819
|
+
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
13820
|
+
} catch {}
|
|
13821
|
+
hosts.add(`localhost:${listenPort}`);
|
|
13822
|
+
hosts.add(`127.0.0.1:${listenPort}`);
|
|
13823
|
+
hosts.add(`[::1]:${listenPort}`);
|
|
13824
|
+
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
13825
|
+
return hosts;
|
|
13826
|
+
}
|
|
13827
|
+
function isHostAllowed(headerHost, allowed) {
|
|
13828
|
+
if (typeof headerHost !== "string") return false;
|
|
13829
|
+
const lower = headerHost.toLowerCase().trim();
|
|
13830
|
+
if (!lower) return false;
|
|
13831
|
+
return allowed.has(lower);
|
|
13832
|
+
}
|
|
13833
|
+
function corsHeaders(req) {
|
|
13834
|
+
const origin = req.headers.origin || "";
|
|
13835
|
+
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
13334
13836
|
return {
|
|
13335
|
-
|
|
13336
|
-
|
|
13837
|
+
"Access-Control-Allow-Origin": allowed,
|
|
13838
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
13839
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
13840
|
+
Vary: "Origin"
|
|
13337
13841
|
};
|
|
13338
13842
|
}
|
|
13339
|
-
function
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13843
|
+
function json(res, status, data, req) {
|
|
13844
|
+
const body = JSON.stringify(data);
|
|
13845
|
+
const cors = req ? corsHeaders(req) : {
|
|
13846
|
+
"Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
|
|
13847
|
+
Vary: "Origin"
|
|
13343
13848
|
};
|
|
13849
|
+
res.writeHead(status, {
|
|
13850
|
+
...cors,
|
|
13851
|
+
"Content-Type": "application/json"
|
|
13852
|
+
});
|
|
13853
|
+
res.end(body);
|
|
13344
13854
|
}
|
|
13345
|
-
function
|
|
13346
|
-
return
|
|
13347
|
-
|
|
13348
|
-
|
|
13855
|
+
function readBody(req) {
|
|
13856
|
+
return new Promise((resolve, reject) => {
|
|
13857
|
+
let data = "";
|
|
13858
|
+
let size = 0;
|
|
13859
|
+
req.on("data", (chunk) => {
|
|
13860
|
+
size += chunk.length;
|
|
13861
|
+
if (size > 1e6) {
|
|
13862
|
+
req.destroy();
|
|
13863
|
+
reject(/* @__PURE__ */ new Error("too large"));
|
|
13864
|
+
return;
|
|
13865
|
+
}
|
|
13866
|
+
data += chunk.toString();
|
|
13867
|
+
});
|
|
13868
|
+
req.on("end", () => resolve(data));
|
|
13869
|
+
req.on("error", reject);
|
|
13870
|
+
});
|
|
13871
|
+
}
|
|
13872
|
+
const MAX_VIEWER_PORT_RETRIES = 10;
|
|
13873
|
+
let boundViewerPort = null;
|
|
13874
|
+
let viewerSkipped = false;
|
|
13875
|
+
function getBoundViewerPort() {
|
|
13876
|
+
return boundViewerPort;
|
|
13877
|
+
}
|
|
13878
|
+
function getViewerSkipped() {
|
|
13879
|
+
return viewerSkipped;
|
|
13880
|
+
}
|
|
13881
|
+
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
13882
|
+
boundViewerPort = null;
|
|
13883
|
+
viewerSkipped = false;
|
|
13884
|
+
const resolvedRestPort = restPort ?? port - 2;
|
|
13885
|
+
const requestedPort = port;
|
|
13886
|
+
let allowedHosts = null;
|
|
13887
|
+
const server = createServer(async (req, res) => {
|
|
13888
|
+
if (!allowedHosts) {
|
|
13889
|
+
const addr = server.address();
|
|
13890
|
+
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
13891
|
+
}
|
|
13892
|
+
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
13893
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
13894
|
+
res.end("forbidden host");
|
|
13895
|
+
return;
|
|
13896
|
+
}
|
|
13897
|
+
const raw = req.url || "/";
|
|
13898
|
+
const qIdx = raw.indexOf("?");
|
|
13899
|
+
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
13900
|
+
const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
|
|
13901
|
+
const method = req.method || "GET";
|
|
13902
|
+
if (method === "OPTIONS") {
|
|
13903
|
+
res.writeHead(204, {
|
|
13904
|
+
...corsHeaders(req),
|
|
13905
|
+
"Access-Control-Max-Age": "86400"
|
|
13906
|
+
});
|
|
13907
|
+
res.end();
|
|
13908
|
+
return;
|
|
13909
|
+
}
|
|
13910
|
+
if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
|
|
13911
|
+
const rendered = renderViewerDocument();
|
|
13912
|
+
if (rendered.found) {
|
|
13913
|
+
res.writeHead(200, {
|
|
13914
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
13915
|
+
"Content-Security-Policy": rendered.csp,
|
|
13916
|
+
"Cache-Control": "no-cache"
|
|
13917
|
+
});
|
|
13918
|
+
res.end(rendered.html);
|
|
13919
|
+
return;
|
|
13920
|
+
}
|
|
13921
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
13922
|
+
res.end("viewer not found");
|
|
13923
|
+
return;
|
|
13924
|
+
}
|
|
13925
|
+
if (method === "GET" && pathname === "/favicon.svg") {
|
|
13926
|
+
const favicon = loadViewerFavicon();
|
|
13927
|
+
if (favicon) {
|
|
13928
|
+
res.writeHead(200, {
|
|
13929
|
+
"Content-Type": "image/svg+xml",
|
|
13930
|
+
"Cache-Control": "public, max-age=3600"
|
|
13931
|
+
});
|
|
13932
|
+
res.end(favicon);
|
|
13933
|
+
return;
|
|
13934
|
+
}
|
|
13935
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
13936
|
+
res.end("favicon not found");
|
|
13937
|
+
return;
|
|
13938
|
+
}
|
|
13939
|
+
try {
|
|
13940
|
+
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
13941
|
+
} catch (err) {
|
|
13942
|
+
console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
|
|
13943
|
+
json(res, 502, { error: "upstream error" }, req);
|
|
13944
|
+
}
|
|
13945
|
+
});
|
|
13946
|
+
let attempt = 0;
|
|
13947
|
+
let currentPort = requestedPort;
|
|
13948
|
+
const tryListen = () => {
|
|
13949
|
+
server.listen(currentPort, "127.0.0.1");
|
|
13950
|
+
};
|
|
13951
|
+
server.on("listening", () => {
|
|
13952
|
+
const addr = server.address();
|
|
13953
|
+
boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
|
|
13954
|
+
viewerSkipped = false;
|
|
13955
|
+
if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
|
|
13956
|
+
else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
|
|
13957
|
+
});
|
|
13958
|
+
server.on("error", (err) => {
|
|
13959
|
+
if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
|
|
13960
|
+
attempt++;
|
|
13961
|
+
currentPort = requestedPort + attempt;
|
|
13962
|
+
setImmediate(tryListen);
|
|
13963
|
+
return;
|
|
13964
|
+
}
|
|
13965
|
+
if (err.code === "EADDRINUSE") {
|
|
13966
|
+
boundViewerPort = null;
|
|
13967
|
+
viewerSkipped = true;
|
|
13968
|
+
console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
|
|
13969
|
+
} else {
|
|
13970
|
+
boundViewerPort = null;
|
|
13971
|
+
viewerSkipped = true;
|
|
13972
|
+
console.error(`[agentmemory] Viewer error:`, err.message);
|
|
13973
|
+
}
|
|
13974
|
+
});
|
|
13975
|
+
tryListen();
|
|
13976
|
+
return server;
|
|
13977
|
+
}
|
|
13978
|
+
async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
|
|
13979
|
+
const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
|
|
13980
|
+
const headers = {};
|
|
13981
|
+
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
|
13982
|
+
const ct = req.headers["content-type"];
|
|
13983
|
+
if (ct) headers["Content-Type"] = ct;
|
|
13984
|
+
let body;
|
|
13985
|
+
if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
|
|
13986
|
+
const controller = new AbortController();
|
|
13987
|
+
const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
|
|
13988
|
+
let upstream;
|
|
13989
|
+
try {
|
|
13990
|
+
upstream = await fetch(upstreamUrl, {
|
|
13991
|
+
method,
|
|
13992
|
+
headers,
|
|
13993
|
+
body: body || void 0,
|
|
13994
|
+
signal: controller.signal
|
|
13995
|
+
});
|
|
13996
|
+
clearTimeout(fetchTimeout);
|
|
13997
|
+
} catch (err) {
|
|
13998
|
+
clearTimeout(fetchTimeout);
|
|
13999
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
14000
|
+
json(res, 504, { error: "upstream timeout" }, req);
|
|
14001
|
+
return;
|
|
14002
|
+
}
|
|
14003
|
+
throw err;
|
|
14004
|
+
}
|
|
14005
|
+
const cors = corsHeaders(req);
|
|
14006
|
+
const responseBody = await upstream.text();
|
|
14007
|
+
const responseHeaders = { ...cors };
|
|
14008
|
+
const upstreamCt = upstream.headers.get("content-type");
|
|
14009
|
+
if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
|
|
14010
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
14011
|
+
res.end(responseBody);
|
|
14012
|
+
}
|
|
14013
|
+
|
|
14014
|
+
//#endregion
|
|
14015
|
+
//#region src/triggers/api.ts
|
|
14016
|
+
function parseOptionalInt(raw) {
|
|
14017
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
14018
|
+
const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
|
|
14019
|
+
return Number.isFinite(n) ? n : void 0;
|
|
14020
|
+
}
|
|
14021
|
+
function checkAuth(req, secret) {
|
|
14022
|
+
if (!secret) return null;
|
|
14023
|
+
const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
|
|
14024
|
+
if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
|
|
14025
|
+
status_code: 401,
|
|
14026
|
+
body: { error: "unauthorized" }
|
|
14027
|
+
};
|
|
14028
|
+
return null;
|
|
14029
|
+
}
|
|
14030
|
+
function requireConfiguredSecret(secret, feature) {
|
|
14031
|
+
if (secret) return null;
|
|
14032
|
+
return {
|
|
14033
|
+
status_code: 503,
|
|
14034
|
+
body: { error: `${feature} requires AGENTMEMORY_SECRET` }
|
|
14035
|
+
};
|
|
14036
|
+
}
|
|
14037
|
+
function flagDisabledResponse(opts) {
|
|
14038
|
+
return {
|
|
14039
|
+
status_code: 503,
|
|
14040
|
+
body: opts
|
|
14041
|
+
};
|
|
14042
|
+
}
|
|
14043
|
+
function graphDisabledResponse() {
|
|
14044
|
+
return flagDisabledResponse({
|
|
14045
|
+
error: "Knowledge graph not enabled",
|
|
14046
|
+
flag: "GRAPH_EXTRACTION_ENABLED",
|
|
13349
14047
|
enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
|
|
13350
14048
|
docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
|
|
13351
14049
|
});
|
|
@@ -13398,7 +14096,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13398
14096
|
status_code: 200,
|
|
13399
14097
|
body: {
|
|
13400
14098
|
status: "ok",
|
|
13401
|
-
service: "agentmemory"
|
|
14099
|
+
service: "agentmemory",
|
|
14100
|
+
viewerPort: getBoundViewerPort(),
|
|
14101
|
+
viewerSkipped: getViewerSkipped()
|
|
13402
14102
|
}
|
|
13403
14103
|
}));
|
|
13404
14104
|
sdk.registerTrigger({
|
|
@@ -13493,7 +14193,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13493
14193
|
version: VERSION,
|
|
13494
14194
|
health: health || null,
|
|
13495
14195
|
functionMetrics,
|
|
13496
|
-
circuitBreaker
|
|
14196
|
+
circuitBreaker,
|
|
14197
|
+
viewerPort: getBoundViewerPort(),
|
|
14198
|
+
viewerSkipped: getViewerSkipped()
|
|
13497
14199
|
}
|
|
13498
14200
|
};
|
|
13499
14201
|
});
|
|
@@ -13746,13 +14448,18 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13746
14448
|
status_code: 400,
|
|
13747
14449
|
body: { error: "sessionId, project, and cwd are required non-empty strings" }
|
|
13748
14450
|
};
|
|
14451
|
+
const title = typeof body.title === "string" ? body.title.trim() : void 0;
|
|
14452
|
+
const agentId = (typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim().slice(0, 128) : void 0) ?? getAgentId();
|
|
13749
14453
|
const session = {
|
|
13750
14454
|
id: sessionId,
|
|
13751
14455
|
project,
|
|
13752
14456
|
cwd,
|
|
13753
14457
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13754
14458
|
status: "active",
|
|
13755
|
-
observationCount: 0
|
|
14459
|
+
observationCount: 0,
|
|
14460
|
+
...title ? { summary: title.slice(0, 200) } : {},
|
|
14461
|
+
...title ? { firstPrompt: title.slice(0, 200) } : {},
|
|
14462
|
+
...agentId ? { agentId } : {}
|
|
13756
14463
|
};
|
|
13757
14464
|
await kv.set(KV.sessions, sessionId, session);
|
|
13758
14465
|
return {
|
|
@@ -13939,9 +14646,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13939
14646
|
sdk.registerFunction("api::sessions", async (req) => {
|
|
13940
14647
|
const authErr = checkAuth(req, secret);
|
|
13941
14648
|
if (authErr) return authErr;
|
|
14649
|
+
const sessions = await kv.list(KV.sessions);
|
|
14650
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
14651
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
14652
|
+
const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
13942
14653
|
return {
|
|
13943
14654
|
status_code: 200,
|
|
13944
|
-
body: { sessions:
|
|
14655
|
+
body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
|
|
13945
14656
|
};
|
|
13946
14657
|
});
|
|
13947
14658
|
sdk.registerTrigger({
|
|
@@ -13960,9 +14671,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13960
14671
|
status_code: 400,
|
|
13961
14672
|
body: { error: "sessionId required" }
|
|
13962
14673
|
};
|
|
14674
|
+
const observations = await kv.list(KV.observations(sessionId));
|
|
14675
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
14676
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
14677
|
+
const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
13963
14678
|
return {
|
|
13964
14679
|
status_code: 200,
|
|
13965
|
-
body: { observations:
|
|
14680
|
+
body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
|
|
13966
14681
|
};
|
|
13967
14682
|
});
|
|
13968
14683
|
sdk.registerTrigger({
|
|
@@ -14238,11 +14953,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14238
14953
|
sdk.registerFunction("api::export", async (req) => {
|
|
14239
14954
|
const authErr = checkAuth(req, secret);
|
|
14240
14955
|
if (authErr) return authErr;
|
|
14956
|
+
const rawMax = req.query_params?.["maxSessions"];
|
|
14957
|
+
const rawOffset = req.query_params?.["offset"];
|
|
14958
|
+
const payload = {};
|
|
14959
|
+
if (typeof rawMax === "string") {
|
|
14960
|
+
const n = Number(rawMax);
|
|
14961
|
+
if (Number.isInteger(n) && n > 0) payload.maxSessions = n;
|
|
14962
|
+
}
|
|
14963
|
+
if (typeof rawOffset === "string") {
|
|
14964
|
+
const n = Number(rawOffset);
|
|
14965
|
+
if (Number.isInteger(n) && n >= 0) payload.offset = n;
|
|
14966
|
+
}
|
|
14241
14967
|
return {
|
|
14242
14968
|
status_code: 200,
|
|
14243
14969
|
body: await sdk.trigger({
|
|
14244
14970
|
function_id: "mem::export",
|
|
14245
|
-
payload
|
|
14971
|
+
payload
|
|
14246
14972
|
})
|
|
14247
14973
|
};
|
|
14248
14974
|
});
|
|
@@ -14728,9 +15454,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14728
15454
|
const authErr = checkAuth(req, secret);
|
|
14729
15455
|
if (authErr) return authErr;
|
|
14730
15456
|
const memories = await kv.list(KV.memories);
|
|
15457
|
+
const latest = req.query_params?.["latest"] === "true";
|
|
15458
|
+
const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
|
|
15459
|
+
const wildcardAgent = normalizedAgentId === "*";
|
|
15460
|
+
const explicitAgentId = normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0;
|
|
15461
|
+
const includeOrphans = req.query_params?.["includeOrphans"] === "true";
|
|
15462
|
+
const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
|
|
15463
|
+
let filtered = latest ? memories.filter((m) => m.isLatest) : memories;
|
|
15464
|
+
if (filterAgentId) filtered = filtered.filter((m) => m.agentId === filterAgentId || includeOrphans && m.agentId === void 0);
|
|
15465
|
+
if (req.query_params?.["count"] === "true") return {
|
|
15466
|
+
status_code: 200,
|
|
15467
|
+
body: {
|
|
15468
|
+
total: filtered.length,
|
|
15469
|
+
latestCount: filtered.filter((m) => m.isLatest).length
|
|
15470
|
+
}
|
|
15471
|
+
};
|
|
15472
|
+
const rawLimit = req.query_params?.["limit"];
|
|
15473
|
+
const rawOffset = req.query_params?.["offset"];
|
|
15474
|
+
const parsedLimit = typeof rawLimit === "string" ? Number(rawLimit) : NaN;
|
|
15475
|
+
const parsedOffset = typeof rawOffset === "string" ? Number(rawOffset) : NaN;
|
|
15476
|
+
const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 5e3) : void 0;
|
|
15477
|
+
const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
|
|
14731
15478
|
return {
|
|
14732
15479
|
status_code: 200,
|
|
14733
|
-
body: {
|
|
15480
|
+
body: {
|
|
15481
|
+
memories: limit !== void 0 ? filtered.slice(offset, offset + limit) : filtered,
|
|
15482
|
+
total: filtered.length,
|
|
15483
|
+
offset,
|
|
15484
|
+
limit: limit ?? null
|
|
15485
|
+
}
|
|
14734
15486
|
};
|
|
14735
15487
|
});
|
|
14736
15488
|
sdk.registerTrigger({
|
|
@@ -18343,201 +19095,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
18343
19095
|
});
|
|
18344
19096
|
}
|
|
18345
19097
|
|
|
18346
|
-
//#endregion
|
|
18347
|
-
//#region src/viewer/server.ts
|
|
18348
|
-
function loadViewerFavicon() {
|
|
18349
|
-
const base = dirname(fileURLToPath(import.meta.url));
|
|
18350
|
-
const candidates = [
|
|
18351
|
-
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
18352
|
-
join(base, "..", "viewer", "favicon.svg"),
|
|
18353
|
-
join(base, "viewer", "favicon.svg")
|
|
18354
|
-
];
|
|
18355
|
-
for (const path of candidates) try {
|
|
18356
|
-
return readFileSync(path);
|
|
18357
|
-
} catch {}
|
|
18358
|
-
return null;
|
|
18359
|
-
}
|
|
18360
|
-
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());
|
|
18361
|
-
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
18362
|
-
function buildAllowedHosts(origins, listenPort) {
|
|
18363
|
-
const hosts = /* @__PURE__ */ new Set();
|
|
18364
|
-
for (const o of origins) try {
|
|
18365
|
-
const parsed = new URL(o);
|
|
18366
|
-
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
18367
|
-
} catch {}
|
|
18368
|
-
hosts.add(`localhost:${listenPort}`);
|
|
18369
|
-
hosts.add(`127.0.0.1:${listenPort}`);
|
|
18370
|
-
hosts.add(`[::1]:${listenPort}`);
|
|
18371
|
-
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
18372
|
-
return hosts;
|
|
18373
|
-
}
|
|
18374
|
-
function isHostAllowed(headerHost, allowed) {
|
|
18375
|
-
if (typeof headerHost !== "string") return false;
|
|
18376
|
-
const lower = headerHost.toLowerCase().trim();
|
|
18377
|
-
if (!lower) return false;
|
|
18378
|
-
return allowed.has(lower);
|
|
18379
|
-
}
|
|
18380
|
-
function corsHeaders(req) {
|
|
18381
|
-
const origin = req.headers.origin || "";
|
|
18382
|
-
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
18383
|
-
return {
|
|
18384
|
-
"Access-Control-Allow-Origin": allowed,
|
|
18385
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
18386
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
18387
|
-
Vary: "Origin"
|
|
18388
|
-
};
|
|
18389
|
-
}
|
|
18390
|
-
function json(res, status, data, req) {
|
|
18391
|
-
const body = JSON.stringify(data);
|
|
18392
|
-
const cors = req ? corsHeaders(req) : {
|
|
18393
|
-
"Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
|
|
18394
|
-
Vary: "Origin"
|
|
18395
|
-
};
|
|
18396
|
-
res.writeHead(status, {
|
|
18397
|
-
...cors,
|
|
18398
|
-
"Content-Type": "application/json"
|
|
18399
|
-
});
|
|
18400
|
-
res.end(body);
|
|
18401
|
-
}
|
|
18402
|
-
function readBody(req) {
|
|
18403
|
-
return new Promise((resolve, reject) => {
|
|
18404
|
-
let data = "";
|
|
18405
|
-
let size = 0;
|
|
18406
|
-
req.on("data", (chunk) => {
|
|
18407
|
-
size += chunk.length;
|
|
18408
|
-
if (size > 1e6) {
|
|
18409
|
-
req.destroy();
|
|
18410
|
-
reject(/* @__PURE__ */ new Error("too large"));
|
|
18411
|
-
return;
|
|
18412
|
-
}
|
|
18413
|
-
data += chunk.toString();
|
|
18414
|
-
});
|
|
18415
|
-
req.on("end", () => resolve(data));
|
|
18416
|
-
req.on("error", reject);
|
|
18417
|
-
});
|
|
18418
|
-
}
|
|
18419
|
-
const MAX_VIEWER_PORT_RETRIES = 10;
|
|
18420
|
-
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
18421
|
-
const resolvedRestPort = restPort ?? port - 2;
|
|
18422
|
-
const requestedPort = port;
|
|
18423
|
-
let allowedHosts = null;
|
|
18424
|
-
const server = createServer(async (req, res) => {
|
|
18425
|
-
if (!allowedHosts) {
|
|
18426
|
-
const addr = server.address();
|
|
18427
|
-
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
18428
|
-
}
|
|
18429
|
-
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
18430
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
18431
|
-
res.end("forbidden host");
|
|
18432
|
-
return;
|
|
18433
|
-
}
|
|
18434
|
-
const raw = req.url || "/";
|
|
18435
|
-
const qIdx = raw.indexOf("?");
|
|
18436
|
-
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
18437
|
-
const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
|
|
18438
|
-
const method = req.method || "GET";
|
|
18439
|
-
if (method === "OPTIONS") {
|
|
18440
|
-
res.writeHead(204, {
|
|
18441
|
-
...corsHeaders(req),
|
|
18442
|
-
"Access-Control-Max-Age": "86400"
|
|
18443
|
-
});
|
|
18444
|
-
res.end();
|
|
18445
|
-
return;
|
|
18446
|
-
}
|
|
18447
|
-
if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
|
|
18448
|
-
const rendered = renderViewerDocument();
|
|
18449
|
-
if (rendered.found) {
|
|
18450
|
-
res.writeHead(200, {
|
|
18451
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
18452
|
-
"Content-Security-Policy": rendered.csp,
|
|
18453
|
-
"Cache-Control": "no-cache"
|
|
18454
|
-
});
|
|
18455
|
-
res.end(rendered.html);
|
|
18456
|
-
return;
|
|
18457
|
-
}
|
|
18458
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
18459
|
-
res.end("viewer not found");
|
|
18460
|
-
return;
|
|
18461
|
-
}
|
|
18462
|
-
if (method === "GET" && pathname === "/favicon.svg") {
|
|
18463
|
-
const favicon = loadViewerFavicon();
|
|
18464
|
-
if (favicon) {
|
|
18465
|
-
res.writeHead(200, {
|
|
18466
|
-
"Content-Type": "image/svg+xml",
|
|
18467
|
-
"Cache-Control": "public, max-age=3600"
|
|
18468
|
-
});
|
|
18469
|
-
res.end(favicon);
|
|
18470
|
-
return;
|
|
18471
|
-
}
|
|
18472
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
18473
|
-
res.end("favicon not found");
|
|
18474
|
-
return;
|
|
18475
|
-
}
|
|
18476
|
-
try {
|
|
18477
|
-
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
18478
|
-
} catch (err) {
|
|
18479
|
-
console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
|
|
18480
|
-
json(res, 502, { error: "upstream error" }, req);
|
|
18481
|
-
}
|
|
18482
|
-
});
|
|
18483
|
-
let attempt = 0;
|
|
18484
|
-
let currentPort = requestedPort;
|
|
18485
|
-
const tryListen = () => {
|
|
18486
|
-
server.listen(currentPort, "127.0.0.1");
|
|
18487
|
-
};
|
|
18488
|
-
server.on("listening", () => {
|
|
18489
|
-
if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
|
|
18490
|
-
else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
|
|
18491
|
-
});
|
|
18492
|
-
server.on("error", (err) => {
|
|
18493
|
-
if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
|
|
18494
|
-
attempt++;
|
|
18495
|
-
currentPort = requestedPort + attempt;
|
|
18496
|
-
setImmediate(tryListen);
|
|
18497
|
-
return;
|
|
18498
|
-
}
|
|
18499
|
-
if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
|
|
18500
|
-
else console.error(`[agentmemory] Viewer error:`, err.message);
|
|
18501
|
-
});
|
|
18502
|
-
tryListen();
|
|
18503
|
-
return server;
|
|
18504
|
-
}
|
|
18505
|
-
async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
|
|
18506
|
-
const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
|
|
18507
|
-
const headers = {};
|
|
18508
|
-
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
|
18509
|
-
const ct = req.headers["content-type"];
|
|
18510
|
-
if (ct) headers["Content-Type"] = ct;
|
|
18511
|
-
let body;
|
|
18512
|
-
if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
|
|
18513
|
-
const controller = new AbortController();
|
|
18514
|
-
const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
|
|
18515
|
-
let upstream;
|
|
18516
|
-
try {
|
|
18517
|
-
upstream = await fetch(upstreamUrl, {
|
|
18518
|
-
method,
|
|
18519
|
-
headers,
|
|
18520
|
-
body: body || void 0,
|
|
18521
|
-
signal: controller.signal
|
|
18522
|
-
});
|
|
18523
|
-
clearTimeout(fetchTimeout);
|
|
18524
|
-
} catch (err) {
|
|
18525
|
-
clearTimeout(fetchTimeout);
|
|
18526
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
18527
|
-
json(res, 504, { error: "upstream timeout" }, req);
|
|
18528
|
-
return;
|
|
18529
|
-
}
|
|
18530
|
-
throw err;
|
|
18531
|
-
}
|
|
18532
|
-
const cors = corsHeaders(req);
|
|
18533
|
-
const responseBody = await upstream.text();
|
|
18534
|
-
const responseHeaders = { ...cors };
|
|
18535
|
-
const upstreamCt = upstream.headers.get("content-type");
|
|
18536
|
-
if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
|
|
18537
|
-
res.writeHead(upstream.status, responseHeaders);
|
|
18538
|
-
res.end(responseBody);
|
|
18539
|
-
}
|
|
18540
|
-
|
|
18541
19098
|
//#endregion
|
|
18542
19099
|
//#region src/eval/metrics-store.ts
|
|
18543
19100
|
var MetricsStore = class {
|
|
@@ -18675,6 +19232,21 @@ function initMetrics(getMeter) {
|
|
|
18675
19232
|
|
|
18676
19233
|
//#endregion
|
|
18677
19234
|
//#region src/index.ts
|
|
19235
|
+
function workerPidfilePath() {
|
|
19236
|
+
return join(homedir(), ".agentmemory", "worker.pid");
|
|
19237
|
+
}
|
|
19238
|
+
function writeWorkerPidfile() {
|
|
19239
|
+
try {
|
|
19240
|
+
const p = workerPidfilePath();
|
|
19241
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
19242
|
+
writeFileSync(p, `${process.pid}\n`, { encoding: "utf-8" });
|
|
19243
|
+
} catch {}
|
|
19244
|
+
}
|
|
19245
|
+
function clearWorkerPidfile() {
|
|
19246
|
+
try {
|
|
19247
|
+
unlinkSync(workerPidfilePath());
|
|
19248
|
+
} catch {}
|
|
19249
|
+
}
|
|
18678
19250
|
function hasGetMeter(sdk) {
|
|
18679
19251
|
return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
|
|
18680
19252
|
}
|
|
@@ -18715,6 +19287,7 @@ async function main() {
|
|
|
18715
19287
|
framework: "iii-sdk"
|
|
18716
19288
|
}
|
|
18717
19289
|
});
|
|
19290
|
+
writeWorkerPidfile();
|
|
18718
19291
|
const kv = new StateKV(sdk);
|
|
18719
19292
|
const secret = getEnvVar("AGENTMEMORY_SECRET");
|
|
18720
19293
|
const metricsStore = new MetricsStore(kv);
|
|
@@ -18810,6 +19383,7 @@ async function main() {
|
|
|
18810
19383
|
registerMcpEndpoints(sdk, kv, secret);
|
|
18811
19384
|
const healthMonitor = registerHealthMonitor(sdk, kv);
|
|
18812
19385
|
const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
|
|
19386
|
+
setIndexPersistence(indexPersistence);
|
|
18813
19387
|
const loaded = await indexPersistence.load().catch((err) => {
|
|
18814
19388
|
console.warn(`[agentmemory] Failed to load persisted index:`, err);
|
|
18815
19389
|
return null;
|
|
@@ -18827,23 +19401,22 @@ async function main() {
|
|
|
18827
19401
|
if (mismatches.length > 0) {
|
|
18828
19402
|
const sample = mismatches.slice(0, 5).map((m) => `${m.obsId} (dim=${m.dim})`).join(", ");
|
|
18829
19403
|
const distinct = Array.from(seenDimensions).sort((a, b) => a - b).join(", ");
|
|
18830
|
-
if (
|
|
19404
|
+
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.`);
|
|
18831
19405
|
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.`);
|
|
18832
19406
|
} else {
|
|
18833
19407
|
vectorIndex.restoreFrom(loaded.vector);
|
|
18834
19408
|
bootLog(`Loaded persisted vector index (${vectorIndex.size} vectors)`);
|
|
18835
19409
|
}
|
|
18836
19410
|
}
|
|
18837
|
-
if (bm25Index.size === 0) {
|
|
18838
|
-
const indexCount = await rebuildIndex(kv).catch((err) => {
|
|
18839
|
-
console.warn(`[agentmemory] Failed to rebuild search index:`, err);
|
|
18840
|
-
return 0;
|
|
18841
|
-
});
|
|
19411
|
+
if (bm25Index.size === 0) rebuildIndex(kv).then((indexCount) => {
|
|
18842
19412
|
if (indexCount > 0) {
|
|
18843
19413
|
bootLog(`Search index rebuilt: ${indexCount} entries`);
|
|
18844
19414
|
indexPersistence.scheduleSave();
|
|
18845
19415
|
}
|
|
18846
|
-
}
|
|
19416
|
+
}).catch((err) => {
|
|
19417
|
+
console.warn(`[agentmemory] Failed to rebuild search index:`, err);
|
|
19418
|
+
});
|
|
19419
|
+
else try {
|
|
18847
19420
|
const memories = await kv.list(KV.memories);
|
|
18848
19421
|
let backfilled = 0;
|
|
18849
19422
|
for (const memory of memories) {
|
|
@@ -18928,6 +19501,7 @@ async function main() {
|
|
|
18928
19501
|
console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
|
|
18929
19502
|
});
|
|
18930
19503
|
await sdk.shutdown();
|
|
19504
|
+
clearWorkerPidfile();
|
|
18931
19505
|
process.exit(0);
|
|
18932
19506
|
};
|
|
18933
19507
|
process.on("SIGINT", shutdown);
|
|
@@ -18940,4 +19514,4 @@ main().catch((err) => {
|
|
|
18940
19514
|
|
|
18941
19515
|
//#endregion
|
|
18942
19516
|
export { };
|
|
18943
|
-
//# sourceMappingURL=src-
|
|
19517
|
+
//# sourceMappingURL=src-gpTAJuBy.mjs.map
|