@agentmemory/agentmemory 0.9.17 → 0.9.18

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.
@@ -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 url = `${this.baseUrl}/v1/messages`;
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",
@@ -208,7 +221,12 @@ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
208
221
  * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
209
222
  * OPENAI_MODEL — model name (default: gpt-4o-mini)
210
223
  * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
211
- * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (default: 60000)
224
+ * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
225
+ * takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
226
+ * for back-compat with the v0.9.17 shipping name).
227
+ * AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
228
+ * raw-fetch LLM + embedding providers. Used when
229
+ * OPENAI_TIMEOUT_MS is not set. Default: 60000.
212
230
  * MAX_TOKENS — max output tokens (default: from config or 4096)
213
231
  * OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
214
232
  * Passthrough for reasoning models (e.g. Ollama Cloud
@@ -232,7 +250,7 @@ var OpenAIProvider = class {
232
250
  this.maxTokens = maxTokens;
233
251
  this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL$1).replace(/\/+$/, "");
234
252
  this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
235
- this.timeoutMs = parseTimeout(getEnvVar("OPENAI_TIMEOUT_MS"));
253
+ this.timeoutMs = resolveTimeout();
236
254
  this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
237
255
  this.isAzure = detectAzure(this.baseUrl);
238
256
  }
@@ -273,21 +291,16 @@ var OpenAIProvider = class {
273
291
  }]
274
292
  };
275
293
  if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
276
- const ac = new AbortController();
277
- const t = setTimeout(() => ac.abort(), this.timeoutMs);
278
294
  let response;
279
295
  try {
280
- response = await fetch(url, {
296
+ response = await fetchWithTimeout(url, {
281
297
  method: "POST",
282
298
  headers: this.buildHeaders(),
283
- body: JSON.stringify(body),
284
- signal: ac.signal
285
- });
299
+ body: JSON.stringify(body)
300
+ }, this.timeoutMs);
286
301
  } catch (err) {
287
- if (ac.signal.aborted || err instanceof Error && err.name === "AbortError") throw new Error(`OpenAI API request timed out after ${this.timeoutMs}ms — set OPENAI_TIMEOUT_MS to raise the bound or check the provider status.`);
302
+ 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
303
  throw err;
289
- } finally {
290
- clearTimeout(t);
291
304
  }
292
305
  if (!response.ok) {
293
306
  const text = await response.text();
@@ -302,10 +315,19 @@ var OpenAIProvider = class {
302
315
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
303
316
  }
304
317
  };
