@agentmemory/agentmemory 0.9.17 → 0.9.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/AGENTS.md +2 -2
- package/README.md +19 -5
- package/dist/.env.example +6 -0
- package/dist/cli.mjs +19 -9
- package/dist/cli.mjs.map +1 -1
- package/dist/hooks/post-commit.d.mts +1 -0
- package/dist/hooks/post-commit.mjs +102 -0
- package/dist/hooks/post-commit.mjs.map +1 -0
- package/dist/index.mjs +537 -91
- package/dist/index.mjs.map +1 -1
- package/dist/{src-TiNuQ3Ub.mjs → src-2wwYDPGA.mjs} +503 -91
- package/dist/src-2wwYDPGA.mjs.map +1 -0
- package/dist/{standalone-BIXq6S80.mjs → standalone-DMLk7YxP.mjs} +17 -8
- package/dist/standalone-DMLk7YxP.mjs.map +1 -0
- package/dist/standalone.mjs +49 -7
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-BFKFKmYh.mjs → tools-registry-Dz8ssuMf.mjs} +34 -1
- package/dist/tools-registry-Dz8ssuMf.mjs.map +1 -0
- package/dist/viewer/favicon.svg +1 -0
- package/dist/viewer/index.html +190 -60
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +5 -1
- package/plugin/hooks/hooks.codex.json +4 -0
- package/plugin/scripts/post-commit.d.mts +1 -0
- package/plugin/scripts/post-commit.mjs +102 -0
- package/plugin/scripts/post-commit.mjs.map +1 -0
- package/plugin/skills/commit-context/SKILL.md +19 -0
- package/plugin/skills/commit-history/SKILL.md +20 -0
- package/plugin/skills/handoff/SKILL.md +21 -0
- package/plugin/skills/recap/SKILL.md +25 -0
- package/dist/src-TiNuQ3Ub.mjs.map +0 -1
- package/dist/standalone-BIXq6S80.mjs.map +0 -1
- package/dist/tools-registry-BFKFKmYh.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as STREAM, c as jaccardSimilarity, i as KV, n as bootLog, o as fingerprintId, r as logger, s as generateId, t as VERSION } from "./cli.mjs";
|
|
2
2
|
import { a as isManagedImagePath, getImageRefCount, i as getMaxBytes, n as IMAGES_DIR, r as deleteImage, t as withKeyedLock } from "./image-refs-R3tin9MR.mjs";
|
|
3
|
-
import { _ as loadTeamConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isGraphExtractionEnabled, f as loadClaudeBridgeConfig, g as loadSnapshotConfig, h as loadFallbackConfig, i as detectLlmProviderKind, l as isConsolidationEnabled, m as loadEmbeddingConfig, n as getVisibleTools, o as getEnvVar, p as loadConfig, r as detectEmbeddingProvider, t as getAllTools, u as isContextInjectionEnabled } from "./tools-registry-
|
|
3
|
+
import { _ as loadTeamConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isGraphExtractionEnabled, f as loadClaudeBridgeConfig, g as loadSnapshotConfig, h as loadFallbackConfig, i as detectLlmProviderKind, l as isConsolidationEnabled, m as loadEmbeddingConfig, n as getVisibleTools, o as getEnvVar, p as loadConfig, r as detectEmbeddingProvider, t as getAllTools, u as isContextInjectionEnabled } from "./tools-registry-Dz8ssuMf.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -104,6 +104,20 @@ var AnthropicProvider = class {
|
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/providers/_fetch.ts
|
|
109
|
+
function fetchWithTimeout(url, init, timeoutMs) {
|
|
110
|
+
const parsed = timeoutMs ?? Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10);
|
|
111
|
+
const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
|
|
112
|
+
const ctl = new AbortController();
|
|
113
|
+
const signal = init.signal ? AbortSignal.any([init.signal, ctl.signal]) : ctl.signal;
|
|
114
|
+
const t = setTimeout(() => ctl.abort(), ms);
|
|
115
|
+
return fetch(url, {
|
|
116
|
+
...init,
|
|
117
|
+
signal
|
|
118
|
+
}).finally(() => clearTimeout(t));
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
//#endregion
|
|
108
122
|
//#region src/providers/minimax.ts
|
|
109
123
|
/**
|
|
@@ -139,8 +153,7 @@ var MinimaxProvider = class {
|
|
|
139
153
|
return this.call(systemPrompt, userPrompt);
|
|
140
154
|
}
|
|
141
155
|
async call(systemPrompt, userPrompt) {
|
|
142
|
-
const
|
|
143
|
-
const response = await fetch(url, {
|
|
156
|
+
const response = await fetchWithTimeout(`${this.baseUrl}/v1/messages`, {
|
|
144
157
|
method: "POST",
|
|
145
158
|
headers: {
|
|
146
159
|
"Content-Type": "application/json",
|
|
@@ -184,11 +197,63 @@ var NoopProvider = class {
|
|
|
184
197
|
}
|
|
185
198
|
};
|
|
186
199
|
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/providers/_openai-shared.ts
|
|
202
|
+
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
203
|
+
const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
|
|
204
|
+
function detectAzure(baseUrl) {
|
|
205
|
+
try {
|
|
206
|
+
return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function azureStyleOf(baseUrl) {
|
|
212
|
+
try {
|
|
213
|
+
const u = new URL(baseUrl);
|
|
214
|
+
if (/\/openai\/deployments\//.test(u.pathname)) return "legacy";
|
|
215
|
+
return "v1";
|
|
216
|
+
} catch {
|
|
217
|
+
return "v1";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function legacyAzureUrl(baseUrl, path, apiVersion) {
|
|
221
|
+
const url = new URL(baseUrl);
|
|
222
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
223
|
+
url.searchParams.set("api-version", apiVersion);
|
|
224
|
+
return url.toString();
|
|
225
|
+
}
|
|
226
|
+
function v1AzureUrl(baseUrl, path) {
|
|
227
|
+
const url = new URL(baseUrl);
|
|
228
|
+
const route = path.startsWith("/") ? path.slice(1) : path;
|
|
229
|
+
url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
|
|
230
|
+
return url.toString();
|
|
231
|
+
}
|
|
232
|
+
function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
|
|
233
|
+
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
|
|
234
|
+
return `${baseUrl}/v1/chat/completions`;
|
|
235
|
+
}
|
|
236
|
+
function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
|
|
237
|
+
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
|
|
238
|
+
return `${baseUrl}/v1/embeddings`;
|
|
239
|
+
}
|
|
240
|
+
function buildAuthHeaders(apiKey, isAzure) {
|
|
241
|
+
if (isAzure) return {
|
|
242
|
+
"Content-Type": "application/json",
|
|
243
|
+
"api-key": apiKey
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
"Content-Type": "application/json",
|
|
247
|
+
Authorization: `Bearer ${apiKey}`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function normalizeBaseUrl(raw) {
|
|
251
|
+
return (raw || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
|
|
252
|
+
}
|
|
253
|
+
|
|
187
254
|
//#endregion
|
|
188
255
|
//#region src/providers/openai.ts
|
|
189
|
-
const DEFAULT_BASE_URL$1 = "https://api.openai.com";
|
|
190
256
|
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
191
|
-
const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
|
|
192
257
|
/**
|
|
193
258
|
* OpenAI-compatible LLM provider.
|
|
194
259
|
*
|
|
@@ -208,7 +273,12 @@ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
|
|
|
208
273
|
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
|
|
209
274
|
* OPENAI_MODEL — model name (default: gpt-4o-mini)
|
|
210
275
|
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
|
|
211
|
-
* OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (
|
|
276
|
+
* OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
|
|
277
|
+
* takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
|
|
278
|
+
* for back-compat with the v0.9.17 shipping name).
|
|
279
|
+
* AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
|
|
280
|
+
* raw-fetch LLM + embedding providers. Used when
|
|
281
|
+
* OPENAI_TIMEOUT_MS is not set. Default: 60000.
|
|
212
282
|
* MAX_TOKENS — max output tokens (default: from config or 4096)
|
|
213
283
|
* OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
|
|
214
284
|
* Passthrough for reasoning models (e.g. Ollama Cloud
|
|
@@ -230,9 +300,9 @@ var OpenAIProvider = class {
|
|
|
230
300
|
this.apiKey = apiKey;
|
|
231
301
|
this.model = model;
|
|
232
302
|
this.maxTokens = maxTokens;
|
|
233
|
-
this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL")
|
|
303
|
+
this.baseUrl = normalizeBaseUrl(baseURL || getEnvVar("OPENAI_BASE_URL"));
|
|
234
304
|
this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
|
|
235
|
-
this.timeoutMs =
|
|
305
|
+
this.timeoutMs = resolveTimeout();
|
|
236
306
|
this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
|
|
237
307
|
this.isAzure = detectAzure(this.baseUrl);
|
|
238
308
|
}
|
|
@@ -242,25 +312,8 @@ var OpenAIProvider = class {
|
|
|
242
312
|
async summarize(systemPrompt, userPrompt) {
|
|
243
313
|
return this.call(systemPrompt, userPrompt);
|
|
244
314
|
}
|
|
245
|
-
buildUrl() {
|
|
246
|
-
if (this.isAzure) {
|
|
247
|
-
const sep = this.baseUrl.includes("?") ? "&" : "?";
|
|
248
|
-
return `${this.baseUrl}/chat/completions${sep}api-version=${encodeURIComponent(this.azureApiVersion)}`;
|
|
249
|
-
}
|
|
250
|
-
return `${this.baseUrl}/v1/chat/completions`;
|
|
251
|
-
}
|
|
252
|
-
buildHeaders() {
|
|
253
|
-
if (this.isAzure) return {
|
|
254
|
-
"Content-Type": "application/json",
|
|
255
|
-
"api-key": this.apiKey
|
|
256
|
-
};
|
|
257
|
-
return {
|
|
258
|
-
"Content-Type": "application/json",
|
|
259
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
315
|
async call(systemPrompt, userPrompt) {
|
|
263
|
-
const url = this.
|
|
316
|
+
const url = buildChatUrl(this.baseUrl, this.isAzure, this.azureApiVersion);
|
|
264
317
|
const body = {
|
|
265
318
|
model: this.model,
|
|
266
319
|
max_tokens: this.maxTokens,
|
|
@@ -273,21 +326,16 @@ var OpenAIProvider = class {
|
|
|
273
326
|
}]
|
|
274
327
|
};
|
|
275
328
|
if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
|
|
276
|
-
const ac = new AbortController();
|
|
277
|
-
const t = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
278
329
|
let response;
|
|
279
330
|
try {
|
|
280
|
-
response = await
|
|
331
|
+
response = await fetchWithTimeout(url, {
|
|
281
332
|
method: "POST",
|
|
282
|
-
headers: this.
|
|
283
|
-
body: JSON.stringify(body)
|
|
284
|
-
|
|
285
|
-
});
|
|
333
|
+
headers: buildAuthHeaders(this.apiKey, this.isAzure),
|
|
334
|
+
body: JSON.stringify(body)
|
|
335
|
+
}, this.timeoutMs);
|
|
286
336
|
} catch (err) {
|
|
287
|
-
if (
|
|
337
|
+
if (err instanceof Error && err.name === "AbortError") throw new Error(`OpenAI API request timed out after ${this.timeoutMs}ms — set OPENAI_TIMEOUT_MS (or AGENTMEMORY_LLM_TIMEOUT_MS) to raise the bound or check the provider status.`);
|
|
288
338
|
throw err;
|
|
289
|
-
} finally {
|
|
290
|
-
clearTimeout(t);
|
|
291
339
|
}
|
|
292
340
|
if (!response.ok) {
|
|
293
341
|
const text = await response.text();
|
|
@@ -302,17 +350,19 @@ var OpenAIProvider = class {
|
|
|
302
350
|
throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
303
351
|
}
|
|
304
352
|
};
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
353
|
+
function resolveTimeout() {
|
|
354
|
+
const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
|
|
355
|
+
if (openai !== void 0) return openai;
|
|
356
|
+
const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
|
|
357
|
+
if (globalMs !== void 0) return globalMs;
|
|
358
|
+
return DEFAULT_TIMEOUT_MS;
|
|
359
|
+
}
|
|
360
|
+
function parsePositiveInt(raw) {
|
|
361
|
+
if (!raw) return void 0;
|
|
362
|
+
const trimmed = raw.trim();
|
|
363
|
+
if (!/^\d+$/.test(trimmed)) return void 0;
|
|
364
|
+
const n = Number(trimmed);
|
|
365
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
316
366
|
}
|
|
317
367
|
|
|
318
368
|
//#endregion
|
|
@@ -337,7 +387,7 @@ var OpenRouterProvider = class {
|
|
|
337
387
|
return this.call(systemPrompt, userPrompt);
|
|
338
388
|
}
|
|
339
389
|
async call(systemPrompt, userPrompt) {
|
|
340
|
-
const response = await
|
|
390
|
+
const response = await fetchWithTimeout(this.baseUrl, {
|
|
341
391
|
method: "POST",
|
|
342
392
|
headers: {
|
|
343
393
|
"Content-Type": "application/json",
|
|
@@ -506,7 +556,7 @@ var GeminiEmbeddingProvider = class {
|
|
|
506
556
|
const results = [];
|
|
507
557
|
for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
|
|
508
558
|
const chunk = texts.slice(i, i + BATCH_LIMIT);
|
|
509
|
-
const response = await
|
|
559
|
+
const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
|
|
510
560
|
method: "POST",
|
|
511
561
|
headers: { "Content-Type": "application/json" },
|
|
512
562
|
body: JSON.stringify({ requests: chunk.map((t) => ({
|
|
@@ -543,7 +593,6 @@ function l2Normalize(vec) {
|
|
|
543
593
|
|
|
544
594
|
//#endregion
|
|
545
595
|
//#region src/providers/embedding/openai.ts
|
|
546
|
-
const DEFAULT_BASE_URL = "https://api.openai.com";
|
|
547
596
|
const DEFAULT_MODEL$1 = "text-embedding-3-small";
|
|
548
597
|
/**
|
|
549
598
|
* Known OpenAI embedding model dimensions. Extend as new models ship.
|
|
@@ -567,11 +616,20 @@ function resolveDimensions(model, override) {
|
|
|
567
616
|
/**
|
|
568
617
|
* OpenAI-compatible embedding provider.
|
|
569
618
|
*
|
|
619
|
+
* Shares transport (URL builder, auth header, Azure detection) with
|
|
620
|
+
* the OpenAI LLM provider via `_openai-shared` (#371). Same env knobs
|
|
621
|
+
* pick up automatically: when `OPENAI_BASE_URL` points at an Azure
|
|
622
|
+
* resource (`.openai.azure.com` hostname) the embedding request uses
|
|
623
|
+
* Azure's `/embeddings` path with the `api-version` query param and
|
|
624
|
+
* `api-key` header instead of `Authorization: Bearer`.
|
|
625
|
+
*
|
|
570
626
|
* Required env vars:
|
|
571
627
|
* OPENAI_API_KEY — API key
|
|
572
628
|
*
|
|
573
629
|
* Optional:
|
|
574
|
-
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
|
|
630
|
+
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
|
|
631
|
+
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
|
|
632
|
+
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
|
|
575
633
|
* OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
|
|
576
634
|
* OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
|
|
577
635
|
* custom / self-hosted models not in the
|
|
@@ -583,25 +641,25 @@ var OpenAIEmbeddingProvider = class {
|
|
|
583
641
|
apiKey;
|
|
584
642
|
baseUrl;
|
|
585
643
|
model;
|
|
644
|
+
isAzure;
|
|
645
|
+
azureApiVersion;
|
|
586
646
|
constructor(apiKey) {
|
|
587
647
|
this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
|
|
588
648
|
if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
|
|
589
|
-
this.baseUrl = getEnvVar("OPENAI_BASE_URL")
|
|
649
|
+
this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
|
|
590
650
|
this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
|
|
591
651
|
this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
|
|
652
|
+
this.isAzure = detectAzure(this.baseUrl);
|
|
653
|
+
this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
|
|
592
654
|
}
|
|
593
655
|
async embed(text) {
|
|
594
656
|
const [result] = await this.embedBatch([text]);
|
|
595
657
|
return result;
|
|
596
658
|
}
|
|
597
659
|
async embedBatch(texts) {
|
|
598
|
-
const
|
|
599
|
-
const response = await fetch(url, {
|
|
660
|
+
const response = await fetchWithTimeout(buildEmbeddingUrl(this.baseUrl, this.isAzure, this.azureApiVersion), {
|
|
600
661
|
method: "POST",
|
|
601
|
-
headers:
|
|
602
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
603
|
-
"Content-Type": "application/json"
|
|
604
|
-
},
|
|
662
|
+
headers: buildAuthHeaders(this.apiKey, this.isAzure),
|
|
605
663
|
body: JSON.stringify({
|
|
606
664
|
model: this.model,
|
|
607
665
|
input: texts
|
|
@@ -631,7 +689,7 @@ var VoyageEmbeddingProvider = class {
|
|
|
631
689
|
return result;
|
|
632
690
|
}
|
|
633
691
|
async embedBatch(texts) {
|
|
634
|
-
const response = await
|
|
692
|
+
const response = await fetchWithTimeout(API_URL$2, {
|
|
635
693
|
method: "POST",
|
|
636
694
|
headers: {
|
|
637
695
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -667,7 +725,7 @@ var CohereEmbeddingProvider = class {
|
|
|
667
725
|
return result;
|
|
668
726
|
}
|
|
669
727
|
async embedBatch(texts) {
|
|
670
|
-
const response = await
|
|
728
|
+
const response = await fetchWithTimeout(API_URL$1, {
|
|
671
729
|
method: "POST",
|
|
672
730
|
headers: {
|
|
673
731
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -705,7 +763,7 @@ var OpenRouterEmbeddingProvider = class {
|
|
|
705
763
|
return result;
|
|
706
764
|
}
|
|
707
765
|
async embedBatch(texts) {
|
|
708
|
-
const response = await
|
|
766
|
+
const response = await fetchWithTimeout(API_URL, {
|
|
709
767
|
method: "POST",
|
|
710
768
|
headers: {
|
|
711
769
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -1133,7 +1191,7 @@ var GraphRetrieval = class {
|
|
|
1133
1191
|
const results = [];
|
|
1134
1192
|
const visitedObs = /* @__PURE__ */ new Set();
|
|
1135
1193
|
for (const startNode of matchingNodes) {
|
|
1136
|
-
const paths = this.
|
|
1194
|
+
const paths = this.dijkstraTraversal(startNode, allNodes, allEdges, maxDepth);
|
|
1137
1195
|
for (const path of paths) {
|
|
1138
1196
|
const lastNode = path[path.length - 1].node;
|
|
1139
1197
|
for (const obsId of lastNode.sourceObservationIds) {
|
|
@@ -1173,7 +1231,7 @@ var GraphRetrieval = class {
|
|
|
1173
1231
|
const results = [];
|
|
1174
1232
|
const visitedObs = new Set(obsIds);
|
|
1175
1233
|
for (const node of linkedNodes) {
|
|
1176
|
-
const paths = this.
|
|
1234
|
+
const paths = this.dijkstraTraversal(node, allNodes, allEdges, maxDepth);
|
|
1177
1235
|
for (const path of paths) {
|
|
1178
1236
|
const lastNode = path[path.length - 1].node;
|
|
1179
1237
|
for (const obsId of lastNode.sourceObservationIds) {
|
|
@@ -1245,37 +1303,104 @@ var GraphRetrieval = class {
|
|
|
1245
1303
|
}
|
|
1246
1304
|
return latest;
|
|
1247
1305
|
}
|
|
1248
|
-
|
|
1249
|
-
const
|
|
1250
|
-
const
|
|
1251
|
-
const
|
|
1306
|
+
dijkstraTraversal(startNode, allNodes, allEdges, maxDepth) {
|
|
1307
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
1308
|
+
for (const n of allNodes) nodeIndex.set(n.id, n);
|
|
1309
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1310
|
+
for (const edge of allEdges) {
|
|
1311
|
+
const a = edge.sourceNodeId;
|
|
1312
|
+
const b = edge.targetNodeId;
|
|
1313
|
+
if (!adjacency.has(a)) adjacency.set(a, []);
|
|
1314
|
+
if (!adjacency.has(b)) adjacency.set(b, []);
|
|
1315
|
+
adjacency.get(a).push({
|
|
1316
|
+
neighborId: b,
|
|
1317
|
+
edge
|
|
1318
|
+
});
|
|
1319
|
+
adjacency.get(b).push({
|
|
1320
|
+
neighborId: a,
|
|
1321
|
+
edge
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
const dist = /* @__PURE__ */ new Map();
|
|
1325
|
+
const pathTo = /* @__PURE__ */ new Map();
|
|
1326
|
+
dist.set(startNode.id, 0);
|
|
1327
|
+
pathTo.set(startNode.id, [{ node: startNode }]);
|
|
1328
|
+
const heap = new MinHeap((a, b) => a.cost - b.cost);
|
|
1329
|
+
heap.push({
|
|
1252
1330
|
nodeId: startNode.id,
|
|
1253
1331
|
depth: 0,
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
paths.push(path);
|
|
1332
|
+
cost: 0
|
|
1333
|
+
});
|
|
1334
|
+
while (heap.size() > 0) {
|
|
1335
|
+
const { nodeId, depth, cost } = heap.pop();
|
|
1336
|
+
if (cost > (dist.get(nodeId) ?? Infinity)) continue;
|
|
1260
1337
|
if (depth >= maxDepth) continue;
|
|
1261
|
-
const
|
|
1262
|
-
for (const edge of
|
|
1263
|
-
const
|
|
1264
|
-
if (visited.has(nextId)) continue;
|
|
1265
|
-
visited.add(nextId);
|
|
1266
|
-
const nextNode = allNodes.find((n) => n.id === nextId);
|
|
1338
|
+
const neighbors = adjacency.get(nodeId) ?? [];
|
|
1339
|
+
for (const { neighborId, edge } of neighbors) {
|
|
1340
|
+
const nextNode = nodeIndex.get(neighborId);
|
|
1267
1341
|
if (!nextNode) continue;
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1342
|
+
const newCost = cost + 1 / Math.max(edge.weight, .01);
|
|
1343
|
+
if (newCost < (dist.get(neighborId) ?? Infinity)) {
|
|
1344
|
+
dist.set(neighborId, newCost);
|
|
1345
|
+
pathTo.set(neighborId, [...pathTo.get(nodeId), {
|
|
1272
1346
|
node: nextNode,
|
|
1273
1347
|
edge
|
|
1274
|
-
}]
|
|
1275
|
-
|
|
1348
|
+
}]);
|
|
1349
|
+
heap.push({
|
|
1350
|
+
nodeId: neighborId,
|
|
1351
|
+
depth: depth + 1,
|
|
1352
|
+
cost: newCost
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1276
1355
|
}
|
|
1277
1356
|
}
|
|
1278
|
-
|
|
1357
|
+
pathTo.delete(startNode.id);
|
|
1358
|
+
return Array.from(pathTo.values());
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
var MinHeap = class {
|
|
1362
|
+
heap = [];
|
|
1363
|
+
constructor(compare) {
|
|
1364
|
+
this.compare = compare;
|
|
1365
|
+
}
|
|
1366
|
+
size() {
|
|
1367
|
+
return this.heap.length;
|
|
1368
|
+
}
|
|
1369
|
+
push(value) {
|
|
1370
|
+
this.heap.push(value);
|
|
1371
|
+
this.bubbleUp(this.heap.length - 1);
|
|
1372
|
+
}
|
|
1373
|
+
pop() {
|
|
1374
|
+
if (this.heap.length === 0) return void 0;
|
|
1375
|
+
const top = this.heap[0];
|
|
1376
|
+
const last = this.heap.pop();
|
|
1377
|
+
if (this.heap.length > 0) {
|
|
1378
|
+
this.heap[0] = last;
|
|
1379
|
+
this.sinkDown(0);
|
|
1380
|
+
}
|
|
1381
|
+
return top;
|
|
1382
|
+
}
|
|
1383
|
+
bubbleUp(i) {
|
|
1384
|
+
while (i > 0) {
|
|
1385
|
+
const parent = i - 1 >> 1;
|
|
1386
|
+
if (this.compare(this.heap[i], this.heap[parent]) < 0) {
|
|
1387
|
+
[this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
|
|
1388
|
+
i = parent;
|
|
1389
|
+
} else break;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
sinkDown(i) {
|
|
1393
|
+
const n = this.heap.length;
|
|
1394
|
+
while (true) {
|
|
1395
|
+
const left = 2 * i + 1;
|
|
1396
|
+
const right = 2 * i + 2;
|
|
1397
|
+
let smallest = i;
|
|
1398
|
+
if (left < n && this.compare(this.heap[left], this.heap[smallest]) < 0) smallest = left;
|
|
1399
|
+
if (right < n && this.compare(this.heap[right], this.heap[smallest]) < 0) smallest = right;
|
|
1400
|
+
if (smallest === i) break;
|
|
1401
|
+
[this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
|
|
1402
|
+
i = smallest;
|
|
1403
|
+
}
|
|
1279
1404
|
}
|
|
1280
1405
|
};
|
|
1281
1406
|
|
|
@@ -4095,7 +4220,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
|
|
|
4095
4220
|
sdk.registerFunction("mem::context", async (data) => {
|
|
4096
4221
|
const budget = data.budget || tokenBudget;
|
|
4097
4222
|
const blocks = [];
|
|
4098
|
-
const [pinnedSlots, profile] = await Promise.all([
|
|
4223
|
+
const [pinnedSlots, profile, lessons] = await Promise.all([
|
|
4224
|
+
isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
|
|
4225
|
+
kv.get(KV.profiles, data.project).catch(() => null),
|
|
4226
|
+
kv.list(KV.lessons).catch(() => [])
|
|
4227
|
+
]);
|
|
4099
4228
|
const slotContent = renderPinnedContext(pinnedSlots);
|
|
4100
4229
|
if (slotContent) blocks.push({
|
|
4101
4230
|
type: "memory",
|
|
@@ -4119,6 +4248,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
|
|
|
4119
4248
|
});
|
|
4120
4249
|
}
|
|
4121
4250
|
}
|
|
4251
|
+
const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
|
|
4252
|
+
const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
|
|
4253
|
+
return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
|
|
4254
|
+
}).slice(0, 10);
|
|
4255
|
+
if (relevantLessons.length > 0) {
|
|
4256
|
+
const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
|
|
4257
|
+
const mostRecent = relevantLessons.reduce((acc, l) => {
|
|
4258
|
+
const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
|
|
4259
|
+
return t > acc ? t : acc;
|
|
4260
|
+
}, 0);
|
|
4261
|
+
blocks.push({
|
|
4262
|
+
type: "memory",
|
|
4263
|
+
content: lessonsContent,
|
|
4264
|
+
tokens: estimateTokens$1(lessonsContent),
|
|
4265
|
+
recency: mostRecent,
|
|
4266
|
+
sourceIds: relevantLessons.map((l) => l.id)
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4122
4269
|
const sessions = (await kv.list(KV.sessions)).filter((s) => s.project === data.project && s.id !== data.sessionId).sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()).slice(0, 10);
|
|
4123
4270
|
const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
|
|
4124
4271
|
const sessionsNeedingObs = [];
|
|
@@ -4941,6 +5088,46 @@ const DEFAULTS$1 = {
|
|
|
4941
5088
|
lowImportanceThreshold: 3,
|
|
4942
5089
|
maxObservationsPerProject: 1e4
|
|
4943
5090
|
};
|
|
5091
|
+
function isValidRecoveryResult(result) {
|
|
5092
|
+
if (!result || typeof result !== "object") return false;
|
|
5093
|
+
if (!("success" in result)) return true;
|
|
5094
|
+
return result.success !== false;
|
|
5095
|
+
}
|
|
5096
|
+
function isCompressedObservation(observation) {
|
|
5097
|
+
return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
|
|
5098
|
+
}
|
|
5099
|
+
async function recoverStaleSession(sdk, sessionId) {
|
|
5100
|
+
try {
|
|
5101
|
+
const result = await sdk.trigger({
|
|
5102
|
+
function_id: "event::session::stopped",
|
|
5103
|
+
payload: { sessionId }
|
|
5104
|
+
});
|
|
5105
|
+
if (!isValidRecoveryResult(result)) {
|
|
5106
|
+
logger.warn("Stale session recovery failed", {
|
|
5107
|
+
sessionId,
|
|
5108
|
+
result
|
|
5109
|
+
});
|
|
5110
|
+
return false;
|
|
5111
|
+
}
|
|
5112
|
+
return true;
|
|
5113
|
+
} catch (err) {
|
|
5114
|
+
logger.warn("Stale session recovery failed", {
|
|
5115
|
+
sessionId,
|
|
5116
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5117
|
+
});
|
|
5118
|
+
return false;
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
async function runRecoveredSessionConsolidation(sdk) {
|
|
5122
|
+
try {
|
|
5123
|
+
await sdk.trigger({
|
|
5124
|
+
function_id: "mem::consolidate-pipeline",
|
|
5125
|
+
payload: { tier: "all" }
|
|
5126
|
+
});
|
|
5127
|
+
} catch (err) {
|
|
5128
|
+
logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
4944
5131
|
function registerEvictFunction(sdk, kv) {
|
|
4945
5132
|
sdk.registerFunction("mem::evict", async (data) => {
|
|
4946
5133
|
const dryRun = data?.dryRun ?? false;
|
|
@@ -4959,6 +5146,7 @@ function registerEvictFunction(sdk, kv) {
|
|
|
4959
5146
|
nonLatestMemories: 0,
|
|
4960
5147
|
dryRun
|
|
4961
5148
|
};
|
|
5149
|
+
let recoveredStaleSessions = 0;
|
|
4962
5150
|
const sessions = await kv.list(KV.sessions).catch(() => []);
|
|
4963
5151
|
const summaries = await kv.list(KV.summaries).catch(() => []);
|
|
4964
5152
|
const summaryIds = new Set(summaries.map((s) => s.sessionId));
|
|
@@ -4966,6 +5154,23 @@ function registerEvictFunction(sdk, kv) {
|
|
|
4966
5154
|
if (!session.startedAt) continue;
|
|
4967
5155
|
if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
|
|
4968
5156
|
else {
|
|
5157
|
+
const observations = await kv.list(KV.observations(session.id)).catch((err) => {
|
|
5158
|
+
logger.warn("Stale session observation scan failed", {
|
|
5159
|
+
sessionId: session.id,
|
|
5160
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5161
|
+
});
|
|
5162
|
+
return null;
|
|
5163
|
+
});
|
|
5164
|
+
if (!observations) continue;
|
|
5165
|
+
let recovered = false;
|
|
5166
|
+
if (observations.some(isCompressedObservation)) {
|
|
5167
|
+
recovered = await recoverStaleSession(sdk, session.id);
|
|
5168
|
+
if (!recovered) continue;
|
|
5169
|
+
recoveredStaleSessions++;
|
|
5170
|
+
} else if (observations.length > 0) {
|
|
5171
|
+
logger.warn("Stale session has no compressed observations", { sessionId: session.id });
|
|
5172
|
+
continue;
|
|
5173
|
+
}
|
|
4969
5174
|
try {
|
|
4970
5175
|
await kv.delete(KV.sessions, session.id);
|
|
4971
5176
|
stats.staleSessions++;
|
|
@@ -4979,11 +5184,12 @@ function registerEvictFunction(sdk, kv) {
|
|
|
4979
5184
|
}
|
|
4980
5185
|
await recordAudit(kv, "delete", "mem::evict", [session.id], {
|
|
4981
5186
|
resource: "session",
|
|
4982
|
-
reason: "stale_session_without_summary",
|
|
5187
|
+
reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
|
|
4983
5188
|
dryRun
|
|
4984
5189
|
});
|
|
4985
5190
|
}
|
|
4986
5191
|
}
|
|
5192
|
+
if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
|
|
4987
5193
|
const projectObs = /* @__PURE__ */ new Map();
|
|
4988
5194
|
for (const session of sessions) {
|
|
4989
5195
|
const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
|
|
@@ -5793,7 +5999,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
5793
5999
|
"0.9.14",
|
|
5794
6000
|
"0.9.15",
|
|
5795
6001
|
"0.9.16",
|
|
5796
|
-
"0.9.17"
|
|
6002
|
+
"0.9.17",
|
|
6003
|
+
"0.9.18",
|
|
6004
|
+
"0.9.19"
|
|
5797
6005
|
]).has(importData.version)) return {
|
|
5798
6006
|
success: false,
|
|
5799
6007
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -13621,6 +13829,112 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13621
13829
|
middleware_function_ids: ["middleware::api-auth"]
|
|
13622
13830
|
}
|
|
13623
13831
|
});
|
|
13832
|
+
sdk.registerFunction("api::session::commit", async (req) => {
|
|
13833
|
+
const body = req.body ?? {};
|
|
13834
|
+
const sha = asNonEmptyString$1(body.sha);
|
|
13835
|
+
if (!sha) return {
|
|
13836
|
+
status_code: 400,
|
|
13837
|
+
body: { error: "sha is required and must be a non-empty string" }
|
|
13838
|
+
};
|
|
13839
|
+
const sessionId = asNonEmptyString$1(body.sessionId) ?? void 0;
|
|
13840
|
+
const branch = asNonEmptyString$1(body.branch) ?? void 0;
|
|
13841
|
+
const repo = asNonEmptyString$1(body.repo) ?? void 0;
|
|
13842
|
+
const message = asNonEmptyString$1(body.message) ?? void 0;
|
|
13843
|
+
const author = asNonEmptyString$1(body.author) ?? void 0;
|
|
13844
|
+
const authoredAt = asNonEmptyString$1(body.authoredAt) ?? void 0;
|
|
13845
|
+
const files = Array.isArray(body.files) ? body.files.filter((f) => typeof f === "string" && f.length > 0) : void 0;
|
|
13846
|
+
const link = await withKeyedLock(`commit:${sha}`, async () => {
|
|
13847
|
+
const existing = await kv.get(KV.commits, sha);
|
|
13848
|
+
const sessionSet = new Set(existing?.sessionIds ?? []);
|
|
13849
|
+
if (sessionId) sessionSet.add(sessionId);
|
|
13850
|
+
const merged = {
|
|
13851
|
+
sha,
|
|
13852
|
+
shortSha: existing?.shortSha ?? sha.slice(0, 7),
|
|
13853
|
+
branch: branch ?? existing?.branch,
|
|
13854
|
+
repo: repo ?? existing?.repo,
|
|
13855
|
+
message: message ?? existing?.message,
|
|
13856
|
+
author: author ?? existing?.author,
|
|
13857
|
+
authoredAt: authoredAt ?? existing?.authoredAt,
|
|
13858
|
+
files: files ?? existing?.files,
|
|
13859
|
+
sessionIds: Array.from(sessionSet),
|
|
13860
|
+
linkedAt: existing?.linkedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
13861
|
+
};
|
|
13862
|
+
await kv.set(KV.commits, sha, merged);
|
|
13863
|
+
return merged;
|
|
13864
|
+
});
|
|
13865
|
+
if (sessionId) await withKeyedLock(`session:${sessionId}`, async () => {
|
|
13866
|
+
const session = await kv.get(KV.sessions, sessionId);
|
|
13867
|
+
if (!session) return;
|
|
13868
|
+
const shaSet = new Set(session.commitShas ?? []);
|
|
13869
|
+
shaSet.add(sha);
|
|
13870
|
+
session.commitShas = Array.from(shaSet);
|
|
13871
|
+
await kv.set(KV.sessions, sessionId, session);
|
|
13872
|
+
});
|
|
13873
|
+
return {
|
|
13874
|
+
status_code: 200,
|
|
13875
|
+
body: { commit: link }
|
|
13876
|
+
};
|
|
13877
|
+
});
|
|
13878
|
+
sdk.registerTrigger({
|
|
13879
|
+
type: "http",
|
|
13880
|
+
function_id: "api::session::commit",
|
|
13881
|
+
config: {
|
|
13882
|
+
api_path: "/agentmemory/session/commit",
|
|
13883
|
+
http_method: "POST",
|
|
13884
|
+
middleware_function_ids: ["middleware::api-auth"]
|
|
13885
|
+
}
|
|
13886
|
+
});
|
|
13887
|
+
sdk.registerFunction("api::session::by-commit", async (req) => {
|
|
13888
|
+
const authErr = checkAuth(req, secret);
|
|
13889
|
+
if (authErr) return authErr;
|
|
13890
|
+
const sha = asNonEmptyString$1(req.query_params?.["sha"]);
|
|
13891
|
+
if (!sha) return {
|
|
13892
|
+
status_code: 400,
|
|
13893
|
+
body: { error: "sha is required and must be a non-empty string" }
|
|
13894
|
+
};
|
|
13895
|
+
const link = await kv.get(KV.commits, sha);
|
|
13896
|
+
if (!link) return {
|
|
13897
|
+
status_code: 404,
|
|
13898
|
+
body: { error: "no sessions linked to this commit" }
|
|
13899
|
+
};
|
|
13900
|
+
return {
|
|
13901
|
+
status_code: 200,
|
|
13902
|
+
body: {
|
|
13903
|
+
commit: link,
|
|
13904
|
+
sessions: (await Promise.all((link.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null)
|
|
13905
|
+
}
|
|
13906
|
+
};
|
|
13907
|
+
});
|
|
13908
|
+
sdk.registerTrigger({
|
|
13909
|
+
type: "http",
|
|
13910
|
+
function_id: "api::session::by-commit",
|
|
13911
|
+
config: {
|
|
13912
|
+
api_path: "/agentmemory/session/by-commit",
|
|
13913
|
+
http_method: "GET",
|
|
13914
|
+
middleware_function_ids: ["middleware::api-auth"]
|
|
13915
|
+
}
|
|
13916
|
+
});
|
|
13917
|
+
sdk.registerFunction("api::commits", async (req) => {
|
|
13918
|
+
const authErr = checkAuth(req, secret);
|
|
13919
|
+
if (authErr) return authErr;
|
|
13920
|
+
const branch = asNonEmptyString$1(req.query_params?.["branch"]);
|
|
13921
|
+
const repo = asNonEmptyString$1(req.query_params?.["repo"]);
|
|
13922
|
+
const rawLimit = parseOptionalInt(req.query_params?.["limit"]);
|
|
13923
|
+
const limit = Math.max(1, Math.min(500, rawLimit ?? 100));
|
|
13924
|
+
return {
|
|
13925
|
+
status_code: 200,
|
|
13926
|
+
body: { commits: (await kv.list(KV.commits)).filter((c) => !branch || c.branch === branch).filter((c) => !repo || c.repo === repo).sort((a, b) => (a.linkedAt ?? "") < (b.linkedAt ?? "") ? 1 : -1).slice(0, limit) }
|
|
13927
|
+
};
|
|
13928
|
+
});
|
|
13929
|
+
sdk.registerTrigger({
|
|
13930
|
+
type: "http",
|
|
13931
|
+
function_id: "api::commits",
|
|
13932
|
+
config: {
|
|
13933
|
+
api_path: "/agentmemory/commits",
|
|
13934
|
+
http_method: "GET",
|
|
13935
|
+
middleware_function_ids: ["middleware::api-auth"]
|
|
13936
|
+
}
|
|
13937
|
+
});
|
|
13624
13938
|
sdk.registerFunction("api::sessions", async (req) => {
|
|
13625
13939
|
const authErr = checkAuth(req, secret);
|
|
13626
13940
|
if (authErr) return authErr;
|
|
@@ -17602,6 +17916,49 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
17602
17916
|
}] }
|
|
17603
17917
|
};
|
|
17604
17918
|
}
|
|
17919
|
+
case "memory_commit_lookup": {
|
|
17920
|
+
const sha = asNonEmptyString(args.sha);
|
|
17921
|
+
if (!sha) return {
|
|
17922
|
+
status_code: 400,
|
|
17923
|
+
body: { error: "sha required" }
|
|
17924
|
+
};
|
|
17925
|
+
const link = await kv.get(KV.commits, sha);
|
|
17926
|
+
if (!link) return {
|
|
17927
|
+
status_code: 200,
|
|
17928
|
+
body: { content: [{
|
|
17929
|
+
type: "text",
|
|
17930
|
+
text: JSON.stringify({
|
|
17931
|
+
commit: null,
|
|
17932
|
+
sessions: []
|
|
17933
|
+
}, null, 2)
|
|
17934
|
+
}] }
|
|
17935
|
+
};
|
|
17936
|
+
const linkRecord = link;
|
|
17937
|
+
const sessions = (await Promise.all((linkRecord.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null);
|
|
17938
|
+
return {
|
|
17939
|
+
status_code: 200,
|
|
17940
|
+
body: { content: [{
|
|
17941
|
+
type: "text",
|
|
17942
|
+
text: JSON.stringify({
|
|
17943
|
+
commit: link,
|
|
17944
|
+
sessions
|
|
17945
|
+
}, null, 2)
|
|
17946
|
+
}] }
|
|
17947
|
+
};
|
|
17948
|
+
}
|
|
17949
|
+
case "memory_commits": {
|
|
17950
|
+
const branch = typeof args.branch === "string" ? args.branch : void 0;
|
|
17951
|
+
const repo = typeof args.repo === "string" ? args.repo : void 0;
|
|
17952
|
+
const limit = Math.max(1, Math.min(500, asNumber(args.limit, 100) ?? 100));
|
|
17953
|
+
const filtered = (await kv.list(KV.commits)).filter((c) => !branch || c.branch === branch).filter((c) => !repo || c.repo === repo).sort((a, b) => (a.linkedAt ?? "") < (b.linkedAt ?? "") ? 1 : -1).slice(0, limit);
|
|
17954
|
+
return {
|
|
17955
|
+
status_code: 200,
|
|
17956
|
+
body: { content: [{
|
|
17957
|
+
type: "text",
|
|
17958
|
+
text: JSON.stringify({ commits: filtered }, null, 2)
|
|
17959
|
+
}] }
|
|
17960
|
+
};
|
|
17961
|
+
}
|
|
17605
17962
|
default: return {
|
|
17606
17963
|
status_code: 400,
|
|
17607
17964
|
body: { error: `Unknown tool: ${name}` }
|
|
@@ -17987,7 +18344,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
17987
18344
|
|
|
17988
18345
|
//#endregion
|
|
17989
18346
|
//#region src/viewer/server.ts
|
|
18347
|
+
function loadViewerFavicon() {
|
|
18348
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
18349
|
+
const candidates = [
|
|
18350
|
+
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
18351
|
+
join(base, "..", "viewer", "favicon.svg"),
|
|
18352
|
+
join(base, "viewer", "favicon.svg")
|
|
18353
|
+
];
|
|
18354
|
+
for (const path of candidates) try {
|
|
18355
|
+
return readFileSync(path);
|
|
18356
|
+
} catch {}
|
|
18357
|
+
return null;
|
|
18358
|
+
}
|
|
17990
18359
|
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());
|
|
18360
|
+
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
18361
|
+
function buildAllowedHosts(origins, listenPort) {
|
|
18362
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
18363
|
+
for (const o of origins) try {
|
|
18364
|
+
const parsed = new URL(o);
|
|
18365
|
+
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
18366
|
+
} catch {}
|
|
18367
|
+
hosts.add(`localhost:${listenPort}`);
|
|
18368
|
+
hosts.add(`127.0.0.1:${listenPort}`);
|
|
18369
|
+
hosts.add(`[::1]:${listenPort}`);
|
|
18370
|
+
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
18371
|
+
return hosts;
|
|
18372
|
+
}
|
|
18373
|
+
function isHostAllowed(headerHost, allowed) {
|
|
18374
|
+
if (typeof headerHost !== "string") return false;
|
|
18375
|
+
const lower = headerHost.toLowerCase().trim();
|
|
18376
|
+
if (!lower) return false;
|
|
18377
|
+
return allowed.has(lower);
|
|
18378
|
+
}
|
|
17991
18379
|
function corsHeaders(req) {
|
|
17992
18380
|
const origin = req.headers.origin || "";
|
|
17993
18381
|
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
@@ -18031,7 +18419,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
|
|
|
18031
18419
|
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
18032
18420
|
const resolvedRestPort = restPort ?? port - 2;
|
|
18033
18421
|
const requestedPort = port;
|
|
18422
|
+
let allowedHosts = null;
|
|
18034
18423
|
const server = createServer(async (req, res) => {
|
|
18424
|
+
if (!allowedHosts) {
|
|
18425
|
+
const addr = server.address();
|
|
18426
|
+
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
18427
|
+
}
|
|
18428
|
+
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
18429
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
18430
|
+
res.end("forbidden host");
|
|
18431
|
+
return;
|
|
18432
|
+
}
|
|
18035
18433
|
const raw = req.url || "/";
|
|
18036
18434
|
const qIdx = raw.indexOf("?");
|
|
18037
18435
|
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
@@ -18060,6 +18458,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
|
18060
18458
|
res.end("viewer not found");
|
|
18061
18459
|
return;
|
|
18062
18460
|
}
|
|
18461
|
+
if (method === "GET" && pathname === "/favicon.svg") {
|
|
18462
|
+
const favicon = loadViewerFavicon();
|
|
18463
|
+
if (favicon) {
|
|
18464
|
+
res.writeHead(200, {
|
|
18465
|
+
"Content-Type": "image/svg+xml",
|
|
18466
|
+
"Cache-Control": "public, max-age=3600"
|
|
18467
|
+
});
|
|
18468
|
+
res.end(favicon);
|
|
18469
|
+
return;
|
|
18470
|
+
}
|
|
18471
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
18472
|
+
res.end("favicon not found");
|
|
18473
|
+
return;
|
|
18474
|
+
}
|
|
18063
18475
|
try {
|
|
18064
18476
|
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
18065
18477
|
} catch (err) {
|
|
@@ -18459,7 +18871,7 @@ async function main() {
|
|
|
18459
18871
|
console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
|
|
18460
18872
|
}
|
|
18461
18873
|
bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
18462
|
-
bootLog(`REST API:
|
|
18874
|
+
bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
|
|
18463
18875
|
bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
|
|
18464
18876
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
18465
18877
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|
|
@@ -18527,4 +18939,4 @@ main().catch((err) => {
|
|
|
18527
18939
|
|
|
18528
18940
|
//#endregion
|
|
18529
18941
|
export { };
|
|
18530
|
-
//# sourceMappingURL=src-
|
|
18942
|
+
//# sourceMappingURL=src-2wwYDPGA.mjs.map
|