@agentmemory/agentmemory 0.9.20 → 0.9.21

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/dist/index.mjs CHANGED
@@ -137,6 +137,9 @@ function getMergedEnv(overrides) {
137
137
  function getEnvVar(key) {
138
138
  return getMergedEnv()[key];
139
139
  }
140
+ function isDropStaleIndexEnabled() {
141
+ return getMergedEnv()["AGENTMEMORY_DROP_STALE_INDEX"] === "true";
142
+ }
140
143
  function detectLlmProviderKind() {
141
144
  const env = getMergedEnv();
142
145
  if (hasRealValue(env["ANTHROPIC_API_KEY"]) || hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"]) || hasRealValue(env["OPENROUTER_API_KEY"]) || hasRealValue(env["MINIMAX_API_KEY"]) || hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") return "llm";
@@ -2985,20 +2988,108 @@ async function vectorIndexAddGuarded(id, sessionId, text, context) {
2985
2988
  return false;
2986
2989
  }
2987
2990
  }
2991
+ async function vectorIndexAddBatchGuarded(items) {
2992
+ const vi = vectorIndex;
2993
+ const ep = currentEmbeddingProvider;
2994
+ if (!vi || !ep || items.length === 0) return {
2995
+ ok: 0,
2996
+ fail: 0
2997
+ };
2998
+ let embeddings;
2999
+ try {
3000
+ embeddings = await ep.embedBatch(items.map((i) => clipEmbedInput(i.text)));
3001
+ } catch (err) {
3002
+ logger.warn("vector-index add batch: embed failed — skipping batch", {
3003
+ batchSize: items.length,
3004
+ provider: ep.name,
3005
+ error: err instanceof Error ? err.message : String(err)
3006
+ });
3007
+ return {
3008
+ ok: 0,
3009
+ fail: items.length
3010
+ };
3011
+ }
3012
+ if (embeddings.length !== items.length) {
3013
+ logger.warn("vector-index add batch: provider returned wrong length — skipping batch", {
3014
+ batchSize: items.length,
3015
+ returned: embeddings.length,
3016
+ provider: ep.name
3017
+ });
3018
+ return {
3019
+ ok: 0,
3020
+ fail: items.length
3021
+ };
3022
+ }
3023
+ let ok = 0;
3024
+ let fail = 0;
3025
+ for (let i = 0; i < items.length; i++) {
3026
+ const item = items[i];
3027
+ const embedding = embeddings[i];
3028
+ if (embedding.length !== ep.dimensions) {
3029
+ logger.warn("vector-index add batch: dimension mismatch — skipping item", {
3030
+ kind: item.context.kind,
3031
+ id: item.context.logId,
3032
+ provider: ep.name,
3033
+ expected: ep.dimensions,
3034
+ received: embedding.length
3035
+ });
3036
+ fail++;
3037
+ continue;
3038
+ }
3039
+ try {
3040
+ vi.add(item.id, item.sessionId, embedding);
3041
+ ok++;
3042
+ } catch (err) {
3043
+ logger.warn("vector-index add batch: index write failed — skipping item", {
3044
+ kind: item.context.kind,
3045
+ id: item.context.logId,
3046
+ error: err instanceof Error ? err.message : String(err)
3047
+ });
3048
+ fail++;
3049
+ }
3050
+ }
3051
+ return {
3052
+ ok,
3053
+ fail
3054
+ };
3055
+ }
3056
+ const DEFAULT_REBUILD_EMBED_BATCH = 32;
3057
+ function getRebuildEmbedBatchSize() {
3058
+ const raw = process.env.REBUILD_EMBED_BATCH_SIZE;
3059
+ if (!raw) return DEFAULT_REBUILD_EMBED_BATCH;
3060
+ const n = parseInt(raw, 10);
3061
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_REBUILD_EMBED_BATCH;
3062
+ }
2988
3063
  async function rebuildIndex(kv) {
2989
3064
  const idx = getSearchIndex();
2990
3065
  idx.clear();
2991
3066
  vectorIndex?.clear();
3067
+ const batchSize = getRebuildEmbedBatchSize();
3068
+ const pending = [];
2992
3069
  let count = 0;
3070
+ const flush = async () => {
3071
+ if (pending.length === 0) return;
3072
+ await vectorIndexAddBatchGuarded(pending);
3073
+ pending.length = 0;
3074
+ };
3075
+ const enqueue = async (job) => {
3076
+ pending.push(job);
3077
+ if (pending.length >= batchSize) await flush();
3078
+ };
2993
3079
  try {
2994
3080
  const memories = await kv.list(KV.memories);
2995
3081
  for (const memory of memories) {
2996
3082
  if (memory.isLatest === false) continue;
2997
3083
  if (!memory.title || !memory.content) continue;
2998
3084
  idx.add(memoryToObservation(memory));
2999
- await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
3000
- kind: "memory",
3001
- logId: memory.id
3085
+ await enqueue({
3086
+ id: memory.id,
3087
+ sessionId: memory.sessionIds[0] ?? "memory",
3088
+ text: memory.title + " " + memory.content,
3089
+ context: {
3090
+ kind: "memory",
3091
+ logId: memory.id
3092
+ }
3002
3093
  });
3003
3094
  count++;
3004
3095
  }
@@ -3006,7 +3097,10 @@ async function rebuildIndex(kv) {
3006
3097
  logger.warn("rebuildIndex: failed to load memories", { error: err instanceof Error ? err.message : String(err) });
3007
3098
  }
3008
3099
  const sessions = await kv.list(KV.sessions);
3009
- if (!sessions.length) return count;
3100
+ if (!sessions.length) {
3101
+ await flush();
3102
+ return count;
3103
+ }
3010
3104
  const obsPerSession = [];
3011
3105
  const failedSessions = [];
3012
3106
  for (let batch = 0; batch < sessions.length; batch += 10) {
@@ -3024,12 +3118,18 @@ async function rebuildIndex(kv) {
3024
3118
  if (failedSessions.length > 0) logger.warn("rebuildIndex: failed to load observations for sessions", { failedSessions });
3025
3119
  for (const observations of obsPerSession) for (const obs of observations) if (obs.title && obs.narrative) {
3026
3120
  idx.add(obs);
3027
- await vectorIndexAddGuarded(obs.id, obs.sessionId, obs.title + " " + obs.narrative, {
3028
- kind: "observation",
3029
- logId: obs.id
3121
+ await enqueue({
3122
+ id: obs.id,
3123
+ sessionId: obs.sessionId,
3124
+ text: obs.title + " " + obs.narrative,
3125
+ context: {
3126
+ kind: "observation",
3127
+ logId: obs.id
3128
+ }
3030
3129
  });
3031
3130
  count++;
3032
3131
  }
3132
+ await flush();
3033
3133
  return count;
3034
3134
  }
3035
3135
  function registerSearchFunction(sdk, kv) {
@@ -4814,9 +4914,134 @@ function buildSummaryPrompt(observations) {
4814
4914
  });
4815
4915
  return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
4816
4916
  }
4917
+ const REDUCE_SYSTEM = `You are merging multiple partial summaries of the SAME coding session into one final session summary. The partials are chronological chunks of one continuous session — not separate sessions.
4918
+
4919
+ Output EXACTLY this XML format with no additional text:
4920
+
4921
+ <summary>
4922
+ <title>Short session title (max 100 chars)</title>
4923
+ <narrative>3-5 sentence narrative covering the whole session</narrative>
4924
+ <decisions>
4925
+ <decision>Key technical decision made</decision>
4926
+ </decisions>
4927
+ <files>
4928
+ <file>path/to/modified/file</file>
4929
+ </files>
4930
+ <concepts>
4931
+ <concept>key concept from session</concept>
4932
+ </concepts>
4933
+ </summary>
4934
+
4935
+ Rules:
4936
+ - Synthesize a single narrative that reflects the whole arc, not a chunk-by-chunk recap
4937
+ - Preserve every distinct decision across chunks
4938
+ - Union (deduplicate) all files and concepts
4939
+ - Title should capture the session's overall outcome`;
4940
+ function buildReducePrompt(partials) {
4941
+ const sections = partials.map((p, i) => {
4942
+ const decisions = p.keyDecisions.map((d) => ` - ${d}`).join("\n");
4943
+ const files = p.filesModified.map((f) => ` - ${f}`).join("\n");
4944
+ const concepts = p.concepts.join(", ");
4945
+ return `[Chunk ${i + 1} of ${partials.length} — obs ${p.obsRangeStart}-${p.obsRangeEnd}]
4946
+ Title: ${p.title}
4947
+ Narrative: ${p.narrative}
4948
+ Decisions:
4949
+ ${decisions}
4950
+ Files:
4951
+ ${files}
4952
+ Concepts: ${concepts}`;
4953
+ });
4954
+ return `Partial summaries (${partials.length} chunks of one session, chronological):\n\n${sections.join("\n\n---\n\n")}`;
4955
+ }
4817
4956
 
4818
4957
  //#endregion
4819
4958
  //#region src/functions/summarize.ts
4959
+ const CHUNK_SIZE_DEFAULT = 400;
4960
+ const CHUNK_CONCURRENCY_DEFAULT = 6;
4961
+ const MAX_SKIP_RATIO = .5;
4962
+ function getChunkSize() {
4963
+ const raw = process.env.SUMMARIZE_CHUNK_SIZE;
4964
+ if (!raw) return CHUNK_SIZE_DEFAULT;
4965
+ const n = parseInt(raw, 10);
4966
+ return Number.isFinite(n) && n > 0 ? n : CHUNK_SIZE_DEFAULT;
4967
+ }
4968
+ function getChunkConcurrency() {
4969
+ const raw = process.env.SUMMARIZE_CHUNK_CONCURRENCY;
4970
+ if (!raw) return CHUNK_CONCURRENCY_DEFAULT;
4971
+ const n = parseInt(raw, 10);
4972
+ return Number.isFinite(n) && n > 0 ? n : CHUNK_CONCURRENCY_DEFAULT;
4973
+ }
4974
+ async function summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, total) {
4975
+ for (let attempt = 1; attempt <= 2; attempt++) try {
4976
+ const parsed = parseSummaryXml(await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(chunk)), sessionId, project, chunk.length);
4977
+ if (parsed) return parsed;
4978
+ logger.warn("Summarize chunk parse failed", {
4979
+ sessionId,
4980
+ chunk: `${idx + 1}/${total}`,
4981
+ attempt
4982
+ });
4983
+ } catch (err) {
4984
+ logger.warn("Summarize chunk LLM call failed", {
4985
+ sessionId,
4986
+ chunk: `${idx + 1}/${total}`,
4987
+ attempt,
4988
+ error: err instanceof Error ? err.message : String(err)
4989
+ });
4990
+ }
4991
+ return null;
4992
+ }
4993
+ async function produceSummaryXml(provider, compressed, sessionId, project) {
4994
+ const chunkSize = getChunkSize();
4995
+ if (compressed.length <= chunkSize) return {
4996
+ response: await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(compressed)),
4997
+ mode: "single",
4998
+ chunks: 1
4999
+ };
5000
+ const chunks = [];
5001
+ for (let i = 0; i < compressed.length; i += chunkSize) chunks.push(compressed.slice(i, i + chunkSize));
5002
+ const concurrency = getChunkConcurrency();
5003
+ logger.info("Summarize chunking session", {
5004
+ sessionId,
5005
+ chunks: chunks.length,
5006
+ chunkSize,
5007
+ concurrency,
5008
+ totalObservations: compressed.length
5009
+ });
5010
+ const partialByIdx = new Array(chunks.length).fill(null);
5011
+ for (let batchStart = 0; batchStart < chunks.length; batchStart += concurrency) {
5012
+ const batch = chunks.slice(batchStart, batchStart + concurrency);
5013
+ await Promise.all(batch.map(async (chunk, j) => {
5014
+ const idx = batchStart + j;
5015
+ partialByIdx[idx] = await summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, chunks.length);
5016
+ }));
5017
+ }
5018
+ const skipped = partialByIdx.filter((p) => p === null).length;
5019
+ const partials = partialByIdx.filter((p) => p !== null);
5020
+ if (skipped > Math.floor(chunks.length * MAX_SKIP_RATIO)) throw new Error(`too_many_chunks_skipped: ${skipped}/${chunks.length} chunks failed to parse after retry`);
5021
+ if (skipped > 0) logger.warn("Summarize chunks partially skipped", {
5022
+ sessionId,
5023
+ skipped,
5024
+ total: chunks.length
5025
+ });
5026
+ const reduceInput = partials.map((p) => {
5027
+ const originalIdx = partialByIdx.indexOf(p);
5028
+ return {
5029
+ title: p.title,
5030
+ narrative: p.narrative,
5031
+ keyDecisions: p.keyDecisions,
5032
+ filesModified: p.filesModified,
5033
+ concepts: p.concepts,
5034
+ obsRangeStart: originalIdx * chunkSize + 1,
5035
+ obsRangeEnd: Math.min((originalIdx + 1) * chunkSize, compressed.length)
5036
+ };
5037
+ });
5038
+ return {
5039
+ response: await provider.summarize(REDUCE_SYSTEM, buildReducePrompt(reduceInput)),
5040
+ mode: "chunked",
5041
+ chunks: chunks.length,
5042
+ skipped
5043
+ };
5044
+ }
4820
5045
  function parseSummaryXml(xml, sessionId, project, obsCount) {
4821
5046
  const title = getXmlTag(xml, "title");
4822
5047
  if (!title) return null;
@@ -4865,16 +5090,15 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
4865
5090
  };
4866
5091
  }
