@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
package/dist/index.mjs
CHANGED
|
@@ -325,6 +325,20 @@ var AnthropicProvider = class {
|
|
|
325
325
|
}
|
|
326
326
|
};
|
|
327
327
|
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/providers/_fetch.ts
|
|
330
|
+
function fetchWithTimeout(url, init, timeoutMs) {
|
|
331
|
+
const parsed = timeoutMs ?? Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10);
|
|
332
|
+
const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
|
|
333
|
+
const ctl = new AbortController();
|
|
334
|
+
const signal = init.signal ? AbortSignal.any([init.signal, ctl.signal]) : ctl.signal;
|
|
335
|
+
const t = setTimeout(() => ctl.abort(), ms);
|
|
336
|
+
return fetch(url, {
|
|
337
|
+
...init,
|
|
338
|
+
signal
|
|
339
|
+
}).finally(() => clearTimeout(t));
|
|
340
|
+
}
|
|
341
|
+
|
|
328
342
|
//#endregion
|
|
329
343
|
//#region src/providers/minimax.ts
|
|
330
344
|
/**
|
|
@@ -360,8 +374,7 @@ var MinimaxProvider = class {
|
|
|
360
374
|
return this.call(systemPrompt, userPrompt);
|
|
361
375
|
}
|
|
362
376
|
async call(systemPrompt, userPrompt) {
|
|
363
|
-
const
|
|
364
|
-
const response = await fetch(url, {
|
|
377
|
+
const response = await fetchWithTimeout(`${this.baseUrl}/v1/messages`, {
|
|
365
378
|
method: "POST",
|
|
366
379
|
headers: {
|
|
367
380
|
"Content-Type": "application/json",
|
|
@@ -405,11 +418,63 @@ var NoopProvider = class {
|
|
|
405
418
|
}
|
|
406
419
|
};
|
|
407
420
|
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/providers/_openai-shared.ts
|
|
423
|
+
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
424
|
+
const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
|
|
425
|
+
function detectAzure(baseUrl) {
|
|
426
|
+
try {
|
|
427
|
+
return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
|
|
428
|
+
} catch {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function azureStyleOf(baseUrl) {
|
|
433
|
+
try {
|
|
434
|
+
const u = new URL(baseUrl);
|
|
435
|
+
if (/\/openai\/deployments\//.test(u.pathname)) return "legacy";
|
|
436
|
+
return "v1";
|
|
437
|
+
} catch {
|
|
438
|
+
return "v1";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function legacyAzureUrl(baseUrl, path, apiVersion) {
|
|
442
|
+
const url = new URL(baseUrl);
|
|
443
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
444
|
+
url.searchParams.set("api-version", apiVersion);
|
|
445
|
+
return url.toString();
|
|
446
|
+
}
|
|
447
|
+
function v1AzureUrl(baseUrl, path) {
|
|
448
|
+
const url = new URL(baseUrl);
|
|
449
|
+
const route = path.startsWith("/") ? path.slice(1) : path;
|
|
450
|
+
url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
|
|
451
|
+
return url.toString();
|
|
452
|
+
}
|
|
453
|
+
function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
|
|
454
|
+
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
|
|
455
|
+
return `${baseUrl}/v1/chat/completions`;
|
|
456
|
+
}
|
|
457
|
+
function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
|
|
458
|
+
if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
|
|
459
|
+
return `${baseUrl}/v1/embeddings`;
|
|
460
|
+
}
|
|
461
|
+
function buildAuthHeaders(apiKey, isAzure) {
|
|
462
|
+
if (isAzure) return {
|
|
463
|
+
"Content-Type": "application/json",
|
|
464
|
+
"api-key": apiKey
|
|
465
|
+
};
|
|
466
|
+
return {
|
|
467
|
+
"Content-Type": "application/json",
|
|
468
|
+
Authorization: `Bearer ${apiKey}`
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function normalizeBaseUrl(raw) {
|
|
472
|
+
return (raw || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
|
|
473
|
+
}
|
|
474
|
+
|
|
408
475
|
//#endregion
|
|
409
476
|
//#region src/providers/openai.ts
|
|
410
|
-
const DEFAULT_BASE_URL$1 = "https://api.openai.com";
|
|
411
477
|
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
412
|
-
const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
|
|
413
478
|
/**
|
|
414
479
|
* OpenAI-compatible LLM provider.
|
|
415
480
|
*
|
|
@@ -429,7 +494,12 @@ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
|
|
|
429
494
|
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
|
|
430
495
|
* OPENAI_MODEL — model name (default: gpt-4o-mini)
|
|
431
496
|
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
|
|
432
|
-
* OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (
|
|
497
|
+
* OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
|
|
498
|
+
* takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
|
|
499
|
+
* for back-compat with the v0.9.17 shipping name).
|
|
500
|
+
* AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
|
|
501
|
+
* raw-fetch LLM + embedding providers. Used when
|
|
502
|
+
* OPENAI_TIMEOUT_MS is not set. Default: 60000.
|
|
433
503
|
* MAX_TOKENS — max output tokens (default: from config or 4096)
|
|
434
504
|
* OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
|
|
435
505
|
* Passthrough for reasoning models (e.g. Ollama Cloud
|
|
@@ -451,9 +521,9 @@ var OpenAIProvider = class {
|
|
|
451
521
|
this.apiKey = apiKey;
|
|
452
522
|
this.model = model;
|
|
453
523
|
this.maxTokens = maxTokens;
|
|
454
|
-
this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL")
|
|
524
|
+
this.baseUrl = normalizeBaseUrl(baseURL || getEnvVar("OPENAI_BASE_URL"));
|
|
455
525
|
this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
|
|
456
|
-
this.timeoutMs =
|
|
526
|
+
this.timeoutMs = resolveTimeout();
|
|
457
527
|
this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
|
|
458
528
|
this.isAzure = detectAzure(this.baseUrl);
|
|
459
529
|
}
|
|
@@ -463,25 +533,8 @@ var OpenAIProvider = class {
|
|
|
463
533
|
async summarize(systemPrompt, userPrompt) {
|
|
464
534
|
return this.call(systemPrompt, userPrompt);
|
|
465
535
|
}
|
|
466
|
-
buildUrl() {
|
|
467
|
-
if (this.isAzure) {
|
|
468
|
-
const sep = this.baseUrl.includes("?") ? "&" : "?";
|
|
469
|
-
return `${this.baseUrl}/chat/completions${sep}api-version=${encodeURIComponent(this.azureApiVersion)}`;
|
|
470
|
-
}
|
|
471
|
-
return `${this.baseUrl}/v1/chat/completions`;
|
|
472
|
-
}
|
|
473
|
-
buildHeaders() {
|
|
474
|
-
if (this.isAzure) return {
|
|
475
|
-
"Content-Type": "application/json",
|
|
476
|
-
"api-key": this.apiKey
|
|
477
|
-
};
|
|
478
|
-
return {
|
|
479
|
-
"Content-Type": "application/json",
|
|
480
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
536
|
async call(systemPrompt, userPrompt) {
|
|
484
|
-
const url = this.
|
|
537
|
+
const url = buildChatUrl(this.baseUrl, this.isAzure, this.azureApiVersion);
|
|
485
538
|
const body = {
|
|
486
539
|
model: this.model,
|
|
487
540
|
max_tokens: this.maxTokens,
|
|
@@ -494,21 +547,16 @@ var OpenAIProvider = class {
|
|
|
494
547
|
}]
|
|
495
548
|
};
|
|
496
549
|
if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
|
|
497
|
-
const ac = new AbortController();
|
|
498
|
-
const t = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
499
550
|
let response;
|
|
500
551
|
try {
|
|
501
|
-
response = await
|
|
552
|
+
response = await fetchWithTimeout(url, {
|
|
502
553
|
method: "POST",
|
|
503
|
-
headers: this.
|
|
504
|
-
body: JSON.stringify(body)
|
|
505
|
-
|
|
506
|
-
});
|
|
554
|
+
headers: buildAuthHeaders(this.apiKey, this.isAzure),
|
|
555
|
+
body: JSON.stringify(body)
|
|
556
|
+
}, this.timeoutMs);
|
|
507
557
|
} catch (err) {
|
|
508
|
-
if (
|
|
558
|
+
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.`);
|
|
509
559
|
throw err;
|
|
510
|
-
} finally {
|
|
511
|
-
clearTimeout(t);
|
|
512
560
|
}
|
|
513
561
|
if (!response.ok) {
|
|
514
562
|
const text = await response.text();
|
|
@@ -523,17 +571,19 @@ var OpenAIProvider = class {
|
|
|
523
571
|
throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
524
572
|
}
|
|
525
573
|
};
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
574
|
+
function resolveTimeout() {
|
|
575
|
+
const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
|
|
576
|
+
if (openai !== void 0) return openai;
|
|
577
|
+
const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
|
|
578
|
+
if (globalMs !== void 0) return globalMs;
|
|
579
|
+
return DEFAULT_TIMEOUT_MS;
|
|
580
|
+
}
|
|
581
|
+
function parsePositiveInt(raw) {
|
|
582
|
+
if (!raw) return void 0;
|
|
583
|
+
const trimmed = raw.trim();
|
|
584
|
+
if (!/^\d+$/.test(trimmed)) return void 0;
|
|
585
|
+
const n = Number(trimmed);
|
|
586
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
537
587
|
}
|
|
538
588
|
|
|
539
589
|
//#endregion
|
|
@@ -558,7 +608,7 @@ var OpenRouterProvider = class {
|
|
|
558
608
|
return this.call(systemPrompt, userPrompt);
|
|
559
609
|
}
|
|
560
610
|
async call(systemPrompt, userPrompt) {
|
|
561
|
-
const response = await
|
|
611
|
+
const response = await fetchWithTimeout(this.baseUrl, {
|
|
562
612
|
method: "POST",
|
|
563
613
|
headers: {
|
|
564
614
|
"Content-Type": "application/json",
|
|
@@ -727,7 +777,7 @@ var GeminiEmbeddingProvider = class {
|
|
|
727
777
|
const results = [];
|
|
728
778
|
for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
|
|
729
779
|
const chunk = texts.slice(i, i + BATCH_LIMIT);
|
|
730
|
-
const response = await
|
|
780
|
+
const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
|
|
731
781
|
method: "POST",
|
|
732
782
|
headers: { "Content-Type": "application/json" },
|
|
733
783
|
body: JSON.stringify({ requests: chunk.map((t) => ({
|
|
@@ -764,7 +814,6 @@ function l2Normalize(vec) {
|
|
|
764
814
|
|
|
765
815
|
//#endregion
|
|
766
816
|
//#region src/providers/embedding/openai.ts
|
|
767
|
-
const DEFAULT_BASE_URL = "https://api.openai.com";
|
|
768
817
|
const DEFAULT_MODEL$1 = "text-embedding-3-small";
|
|
769
818
|
/**
|
|
770
819
|
* Known OpenAI embedding model dimensions. Extend as new models ship.
|
|
@@ -788,11 +837,20 @@ function resolveDimensions(model, override) {
|
|
|
788
837
|
/**
|
|
789
838
|
* OpenAI-compatible embedding provider.
|
|
790
839
|
*
|
|
840
|
+
* Shares transport (URL builder, auth header, Azure detection) with
|
|
841
|
+
* the OpenAI LLM provider via `_openai-shared` (#371). Same env knobs
|
|
842
|
+
* pick up automatically: when `OPENAI_BASE_URL` points at an Azure
|
|
843
|
+
* resource (`.openai.azure.com` hostname) the embedding request uses
|
|
844
|
+
* Azure's `/embeddings` path with the `api-version` query param and
|
|
845
|
+
* `api-key` header instead of `Authorization: Bearer`.
|
|
846
|
+
*
|
|
791
847
|
* Required env vars:
|
|
792
848
|
* OPENAI_API_KEY — API key
|
|
793
849
|
*
|
|
794
850
|
* Optional:
|
|
795
|
-
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
|
|
851
|
+
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
|
|
852
|
+
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
|
|
853
|
+
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
|
|
796
854
|
* OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
|
|
797
855
|
* OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
|
|
798
856
|
* custom / self-hosted models not in the
|
|
@@ -804,25 +862,25 @@ var OpenAIEmbeddingProvider = class {
|
|
|
804
862
|
apiKey;
|
|
805
863
|
baseUrl;
|
|
806
864
|
model;
|
|
865
|
+
isAzure;
|
|
866
|
+
azureApiVersion;
|
|
807
867
|
constructor(apiKey) {
|
|
808
868
|
this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
|
|
809
869
|
if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
|
|
810
|
-
this.baseUrl = getEnvVar("OPENAI_BASE_URL")
|
|
870
|
+
this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
|
|
811
871
|
this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
|
|
812
872
|
this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
|
|
873
|
+
this.isAzure = detectAzure(this.baseUrl);
|
|
874
|
+
this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
|
|
813
875
|
}
|
|
814
876
|
async embed(text) {
|
|
815
877
|
const [result] = await this.embedBatch([text]);
|
|
816
878
|
return result;
|
|
817
879
|
}
|
|
818
880
|
async embedBatch(texts) {
|
|
819
|
-
const
|
|
820
|
-
const response = await fetch(url, {
|
|
881
|
+
const response = await fetchWithTimeout(buildEmbeddingUrl(this.baseUrl, this.isAzure, this.azureApiVersion), {
|
|
821
882
|
method: "POST",
|
|
822
|
-
headers:
|
|
823
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
824
|
-
"Content-Type": "application/json"
|
|
825
|
-
},
|
|
883
|
+
headers: buildAuthHeaders(this.apiKey, this.isAzure),
|
|
826
884
|
body: JSON.stringify({
|
|
827
885
|
model: this.model,
|
|
828
886
|
input: texts
|
|
@@ -852,7 +910,7 @@ var VoyageEmbeddingProvider = class {
|
|
|
852
910
|
return result;
|
|
853
911
|
}
|
|
854
912
|
async embedBatch(texts) {
|
|
855
|
-
const response = await
|
|
913
|
+
const response = await fetchWithTimeout(API_URL$2, {
|
|
856
914
|
method: "POST",
|
|
857
915
|
headers: {
|
|
858
916
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -888,7 +946,7 @@ var CohereEmbeddingProvider = class {
|
|
|
888
946
|
return result;
|
|
889
947
|
}
|
|
890
948
|
async embedBatch(texts) {
|
|
891
|
-
const response = await
|
|
949
|
+
const response = await fetchWithTimeout(API_URL$1, {
|
|
892
950
|
method: "POST",
|
|
893
951
|
headers: {
|
|
894
952
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -926,7 +984,7 @@ var OpenRouterEmbeddingProvider = class {
|
|
|
926
984
|
return result;
|
|
927
985
|
}
|
|
928
986
|
async embedBatch(texts) {
|
|
929
|
-
const response = await
|
|
987
|
+
const response = await fetchWithTimeout(API_URL, {
|
|
930
988
|
method: "POST",
|
|
931
989
|
headers: {
|
|
932
990
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -1229,7 +1287,8 @@ const KV = {
|
|
|
1229
1287
|
imageEmbeddings: "mem:image-embeddings",
|
|
1230
1288
|
slots: "mem:slots",
|
|
1231
1289
|
globalSlots: "mem:slots:global",
|
|
1232
|
-
state: "mem:state"
|
|
1290
|
+
state: "mem:state",
|
|
1291
|
+
commits: "mem:commits"
|
|
1233
1292
|
};
|
|
1234
1293
|
const STREAM = {
|
|
1235
1294
|
name: "mem-live",
|
|
@@ -1423,7 +1482,7 @@ var GraphRetrieval = class {
|
|
|
1423
1482
|
const results = [];
|
|
1424
1483
|
const visitedObs = /* @__PURE__ */ new Set();
|
|
1425
1484
|
for (const startNode of matchingNodes) {
|
|
1426
|
-
const paths = this.
|
|
1485
|
+
const paths = this.dijkstraTraversal(startNode, allNodes, allEdges, maxDepth);
|
|
1427
1486
|
for (const path of paths) {
|
|
1428
1487
|
const lastNode = path[path.length - 1].node;
|
|
1429
1488
|
for (const obsId of lastNode.sourceObservationIds) {
|
|
@@ -1463,7 +1522,7 @@ var GraphRetrieval = class {
|
|
|
1463
1522
|
const results = [];
|
|
1464
1523
|
const visitedObs = new Set(obsIds);
|
|
1465
1524
|
for (const node of linkedNodes) {
|
|
1466
|
-
const paths = this.
|
|
1525
|
+
const paths = this.dijkstraTraversal(node, allNodes, allEdges, maxDepth);
|
|
1467
1526
|
for (const path of paths) {
|
|
1468
1527
|
const lastNode = path[path.length - 1].node;
|
|
1469
1528
|
for (const obsId of lastNode.sourceObservationIds) {
|
|
@@ -1535,37 +1594,104 @@ var GraphRetrieval = class {
|
|
|
1535
1594
|
}
|
|
1536
1595
|
return latest;
|
|
1537
1596
|
}
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
const
|
|
1541
|
-
const
|
|
1597
|
+
dijkstraTraversal(startNode, allNodes, allEdges, maxDepth) {
|
|
1598
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
1599
|
+
for (const n of allNodes) nodeIndex.set(n.id, n);
|
|
1600
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1601
|
+
for (const edge of allEdges) {
|
|
1602
|
+
const a = edge.sourceNodeId;
|
|
1603
|
+
const b = edge.targetNodeId;
|
|
1604
|
+
if (!adjacency.has(a)) adjacency.set(a, []);
|
|
1605
|
+
if (!adjacency.has(b)) adjacency.set(b, []);
|
|
1606
|
+
adjacency.get(a).push({
|
|
1607
|
+
neighborId: b,
|
|
1608
|
+
edge
|
|
1609
|
+
});
|
|
1610
|
+
adjacency.get(b).push({
|
|
1611
|
+
neighborId: a,
|
|
1612
|
+
edge
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
const dist = /* @__PURE__ */ new Map();
|
|
1616
|
+
const pathTo = /* @__PURE__ */ new Map();
|
|
1617
|
+
dist.set(startNode.id, 0);
|
|
1618
|
+
pathTo.set(startNode.id, [{ node: startNode }]);
|
|
1619
|
+
const heap = new MinHeap((a, b) => a.cost - b.cost);
|
|
1620
|
+
heap.push({
|
|
1542
1621
|
nodeId: startNode.id,
|
|
1543
1622
|
depth: 0,
|
|
1544
|
-
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
paths.push(path);
|
|
1623
|
+
cost: 0
|
|
1624
|
+
});
|
|
1625
|
+
while (heap.size() > 0) {
|
|
1626
|
+
const { nodeId, depth, cost } = heap.pop();
|
|
1627
|
+
if (cost > (dist.get(nodeId) ?? Infinity)) continue;
|
|
1550
1628
|
if (depth >= maxDepth) continue;
|
|
1551
|
-
const
|
|
1552
|
-
for (const edge of
|
|
1553
|
-
const
|
|
1554
|
-
if (visited.has(nextId)) continue;
|
|
1555
|
-
visited.add(nextId);
|
|
1556
|
-
const nextNode = allNodes.find((n) => n.id === nextId);
|
|
1629
|
+
const neighbors = adjacency.get(nodeId) ?? [];
|
|
1630
|
+
for (const { neighborId, edge } of neighbors) {
|
|
1631
|
+
const nextNode = nodeIndex.get(neighborId);
|
|
1557
1632
|
if (!nextNode) continue;
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1633
|
+
const newCost = cost + 1 / Math.max(edge.weight, .01);
|
|
1634
|
+
if (newCost < (dist.get(neighborId) ?? Infinity)) {
|
|
1635
|
+
dist.set(neighborId, newCost);
|
|
1636
|
+
pathTo.set(neighborId, [...pathTo.get(nodeId), {
|
|
1562
1637
|
node: nextNode,
|
|
1563
1638
|
edge
|
|
1564
|
-
}]
|
|
1565
|
-
|
|
1639
|
+
}]);
|
|
1640
|
+
heap.push({
|
|
1641
|
+
nodeId: neighborId,
|
|
1642
|
+
depth: depth + 1,
|
|
1643
|
+
cost: newCost
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1566
1646
|
}
|
|
1567
1647
|
}
|
|
1568
|
-
|
|
1648
|
+
pathTo.delete(startNode.id);
|
|
1649
|
+
return Array.from(pathTo.values());
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
var MinHeap = class {
|
|
1653
|
+
heap = [];
|
|
1654
|
+
constructor(compare) {
|
|
1655
|
+
this.compare = compare;
|
|
1656
|
+
}
|
|
1657
|
+
size() {
|
|
1658
|
+
return this.heap.length;
|
|
1659
|
+
}
|
|
1660
|
+
push(value) {
|
|
1661
|
+
this.heap.push(value);
|
|
1662
|
+
this.bubbleUp(this.heap.length - 1);
|
|
1663
|
+
}
|
|
1664
|
+
pop() {
|
|
1665
|
+
if (this.heap.length === 0) return void 0;
|
|
1666
|
+
const top = this.heap[0];
|
|
1667
|
+
const last = this.heap.pop();
|
|
1668
|
+
if (this.heap.length > 0) {
|
|
1669
|
+
this.heap[0] = last;
|
|
1670
|
+
this.sinkDown(0);
|
|
1671
|
+
}
|
|
1672
|
+
return top;
|
|
1673
|
+
}
|
|
1674
|
+
bubbleUp(i) {
|
|
1675
|
+
while (i > 0) {
|
|
1676
|
+
const parent = i - 1 >> 1;
|
|
1677
|
+
if (this.compare(this.heap[i], this.heap[parent]) < 0) {
|
|
1678
|
+
[this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
|
|
1679
|
+
i = parent;
|
|
1680
|
+
} else break;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
sinkDown(i) {
|
|
1684
|
+
const n = this.heap.length;
|
|
1685
|
+
while (true) {
|
|
1686
|
+
const left = 2 * i + 1;
|
|
1687
|
+
const right = 2 * i + 2;
|
|
1688
|
+
let smallest = i;
|
|
1689
|
+
if (left < n && this.compare(this.heap[left], this.heap[smallest]) < 0) smallest = left;
|
|
1690
|
+
if (right < n && this.compare(this.heap[right], this.heap[smallest]) < 0) smallest = right;
|
|
1691
|
+
if (smallest === i) break;
|
|
1692
|
+
[this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
|
|
1693
|
+
i = smallest;
|
|
1694
|
+
}
|
|
1569
1695
|
}
|
|
1570
1696
|
};
|
|
1571
1697
|
|
|
@@ -4543,7 +4669,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
|
|
|
4543
4669
|
sdk.registerFunction("mem::context", async (data) => {
|
|
4544
4670
|
const budget = data.budget || tokenBudget;
|
|
4545
4671
|
const blocks = [];
|
|
4546
|
-
const [pinnedSlots, profile] = await Promise.all([
|
|
4672
|
+
const [pinnedSlots, profile, lessons] = await Promise.all([
|
|
4673
|
+
isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
|
|
4674
|
+
kv.get(KV.profiles, data.project).catch(() => null),
|
|
4675
|
+
kv.list(KV.lessons).catch(() => [])
|
|
4676
|
+
]);
|
|
4547
4677
|
const slotContent = renderPinnedContext(pinnedSlots);
|
|
4548
4678
|
if (slotContent) blocks.push({
|
|
4549
4679
|
type: "memory",
|
|
@@ -4567,6 +4697,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
|
|
|
4567
4697
|
});
|
|
4568
4698
|
}
|
|
4569
4699
|
}
|
|
4700
|
+
const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
|
|
4701
|
+
const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
|
|
4702
|
+
return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
|
|
4703
|
+
}).slice(0, 10);
|
|
4704
|
+
if (relevantLessons.length > 0) {
|
|
4705
|
+
const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
|
|
4706
|
+
const mostRecent = relevantLessons.reduce((acc, l) => {
|
|
4707
|
+
const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
|
|
4708
|
+
return t > acc ? t : acc;
|
|
4709
|
+
}, 0);
|
|
4710
|
+
blocks.push({
|
|
4711
|
+
type: "memory",
|
|
4712
|
+
content: lessonsContent,
|
|
4713
|
+
tokens: estimateTokens$1(lessonsContent),
|
|
4714
|
+
recency: mostRecent,
|
|
4715
|
+
sourceIds: relevantLessons.map((l) => l.id)
|
|
4716
|
+
});
|
|
4717
|
+
}
|
|
4570
4718
|
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);
|
|
4571
4719
|
const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
|
|
4572
4720
|
const sessionsNeedingObs = [];
|
|
@@ -5389,6 +5537,46 @@ const DEFAULTS$1 = {
|
|
|
5389
5537
|
lowImportanceThreshold: 3,
|
|
5390
5538
|
maxObservationsPerProject: 1e4
|
|
5391
5539
|
};
|
|
5540
|
+
function isValidRecoveryResult(result) {
|
|
5541
|
+
if (!result || typeof result !== "object") return false;
|
|
5542
|
+
if (!("success" in result)) return true;
|
|
5543
|
+
return result.success !== false;
|
|
5544
|
+
}
|
|
5545
|
+
function isCompressedObservation(observation) {
|
|
5546
|
+
return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
|
|
5547
|
+
}
|
|
5548
|
+
async function recoverStaleSession(sdk, sessionId) {
|
|
5549
|
+
try {
|
|
5550
|
+
const result = await sdk.trigger({
|
|
5551
|
+
function_id: "event::session::stopped",
|
|
5552
|
+
payload: { sessionId }
|
|
5553
|
+
});
|
|
5554
|
+
if (!isValidRecoveryResult(result)) {
|
|
5555
|
+
logger.warn("Stale session recovery failed", {
|
|
5556
|
+
sessionId,
|
|
5557
|
+
result
|
|
5558
|
+
});
|
|
5559
|
+
return false;
|
|
5560
|
+
}
|
|
5561
|
+
return true;
|
|
5562
|
+
} catch (err) {
|
|
5563
|
+
logger.warn("Stale session recovery failed", {
|
|
5564
|
+
sessionId,
|
|
5565
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5566
|
+
});
|
|
5567
|
+
return false;
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
async function runRecoveredSessionConsolidation(sdk) {
|
|
5571
|
+
try {
|
|
5572
|
+
await sdk.trigger({
|
|
5573
|
+
function_id: "mem::consolidate-pipeline",
|
|
5574
|
+
payload: { tier: "all" }
|
|
5575
|
+
});
|
|
5576
|
+
} catch (err) {
|
|
5577
|
+
logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5392
5580
|
function registerEvictFunction(sdk, kv) {
|
|
5393
5581
|
sdk.registerFunction("mem::evict", async (data) => {
|
|
5394
5582
|
const dryRun = data?.dryRun ?? false;
|
|
@@ -5407,6 +5595,7 @@ function registerEvictFunction(sdk, kv) {
|
|
|
5407
5595
|
nonLatestMemories: 0,
|
|
5408
5596
|
dryRun
|
|
5409
5597
|
};
|
|
5598
|
+
let recoveredStaleSessions = 0;
|
|
5410
5599
|
const sessions = await kv.list(KV.sessions).catch(() => []);
|
|
5411
5600
|
const summaries = await kv.list(KV.summaries).catch(() => []);
|
|
5412
5601
|
const summaryIds = new Set(summaries.map((s) => s.sessionId));
|
|
@@ -5414,6 +5603,23 @@ function registerEvictFunction(sdk, kv) {
|
|
|
5414
5603
|
if (!session.startedAt) continue;
|
|
5415
5604
|
if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
|
|
5416
5605
|
else {
|
|
5606
|
+
const observations = await kv.list(KV.observations(session.id)).catch((err) => {
|
|
5607
|
+
logger.warn("Stale session observation scan failed", {
|
|
5608
|
+
sessionId: session.id,
|
|
5609
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5610
|
+
});
|
|
5611
|
+
return null;
|
|
5612
|
+
});
|
|
5613
|
+
if (!observations) continue;
|
|
5614
|
+
let recovered = false;
|
|
5615
|
+
if (observations.some(isCompressedObservation)) {
|
|
5616
|
+
recovered = await recoverStaleSession(sdk, session.id);
|
|
5617
|
+
if (!recovered) continue;
|
|
5618
|
+
recoveredStaleSessions++;
|
|
5619
|
+
} else if (observations.length > 0) {
|
|
5620
|
+
logger.warn("Stale session has no compressed observations", { sessionId: session.id });
|
|
5621
|
+
continue;
|
|
5622
|
+
}
|
|
5417
5623
|
try {
|
|
5418
5624
|
await kv.delete(KV.sessions, session.id);
|
|
5419
5625
|
stats.staleSessions++;
|
|
@@ -5427,11 +5633,12 @@ function registerEvictFunction(sdk, kv) {
|
|
|
5427
5633
|
}
|
|
5428
5634
|
await recordAudit(kv, "delete", "mem::evict", [session.id], {
|
|
5429
5635
|
resource: "session",
|
|
5430
|
-
reason: "stale_session_without_summary",
|
|
5636
|
+
reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
|
|
5431
5637
|
dryRun
|
|
5432
5638
|
});
|
|
5433
5639
|
}
|
|
5434
5640
|
}
|
|
5641
|
+
if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
|
|
5435
5642
|
const projectObs = /* @__PURE__ */ new Map();
|
|
5436
5643
|
for (const session of sessions) {
|
|
5437
5644
|
const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
|
|
@@ -6110,7 +6317,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
6110
6317
|
|
|
6111
6318
|
//#endregion
|
|
6112
6319
|
//#region src/version.ts
|
|
6113
|
-
const VERSION = "0.9.
|
|
6320
|
+
const VERSION = "0.9.19";
|
|
6114
6321
|
|
|
6115
6322
|
//#endregion
|
|
6116
6323
|
//#region src/functions/export-import.ts
|
|
@@ -6245,7 +6452,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
6245
6452
|
"0.9.14",
|
|
6246
6453
|
"0.9.15",
|
|
6247
6454
|
"0.9.16",
|
|
6248
|
-
"0.9.17"
|
|
6455
|
+
"0.9.17",
|
|
6456
|
+
"0.9.18",
|
|
6457
|
+
"0.9.19"
|
|
6249
6458
|
]).has(importData.version)) return {
|
|
6250
6459
|
success: false,
|
|
6251
6460
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -14073,6 +14282,112 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
14073
14282
|
middleware_function_ids: ["middleware::api-auth"]
|
|
14074
14283
|
}
|
|
14075
14284
|
});
|
|
14285
|
+
sdk.registerFunction("api::session::commit", async (req) => {
|
|
14286
|
+
const body = req.body ?? {};
|
|
14287
|
+
const sha = asNonEmptyString$1(body.sha);
|
|
14288
|
+
if (!sha) return {
|
|
14289
|
+
status_code: 400,
|
|
14290
|
+
body: { error: "sha is required and must be a non-empty string" }
|
|
14291
|
+
};
|
|
14292
|
+
const sessionId = asNonEmptyString$1(body.sessionId) ?? void 0;
|
|
14293
|
+
const branch = asNonEmptyString$1(body.branch) ?? void 0;
|
|
14294
|
+
const repo = asNonEmptyString$1(body.repo) ?? void 0;
|
|
14295
|
+
const message = asNonEmptyString$1(body.message) ?? void 0;
|
|
14296
|
+
const author = asNonEmptyString$1(body.author) ?? void 0;
|
|
14297
|
+
const authoredAt = asNonEmptyString$1(body.authoredAt) ?? void 0;
|
|
14298
|
+
const files = Array.isArray(body.files) ? body.files.filter((f) => typeof f === "string" && f.length > 0) : void 0;
|
|
14299
|
+
const link = await withKeyedLock(`commit:${sha}`, async () => {
|
|
14300
|
+
const existing = await kv.get(KV.commits, sha);
|
|
14301
|
+
const sessionSet = new Set(existing?.sessionIds ?? []);
|
|
14302
|
+
if (sessionId) sessionSet.add(sessionId);
|
|
14303
|
+
const merged = {
|
|
14304
|
+
sha,
|
|
14305
|
+
shortSha: existing?.shortSha ?? sha.slice(0, 7),
|
|
14306
|
+
branch: branch ?? existing?.branch,
|
|
14307
|
+
repo: repo ?? existing?.repo,
|
|
14308
|
+
message: message ?? existing?.message,
|
|
14309
|
+
author: author ?? existing?.author,
|
|
14310
|
+
authoredAt: authoredAt ?? existing?.authoredAt,
|
|
14311
|
+
files: files ?? existing?.files,
|
|
14312
|
+
sessionIds: Array.from(sessionSet),
|
|
14313
|
+
linkedAt: existing?.linkedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
14314
|
+
};
|
|
14315
|
+
await kv.set(KV.commits, sha, merged);
|
|
14316
|
+
return merged;
|
|
14317
|
+
});
|
|
14318
|
+
if (sessionId) await withKeyedLock(`session:${sessionId}`, async () => {
|
|
14319
|
+
const session = await kv.get(KV.sessions, sessionId);
|
|
14320
|
+
if (!session) return;
|
|
14321
|
+
const shaSet = new Set(session.commitShas ?? []);
|
|
14322
|
+
shaSet.add(sha);
|
|
14323
|
+
session.commitShas = Array.from(shaSet);
|
|
14324
|
+
await kv.set(KV.sessions, sessionId, session);
|
|
14325
|
+
});
|
|
14326
|
+
return {
|
|
14327
|
+
status_code: 200,
|
|
14328
|
+
body: { commit: link }
|
|
14329
|
+
};
|
|
14330
|
+
});
|
|
14331
|
+
sdk.registerTrigger({
|
|
14332
|
+
type: "http",
|
|
14333
|
+
function_id: "api::session::commit",
|
|
14334
|
+
config: {
|
|
14335
|
+
api_path: "/agentmemory/session/commit",
|
|
14336
|
+
http_method: "POST",
|
|
14337
|
+
middleware_function_ids: ["middleware::api-auth"]
|
|
14338
|
+
}
|
|
14339
|
+
});
|
|
14340
|
+
sdk.registerFunction("api::session::by-commit", async (req) => {
|
|
14341
|
+
const authErr = checkAuth(req, secret);
|
|
14342
|
+
if (authErr) return authErr;
|
|
14343
|
+
const sha = asNonEmptyString$1(req.query_params?.["sha"]);
|
|
14344
|
+
if (!sha) return {
|
|
14345
|
+
status_code: 400,
|
|
14346
|
+
body: { error: "sha is required and must be a non-empty string" }
|
|
14347
|
+
};
|
|
14348
|
+
const link = await kv.get(KV.commits, sha);
|
|
14349
|
+
if (!link) return {
|
|
14350
|
+
status_code: 404,
|
|
14351
|
+
body: { error: "no sessions linked to this commit" }
|
|
14352
|
+
};
|
|
14353
|
+
return {
|
|
14354
|
+
status_code: 200,
|
|
14355
|
+
body: {
|
|
14356
|
+
commit: link,
|
|
14357
|
+
sessions: (await Promise.all((link.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null)
|
|
14358
|
+
}
|
|
14359
|
+
};
|
|
14360
|
+
});
|
|
14361
|
+
sdk.registerTrigger({
|
|
14362
|
+
type: "http",
|
|
14363
|
+
function_id: "api::session::by-commit",
|
|
14364
|
+
config: {
|
|
14365
|
+
api_path: "/agentmemory/session/by-commit",
|
|
14366
|
+
http_method: "GET",
|
|
14367
|
+
middleware_function_ids: ["middleware::api-auth"]
|
|
14368
|
+
}
|
|
14369
|
+
});
|
|
14370
|
+
sdk.registerFunction("api::commits", async (req) => {
|
|
14371
|
+
const authErr = checkAuth(req, secret);
|
|
14372
|
+
if (authErr) return authErr;
|
|
14373
|
+
const branch = asNonEmptyString$1(req.query_params?.["branch"]);
|
|
14374
|
+
const repo = asNonEmptyString$1(req.query_params?.["repo"]);
|
|
14375
|
+
const rawLimit = parseOptionalInt(req.query_params?.["limit"]);
|
|
14376
|
+
const limit = Math.max(1, Math.min(500, rawLimit ?? 100));
|
|
14377
|
+
return {
|
|
14378
|
+
status_code: 200,
|
|
14379
|
+
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) }
|
|
14380
|
+
};
|
|
14381
|
+
});
|
|
14382
|
+
sdk.registerTrigger({
|
|
14383
|
+
type: "http",
|
|
14384
|
+
function_id: "api::commits",
|
|
14385
|
+
config: {
|
|
14386
|
+
api_path: "/agentmemory/commits",
|
|
14387
|
+
http_method: "GET",
|
|
14388
|
+
middleware_function_ids: ["middleware::api-auth"]
|
|
14389
|
+
}
|
|
14390
|
+
});
|
|
14076
14391
|
sdk.registerFunction("api::sessions", async (req) => {
|
|
14077
14392
|
const authErr = checkAuth(req, secret);
|
|
14078
14393
|
if (authErr) return authErr;
|
|
@@ -17073,6 +17388,39 @@ const CORE_TOOLS = [
|
|
|
17073
17388
|
},
|
|
17074
17389
|
required: ["memoryId"]
|
|
17075
17390
|
}
|
|
17391
|
+
},
|
|
17392
|
+
{
|
|
17393
|
+
name: "memory_commit_lookup",
|
|
17394
|
+
description: "Look up the agent session(s) that produced a specific git commit, given its SHA. Returns the commit metadata and linked sessions.",
|
|
17395
|
+
inputSchema: {
|
|
17396
|
+
type: "object",
|
|
17397
|
+
properties: { sha: {
|
|
17398
|
+
type: "string",
|
|
17399
|
+
description: "Full git commit SHA"
|
|
17400
|
+
} },
|
|
17401
|
+
required: ["sha"]
|
|
17402
|
+
}
|
|
17403
|
+
},
|
|
17404
|
+
{
|
|
17405
|
+
name: "memory_commits",
|
|
17406
|
+
description: "List recent commits linked to agent sessions, optionally filtered by branch or repo.",
|
|
17407
|
+
inputSchema: {
|
|
17408
|
+
type: "object",
|
|
17409
|
+
properties: {
|
|
17410
|
+
branch: {
|
|
17411
|
+
type: "string",
|
|
17412
|
+
description: "Filter by branch name"
|
|
17413
|
+
},
|
|
17414
|
+
repo: {
|
|
17415
|
+
type: "string",
|
|
17416
|
+
description: "Filter by remote URL"
|
|
17417
|
+
},
|
|
17418
|
+
limit: {
|
|
17419
|
+
type: "number",
|
|
17420
|
+
description: "Max results (default 100, max 500)"
|
|
17421
|
+
}
|
|
17422
|
+
}
|
|
17423
|
+
}
|
|
17076
17424
|
}
|
|
17077
17425
|
];
|
|
17078
17426
|
const V040_TOOLS = [
|
|
@@ -19127,6 +19475,49 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
19127
19475
|
}] }
|
|
19128
19476
|
};
|
|
19129
19477
|
}
|
|
19478
|
+
case "memory_commit_lookup": {
|
|
19479
|
+
const sha = asNonEmptyString(args.sha);
|
|
19480
|
+
if (!sha) return {
|
|
19481
|
+
status_code: 400,
|
|
19482
|
+
body: { error: "sha required" }
|
|
19483
|
+
};
|
|
19484
|
+
const link = await kv.get(KV.commits, sha);
|
|
19485
|
+
if (!link) return {
|
|
19486
|
+
status_code: 200,
|
|
19487
|
+
body: { content: [{
|
|
19488
|
+
type: "text",
|
|
19489
|
+
text: JSON.stringify({
|
|
19490
|
+
commit: null,
|
|
19491
|
+
sessions: []
|
|
19492
|
+
}, null, 2)
|
|
19493
|
+
}] }
|
|
19494
|
+
};
|
|
19495
|
+
const linkRecord = link;
|
|
19496
|
+
const sessions = (await Promise.all((linkRecord.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null);
|
|
19497
|
+
return {
|
|
19498
|
+
status_code: 200,
|
|
19499
|
+
body: { content: [{
|
|
19500
|
+
type: "text",
|
|
19501
|
+
text: JSON.stringify({
|
|
19502
|
+
commit: link,
|
|
19503
|
+
sessions
|
|
19504
|
+
}, null, 2)
|
|
19505
|
+
}] }
|
|
19506
|
+
};
|
|
19507
|
+
}
|
|
19508
|
+
case "memory_commits": {
|
|
19509
|
+
const branch = typeof args.branch === "string" ? args.branch : void 0;
|
|
19510
|
+
const repo = typeof args.repo === "string" ? args.repo : void 0;
|
|
19511
|
+
const limit = Math.max(1, Math.min(500, asNumber(args.limit, 100) ?? 100));
|
|
19512
|
+
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);
|
|
19513
|
+
return {
|
|
19514
|
+
status_code: 200,
|
|
19515
|
+
body: { content: [{
|
|
19516
|
+
type: "text",
|
|
19517
|
+
text: JSON.stringify({ commits: filtered }, null, 2)
|
|
19518
|
+
}] }
|
|
19519
|
+
};
|
|
19520
|
+
}
|
|
19130
19521
|
default: return {
|
|
19131
19522
|
status_code: 400,
|
|
19132
19523
|
body: { error: `Unknown tool: ${name}` }
|
|
@@ -19512,7 +19903,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
19512
19903
|
|
|
19513
19904
|
//#endregion
|
|
19514
19905
|
//#region src/viewer/server.ts
|
|
19906
|
+
function loadViewerFavicon() {
|
|
19907
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
19908
|
+
const candidates = [
|
|
19909
|
+
join(base, "..", "src", "viewer", "favicon.svg"),
|
|
19910
|
+
join(base, "..", "viewer", "favicon.svg"),
|
|
19911
|
+
join(base, "viewer", "favicon.svg")
|
|
19912
|
+
];
|
|
19913
|
+
for (const path of candidates) try {
|
|
19914
|
+
return readFileSync(path);
|
|
19915
|
+
} catch {}
|
|
19916
|
+
return null;
|
|
19917
|
+
}
|
|
19515
19918
|
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());
|
|
19919
|
+
const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
19920
|
+
function buildAllowedHosts(origins, listenPort) {
|
|
19921
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
19922
|
+
for (const o of origins) try {
|
|
19923
|
+
const parsed = new URL(o);
|
|
19924
|
+
if (parsed.host) hosts.add(parsed.host.toLowerCase());
|
|
19925
|
+
} catch {}
|
|
19926
|
+
hosts.add(`localhost:${listenPort}`);
|
|
19927
|
+
hosts.add(`127.0.0.1:${listenPort}`);
|
|
19928
|
+
hosts.add(`[::1]:${listenPort}`);
|
|
19929
|
+
for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
|
|
19930
|
+
return hosts;
|
|
19931
|
+
}
|
|
19932
|
+
function isHostAllowed(headerHost, allowed) {
|
|
19933
|
+
if (typeof headerHost !== "string") return false;
|
|
19934
|
+
const lower = headerHost.toLowerCase().trim();
|
|
19935
|
+
if (!lower) return false;
|
|
19936
|
+
return allowed.has(lower);
|
|
19937
|
+
}
|
|
19516
19938
|
function corsHeaders(req) {
|
|
19517
19939
|
const origin = req.headers.origin || "";
|
|
19518
19940
|
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
|
|
@@ -19556,7 +19978,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
|
|
|
19556
19978
|
function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
19557
19979
|
const resolvedRestPort = restPort ?? port - 2;
|
|
19558
19980
|
const requestedPort = port;
|
|
19981
|
+
let allowedHosts = null;
|
|
19559
19982
|
const server = createServer(async (req, res) => {
|
|
19983
|
+
if (!allowedHosts) {
|
|
19984
|
+
const addr = server.address();
|
|
19985
|
+
allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
|
|
19986
|
+
}
|
|
19987
|
+
if (!isHostAllowed(req.headers.host, allowedHosts)) {
|
|
19988
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
19989
|
+
res.end("forbidden host");
|
|
19990
|
+
return;
|
|
19991
|
+
}
|
|
19560
19992
|
const raw = req.url || "/";
|
|
19561
19993
|
const qIdx = raw.indexOf("?");
|
|
19562
19994
|
const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
|
|
@@ -19585,6 +20017,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
|
|
|
19585
20017
|
res.end("viewer not found");
|
|
19586
20018
|
return;
|
|
19587
20019
|
}
|
|
20020
|
+
if (method === "GET" && pathname === "/favicon.svg") {
|
|
20021
|
+
const favicon = loadViewerFavicon();
|
|
20022
|
+
if (favicon) {
|
|
20023
|
+
res.writeHead(200, {
|
|
20024
|
+
"Content-Type": "image/svg+xml",
|
|
20025
|
+
"Cache-Control": "public, max-age=3600"
|
|
20026
|
+
});
|
|
20027
|
+
res.end(favicon);
|
|
20028
|
+
return;
|
|
20029
|
+
}
|
|
20030
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
20031
|
+
res.end("favicon not found");
|
|
20032
|
+
return;
|
|
20033
|
+
}
|
|
19588
20034
|
try {
|
|
19589
20035
|
await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
|
|
19590
20036
|
} catch (err) {
|
|
@@ -19984,7 +20430,7 @@ async function main() {
|
|
|
19984
20430
|
console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
|
|
19985
20431
|
}
|
|
19986
20432
|
bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
19987
|
-
bootLog(`REST API:
|
|
20433
|
+
bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
|
|
19988
20434
|
bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
|
|
19989
20435
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
19990
20436
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|