305
- function parseTimeout(raw) {
306
- if (!raw) return DEFAULT_TIMEOUT_MS;
307
- const n = parseInt(raw, 10);
308
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_TIMEOUT_MS;
318
+ function resolveTimeout() {
319
+ const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
320
+ if (openai !== void 0) return openai;
321
+ const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
322
+ if (globalMs !== void 0) return globalMs;
323
+ return DEFAULT_TIMEOUT_MS;
324
+ }
325
+ function parsePositiveInt(raw) {
326
+ if (!raw) return void 0;
327
+ const trimmed = raw.trim();
328
+ if (!/^\d+$/.test(trimmed)) return void 0;
329
+ const n = Number(trimmed);
330
+ return Number.isFinite(n) && n > 0 ? n : void 0;
309
331
  }
310
332
  function detectAzure(baseUrl) {
311
333
  try {
@@ -337,7 +359,7 @@ var OpenRouterProvider = class {
337
359
  return this.call(systemPrompt, userPrompt);
338
360
  }
339
361
  async call(systemPrompt, userPrompt) {
340
- const response = await fetch(this.baseUrl, {
362
+ const response = await fetchWithTimeout(this.baseUrl, {
341
363
  method: "POST",
342
364
  headers: {
343
365
  "Content-Type": "application/json",
@@ -506,7 +528,7 @@ var GeminiEmbeddingProvider = class {
506
528
  const results = [];
507
529
  for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
508
530
  const chunk = texts.slice(i, i + BATCH_LIMIT);
509
- const response = await fetch(`${API_BASE}?key=${this.apiKey}`, {
531
+ const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
510
532
  method: "POST",
511
533
  headers: { "Content-Type": "application/json" },
512
534
  body: JSON.stringify({ requests: chunk.map((t) => ({
@@ -595,8 +617,7 @@ var OpenAIEmbeddingProvider = class {
595
617
  return result;
596
618
  }
597
619
  async embedBatch(texts) {
598
- const url = `${this.baseUrl}/v1/embeddings`;
599
- const response = await fetch(url, {
620
+ const response = await fetchWithTimeout(`${this.baseUrl}/v1/embeddings`, {
600
621
  method: "POST",
601
622
  headers: {
602
623
  Authorization: `Bearer ${this.apiKey}`,
@@ -631,7 +652,7 @@ var VoyageEmbeddingProvider = class {
631
652
  return result;
632
653
  }
633
654
  async embedBatch(texts) {
634
- const response = await fetch(API_URL$2, {
655
+ const response = await fetchWithTimeout(API_URL$2, {
635
656
  method: "POST",
636
657
  headers: {
637
658
  Authorization: `Bearer ${this.apiKey}`,
@@ -667,7 +688,7 @@ var CohereEmbeddingProvider = class {
667
688
  return result;
668
689
  }
669
690
  async embedBatch(texts) {
670
- const response = await fetch(API_URL$1, {
691
+ const response = await fetchWithTimeout(API_URL$1, {
671
692
  method: "POST",
672
693
  headers: {
673
694
  Authorization: `Bearer ${this.apiKey}`,
@@ -705,7 +726,7 @@ var OpenRouterEmbeddingProvider = class {
705
726
  return result;
706
727
  }
707
728
  async embedBatch(texts) {
708
- const response = await fetch(API_URL, {
729
+ const response = await fetchWithTimeout(API_URL, {
709
730
  method: "POST",
710
731
  headers: {
711
732
  Authorization: `Bearer ${this.apiKey}`,
@@ -4095,7 +4116,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4095
4116
  sdk.registerFunction("mem::context", async (data) => {
4096
4117
  const budget = data.budget || tokenBudget;
4097
4118
  const blocks = [];
4098
- const [pinnedSlots, profile] = await Promise.all([isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]), kv.get(KV.profiles, data.project).catch(() => null)]);
4119
+ const [pinnedSlots, profile, lessons] = await Promise.all([
4120
+ isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
4121
+ kv.get(KV.profiles, data.project).catch(() => null),
4122
+ kv.list(KV.lessons).catch(() => [])
4123
+ ]);
4099
4124
  const slotContent = renderPinnedContext(pinnedSlots);
4100
4125
  if (slotContent) blocks.push({
4101
4126
  type: "memory",
@@ -4119,6 +4144,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4119
4144
  });
4120
4145
  }
4121
4146
  }
4147
+ const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
4148
+ const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
4149
+ return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
4150
+ }).slice(0, 10);
4151
+ if (relevantLessons.length > 0) {
4152
+ const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
4153
+ const mostRecent = relevantLessons.reduce((acc, l) => {
4154
+ const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
4155
+ return t > acc ? t : acc;
4156
+ }, 0);
4157
+ blocks.push({
4158
+ type: "memory",
4159
+ content: lessonsContent,
4160
+ tokens: estimateTokens$1(lessonsContent),
4161
+ recency: mostRecent,
4162
+ sourceIds: relevantLessons.map((l) => l.id)
4163
+ });
4164
+ }
4122
4165
  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
4166
  const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
4124
4167
  const sessionsNeedingObs = [];
@@ -4941,6 +4984,46 @@ const DEFAULTS$1 = {
4941
4984
  lowImportanceThreshold: 3,
4942
4985
  maxObservationsPerProject: 1e4
4943
4986
  };
4987
+ function isValidRecoveryResult(result) {
4988
+ if (!result || typeof result !== "object") return false;
4989
+ if (!("success" in result)) return true;
4990
+ return result.success !== false;
4991
+ }
4992
+ function isCompressedObservation(observation) {
4993
+ return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
4994
+ }
4995
+ async function recoverStaleSession(sdk, sessionId) {
4996
+ try {
4997
+ const result = await sdk.trigger({
4998
+ function_id: "event::session::stopped",
4999
+ payload: { sessionId }
5000
+ });
5001
+ if (!isValidRecoveryResult(result)) {
5002
+ logger.warn("Stale session recovery failed", {
5003
+ sessionId,
5004
+ result
5005
+ });
5006
+ return false;
5007
+ }
5008
+ return true;
5009
+ } catch (err) {
5010
+ logger.warn("Stale session recovery failed", {
5011
+ sessionId,
5012
+ error: err instanceof Error ? err.message : String(err)
5013
+ });
5014
+ return false;
5015
+ }
5016
+ }
5017
+ async function runRecoveredSessionConsolidation(sdk) {
5018
+ try {
5019
+ await sdk.trigger({
5020
+ function_id: "mem::consolidate-pipeline",
5021
+ payload: { tier: "all" }
5022
+ });
5023
+ } catch (err) {
5024
+ logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
5025
+ }
5026
+ }
4944
5027
  function registerEvictFunction(sdk, kv) {
4945
5028
  sdk.registerFunction("mem::evict", async (data) => {
4946
5029
  const dryRun = data?.dryRun ?? false;
@@ -4959,6 +5042,7 @@ function registerEvictFunction(sdk, kv) {
4959
5042
  nonLatestMemories: 0,
4960
5043
  dryRun
4961
5044
  };
5045
+ let recoveredStaleSessions = 0;
4962
5046
  const sessions = await kv.list(KV.sessions).catch(() => []);
4963
5047
  const summaries = await kv.list(KV.summaries).catch(() => []);
4964
5048
  const summaryIds = new Set(summaries.map((s) => s.sessionId));
@@ -4966,6 +5050,23 @@ function registerEvictFunction(sdk, kv) {
4966
5050
  if (!session.startedAt) continue;
4967
5051
  if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
4968
5052
  else {
5053
+ const observations = await kv.list(KV.observations(session.id)).catch((err) => {
5054
+ logger.warn("Stale session observation scan failed", {
5055
+ sessionId: session.id,
5056
+ error: err instanceof Error ? err.message : String(err)
5057
+ });
5058
+ return null;
5059
+ });
5060
+ if (!observations) continue;
5061
+ let recovered = false;
5062
+ if (observations.some(isCompressedObservation)) {
5063
+ recovered = await recoverStaleSession(sdk, session.id);
5064
+ if (!recovered) continue;
5065
+ recoveredStaleSessions++;
5066
+ } else if (observations.length > 0) {
5067
+ logger.warn("Stale session has no compressed observations", { sessionId: session.id });
5068
+ continue;
5069
+ }
4969
5070
  try {
4970
5071
  await kv.delete(KV.sessions, session.id);
4971
5072
  stats.staleSessions++;
@@ -4979,11 +5080,12 @@ function registerEvictFunction(sdk, kv) {
4979
5080
  }
4980
5081
  await recordAudit(kv, "delete", "mem::evict", [session.id], {
4981
5082
  resource: "session",
4982
- reason: "stale_session_without_summary",
5083
+ reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
4983
5084
  dryRun
4984
5085
  });
4985
5086
  }
4986
5087
  }
5088
+ if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
4987
5089
  const projectObs = /* @__PURE__ */ new Map();
4988
5090
  for (const session of sessions) {
4989
5091
  const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
@@ -5793,7 +5895,8 @@ function registerExportImportFunction(sdk, kv) {
5793
5895
  "0.9.14",
5794
5896
  "0.9.15",
5795
5897
  "0.9.16",
5796
- "0.9.17"
5898
+ "0.9.17",
5899
+ "0.9.18"
5797
5900
  ]).has(importData.version)) return {
5798
5901
  success: false,
5799
5902
  error: `Unsupported export version: ${importData.version}`
@@ -17987,7 +18090,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
17987
18090
 
17988
18091
  //#endregion
17989
18092
  //#region src/viewer/server.ts
18093
+ function loadViewerFavicon() {
18094
+ const base = dirname(fileURLToPath(import.meta.url));
18095
+ const candidates = [
18096
+ join(base, "..", "src", "viewer", "favicon.svg"),
18097
+ join(base, "..", "viewer", "favicon.svg"),
18098
+ join(base, "viewer", "favicon.svg")
18099
+ ];
18100
+ for (const path of candidates) try {
18101
+ return readFileSync(path);
18102
+ } catch {}
18103
+ return null;
18104
+ }
17990
18105
  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());
18106
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
18107
+ function buildAllowedHosts(origins, listenPort) {
18108
+ const hosts = /* @__PURE__ */ new Set();
18109
+ for (const o of origins) try {
18110
+ const parsed = new URL(o);
18111
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
18112
+ } catch {}
18113
+ hosts.add(`localhost:${listenPort}`);
18114
+ hosts.add(`127.0.0.1:${listenPort}`);
18115
+ hosts.add(`[::1]:${listenPort}`);
18116
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
18117
+ return hosts;
18118
+ }
18119
+ function isHostAllowed(headerHost, allowed) {
18120
+ if (typeof headerHost !== "string") return false;
18121
+ const lower = headerHost.toLowerCase().trim();
18122
+ if (!lower) return false;
18123
+ return allowed.has(lower);
18124
+ }
17991
18125
  function corsHeaders(req) {
17992
18126
  const origin = req.headers.origin || "";
17993
18127
  const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
@@ -18031,7 +18165,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
18031
18165
  function startViewerServer(port, _kv, _sdk, secret, restPort) {
18032
18166
  const resolvedRestPort = restPort ?? port - 2;
18033
18167
  const requestedPort = port;
18168
+ let allowedHosts = null;
18034
18169
  const server = createServer(async (req, res) => {
18170
+ if (!allowedHosts) {
18171
+ const addr = server.address();
18172
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
18173
+ }
18174
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
18175
+ res.writeHead(403, { "Content-Type": "text/plain" });
18176
+ res.end("forbidden host");
18177
+ return;
18178
+ }
18035
18179
  const raw = req.url || "/";
18036
18180
  const qIdx = raw.indexOf("?");
18037
18181
  const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
@@ -18060,6 +18204,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
18060
18204
  res.end("viewer not found");
18061
18205
  return;
18062
18206
  }
18207
+ if (method === "GET" && pathname === "/favicon.svg") {
18208
+ const favicon = loadViewerFavicon();
18209
+ if (favicon) {
18210
+ res.writeHead(200, {
18211
+ "Content-Type": "image/svg+xml",
18212
+ "Cache-Control": "public, max-age=3600"
18213
+ });
18214
+ res.end(favicon);
18215
+ return;
18216
+ }
18217
+ res.writeHead(404, { "Content-Type": "text/plain" });
18218
+ res.end("favicon not found");
18219
+ return;
18220
+ }
18063
18221
  try {
18064
18222
  await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
18065
18223
  } catch (err) {
@@ -18527,4 +18685,4 @@ main().catch((err) => {
18527
18685
 
18528
18686
  //#endregion
18529
18687
  export { };
18530
- //# sourceMappingURL=src-TiNuQ3Ub.mjs.map
18688
+ //# sourceMappingURL=src-C7vygXCj.mjs.map