4867
5092
  try {
4868
- const prompt = buildSummaryPrompt(compressed);
4869
- const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
5093
+ const { response, mode, chunks } = await produceSummaryXml(provider, compressed, sessionId, session.project);
4870
5094
  if (!response || !response.trim()) {
4871
5095
  const latencyMs = Date.now() - startMs;
4872
5096
  if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
4873
5097
  logger.warn("Empty provider response on summarize", {
4874
5098
  sessionId,
4875
5099
  provider: provider.name,
4876
- promptBytes: prompt.length,
4877
- systemBytes: SUMMARY_SYSTEM.length,
5100
+ mode,
5101
+ chunks,
4878
5102
  observationCount: compressed.length
4879
5103
  });
4880
5104
  return {
@@ -6031,6 +6255,7 @@ async function findByKeyword(kv, keyword, project) {
6031
6255
 
6032
6256
  //#endregion
6033
6257
  //#region src/functions/smart-search.ts
6258
+ const LESSON_CONTENT_PREVIEW_CHARS = 240;
6034
6259
  function registerSmartSearchFunction(sdk, kv, searchFn) {
6035
6260
  sdk.registerFunction("mem::smart-search", async (data) => {
6036
6261
  if (data.expandIds && data.expandIds.length > 0) {
@@ -6073,7 +6298,10 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6073
6298
  error: "query is required"
6074
6299
  };
6075
6300
  const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
6076
- const compact = (await searchFn(data.query, limit)).map((r) => ({
6301
+ const lessonLimit = Math.min(limit, 10);
6302
+ const includeLessons = data.includeLessons !== false;
6303
+ const [hybridResults, lessons] = await Promise.all([searchFn(data.query, limit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
6304
+ const compact = hybridResults.map((r) => ({
6077
6305
  obsId: r.observation.id,
6078
6306
  sessionId: r.sessionId,
6079
6307
  title: r.observation.title,
@@ -6084,14 +6312,42 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6084
6312
  recordAccessBatch(kv, compact.map((r) => r.obsId));
6085
6313
  logger.info("Smart search compact", {
6086
6314
  query: data.query,
6087
- results: compact.length
6315
+ results: compact.length,
6316
+ lessons: lessons.length
6088
6317
  });
6089
- return {
6318
+ const response = {
6090
6319
  mode: "compact",
6091
6320
  results: compact
6092
6321
  };
6322
+ if (includeLessons) response.lessons = lessons;
6323
+ return response;
6093
6324
  });
6094
6325
  }
6326
+ async function recallLessons(sdk, query, limit, project) {
6327
+ try {
6328
+ const result = await sdk.trigger({
6329
+ function_id: "mem::lesson-recall",
6330
+ payload: {
6331
+ query,
6332
+ limit,
6333
+ project
6334
+ }
6335
+ });
6336
+ if (!result?.success || !Array.isArray(result.lessons)) return [];
6337
+ return result.lessons.map((l) => ({
6338
+ lessonId: l.id,
6339
+ content: l.content.length > LESSON_CONTENT_PREVIEW_CHARS ? l.content.slice(0, LESSON_CONTENT_PREVIEW_CHARS) + "…" : l.content,
6340
+ confidence: l.confidence,
6341
+ score: l.score ?? l.confidence,
6342
+ createdAt: l.createdAt,
6343
+ project: l.project,
6344
+ tags: l.tags ?? []
6345
+ }));
6346
+ } catch (err) {
6347
+ logger.warn("Smart search: mem::lesson-recall failed; returning empty lesson list", { error: err instanceof Error ? err.message : String(err) });
6348
+ return [];
6349
+ }
6350
+ }
6095
6351
  async function findObservation$1(kv, obsId, sessionIdHint) {
6096
6352
  if (sessionIdHint) {
6097
6353
  const obs = await kv.get(KV.observations(sessionIdHint), obsId).catch(() => null);
@@ -6317,7 +6573,7 @@ function registerAutoForgetFunction(sdk, kv) {
6317
6573
 
6318
6574
  //#endregion
6319
6575
  //#region src/version.ts
6320
- const VERSION = "0.9.20";
6576
+ const VERSION = "0.9.21";
6321
6577
 
6322
6578
  //#endregion
6323
6579
  //#region src/functions/export-import.ts
@@ -6455,7 +6711,8 @@ function registerExportImportFunction(sdk, kv) {
6455
6711
  "0.9.17",
6456
6712
  "0.9.18",
6457
6713
  "0.9.19",
6458
- "0.9.20"
6714
+ "0.9.20",
6715
+ "0.9.21"
6459
6716
  ]).has(importData.version)) return {
6460
6717
  success: false,
6461
6718
  error: `Unsupported export version: ${importData.version}`
@@ -10157,6 +10414,12 @@ const ALL_CATEGORIES = [
10157
10414
  "signals",
10158
10415
  "sessions",
10159
10416
  "memories",
10417
+ "lessons",
10418
+ "summaries",
10419
+ "semantic",
10420
+ "procedural",
10421
+ "crystals",
10422
+ "insights",
10160
10423
  "mesh"
10161
10424
  ];
10162
10425
  const TWENTY_FOUR_HOURS_MS = 1440 * 60 * 1e3;
@@ -10389,6 +10652,133 @@ function registerDiagnosticsFunction(sdk, kv) {
10389
10652
  fixable: false
10390
10653
  });
10391
10654
  }
10655
+ if (categories.includes("lessons")) {
10656
+ const lessons = await kv.list(KV.lessons);
10657
+ const live = lessons.filter((l) => !l.deleted);
10658
+ let lessonIssues = 0;
10659
+ for (const l of live) if (!Number.isFinite(l.confidence) || l.confidence < 0 || l.confidence > 1) {
10660
+ checks.push({
10661
+ name: `lesson-bad-confidence:${l.id}`,
10662
+ category: "lessons",
10663
+ status: "warn",
10664
+ message: `Lesson ${l.id} has confidence ${l.confidence} (expected finite number in 0..1)`,
10665
+ fixable: false
10666
+ });
10667
+ lessonIssues++;
10668
+ }
10669
+ if (lessonIssues === 0) checks.push({
10670
+ name: "lessons-ok",
10671
+ category: "lessons",
10672
+ status: "pass",
10673
+ message: `All ${live.length} lessons are healthy (${lessons.length - live.length} tombstoned)`,
10674
+ fixable: false
10675
+ });
10676
+ }
10677
+ if (categories.includes("summaries")) {
10678
+ const summaries = await kv.list(KV.summaries);
10679
+ let summaryIssues = 0;
10680
+ for (const s of summaries) if (typeof s.title !== "string" || s.title.trim().length === 0) {
10681
+ checks.push({
10682
+ name: `summary-missing-title:${s.sessionId}`,
10683
+ category: "summaries",
10684
+ status: "warn",
10685
+ message: `Summary for session ${s.sessionId} has no title`,
10686
+ fixable: false
10687
+ });
10688
+ summaryIssues++;
10689
+ }
10690
+ if (summaryIssues === 0) checks.push({
10691
+ name: "summaries-ok",
10692
+ category: "summaries",
10693
+ status: "pass",
10694
+ message: `All ${summaries.length} session summaries are consistent`,
10695
+ fixable: false
10696
+ });
10697
+ }
10698
+ if (categories.includes("semantic")) {
10699
+ const semantic = await kv.list(KV.semantic);
10700
+ let semanticIssues = 0;
10701
+ for (const s of semantic) if (!Number.isFinite(s.confidence) || s.confidence < 0 || s.confidence > 1) {
10702
+ checks.push({
10703
+ name: `semantic-bad-confidence:${s.id}`,
10704
+ category: "semantic",
10705
+ status: "warn",
10706
+ message: `Semantic fact ${s.id} has confidence ${s.confidence} (expected finite number in 0..1)`,
10707
+ fixable: false
10708
+ });
10709
+ semanticIssues++;
10710
+ }
10711
+ if (semanticIssues === 0) checks.push({
10712
+ name: "semantic-ok",
10713
+ category: "semantic",
10714
+ status: "pass",
10715
+ message: `All ${semantic.length} semantic memories are consistent`,
10716
+ fixable: false
10717
+ });
10718
+ }
10719
+ if (categories.includes("procedural")) {
10720
+ const procedural = await kv.list(KV.procedural);
10721
+ let proceduralIssues = 0;
10722
+ for (const p of procedural) if (!Array.isArray(p.steps) || p.steps.length === 0) {
10723
+ checks.push({
10724
+ name: `procedural-empty-steps:${p.id}`,
10725
+ category: "procedural",
10726
+ status: "warn",
10727
+ message: `Procedural memory "${p.name}" (${p.id}) has no steps`,
10728
+ fixable: false
10729
+ });
10730
+ proceduralIssues++;
10731
+ }
10732
+ if (proceduralIssues === 0) checks.push({
10733
+ name: "procedural-ok",
10734
+ category: "procedural",
10735
+ status: "pass",
10736
+ message: `All ${procedural.length} procedural memories are consistent`,
10737
+ fixable: false
10738
+ });
10739
+ }
10740
+ if (categories.includes("crystals")) {
10741
+ const crystals = await kv.list(KV.crystals);
10742
+ let crystalIssues = 0;
10743
+ for (const c of crystals) if (typeof c.narrative !== "string" || c.narrative.trim().length === 0) {
10744
+ checks.push({
10745
+ name: `crystal-empty-narrative:${c.id}`,
10746
+ category: "crystals",
10747
+ status: "warn",
10748
+ message: `Crystal ${c.id} has empty narrative`,
10749
+ fixable: false
10750
+ });
10751
+ crystalIssues++;
10752
+ }
10753
+ if (crystalIssues === 0) checks.push({
10754
+ name: "crystals-ok",
10755
+ category: "crystals",
10756
+ status: "pass",
10757
+ message: `All ${crystals.length} crystals are consistent`,
10758
+ fixable: false
10759
+ });
10760
+ }
10761
+ if (categories.includes("insights")) {
10762
+ const insights = await kv.list(KV.insights);
10763
+ let insightIssues = 0;
10764
+ for (const i of insights) if (!Number.isFinite(i.confidence) || i.confidence < 0 || i.confidence > 1) {
10765
+ checks.push({
10766
+ name: `insight-bad-confidence:${i.id}`,
10767
+ category: "insights",
10768
+ status: "warn",
10769
+ message: `Insight ${i.id} has confidence ${i.confidence} (expected finite number in 0..1)`,
10770
+ fixable: false
10771
+ });
10772
+ insightIssues++;
10773
+ }
10774
+ if (insightIssues === 0) checks.push({
10775
+ name: "insights-ok",
10776
+ category: "insights",
10777
+ status: "pass",
10778
+ message: `All ${insights.length} insights are consistent`,
10779
+ fixable: false
10780
+ });
10781
+ }
10392
10782
  if (categories.includes("mesh")) {
10393
10783
  const peers = await kv.list(KV.mesh);
10394
10784
  let meshIssues = 0;
@@ -14199,13 +14589,16 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14199
14589
  status_code: 400,
14200
14590
  body: { error: "sessionId, project, and cwd are required non-empty strings" }
14201
14591
  };
14592
+ const title = typeof body.title === "string" ? body.title.trim() : void 0;
14202
14593
  const session = {
14203
14594
  id: sessionId,
14204
14595
  project,
14205
14596
  cwd,
14206
14597
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
14207
14598
  status: "active",
14208
- observationCount: 0
14599
+ observationCount: 0,
14600
+ ...title ? { summary: title.slice(0, 200) } : {},
14601
+ ...title ? { firstPrompt: title.slice(0, 200) } : {}
14209
14602
  };
14210
14603
  await kv.set(KV.sessions, sessionId, session);
14211
14604
  return {
@@ -20386,23 +20779,22 @@ async function main() {
20386
20779
  if (mismatches.length > 0) {
20387
20780
  const sample = mismatches.slice(0, 5).map((m) => `${m.obsId} (dim=${m.dim})`).join(", ");
20388
20781
  const distinct = Array.from(seenDimensions).sort((a, b) => a - b).join(", ");
20389
- if (process.env["AGENTMEMORY_DROP_STALE_INDEX"] === "true") console.warn(`[agentmemory] Persisted vector index has ${mismatches.length} of ${loaded.vector.size} vectors with the wrong dimension. Active provider (${embeddingProvider?.name}) declares ${activeDim}; dimensions seen on disk: ${distinct}. AGENTMEMORY_DROP_STALE_INDEX=true is set — discarding the persisted vectors. Live observations will rebuild the index over time.`);
20782
+ if (isDropStaleIndexEnabled()) console.warn(`[agentmemory] Persisted vector index has ${mismatches.length} of ${loaded.vector.size} vectors with the wrong dimension. Active provider (${embeddingProvider?.name}) declares ${activeDim}; dimensions seen on disk: ${distinct}. AGENTMEMORY_DROP_STALE_INDEX=true is set — discarding the persisted vectors. Live observations will rebuild the index over time.`);
20390
20783
  else throw new Error(`[agentmemory] Refusing to start: persisted vector index has ${mismatches.length} of ${loaded.vector.size} vectors with the wrong dimension. Active provider (${embeddingProvider?.name}) declares ${activeDim}; dimensions seen on disk: ${distinct}. First mismatched obsIds: ${sample}. Loading would silently corrupt search (cross-dimension cosine returns 0). Choose one:\n - Re-embed the existing index against the new provider, then start.\n - Set AGENTMEMORY_DROP_STALE_INDEX=true to discard the persisted vectors and rebuild from live observations.\n - Switch the embedding provider back to the one that wrote the index.`);
20391
20784
  } else {
20392
20785
  vectorIndex.restoreFrom(loaded.vector);
20393
20786
  bootLog(`Loaded persisted vector index (${vectorIndex.size} vectors)`);
20394
20787
  }
20395
20788
  }
20396
- if (bm25Index.size === 0) {
20397
- const indexCount = await rebuildIndex(kv).catch((err) => {
20398
- console.warn(`[agentmemory] Failed to rebuild search index:`, err);
20399
- return 0;
20400
- });
20789
+ if (bm25Index.size === 0) rebuildIndex(kv).then((indexCount) => {
20401
20790
  if (indexCount > 0) {
20402
20791
  bootLog(`Search index rebuilt: ${indexCount} entries`);
20403
20792
  indexPersistence.scheduleSave();
20404
20793
  }
20405
- } else try {
20794
+ }).catch((err) => {
20795
+ console.warn(`[agentmemory] Failed to rebuild search index:`, err);
20796
+ });
20797
+ else try {
20406
20798
  const memories = await kv.list(KV.memories);
20407
20799
  let backfilled = 0;
20408
20800
  for (const memory of memories) {