@agentmemory/agentmemory 0.9.31 → 0.9.32

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.
Files changed (138) hide show
  1. package/.env.example +18 -15
  2. package/AGENTS.md +4 -4
  3. package/README.md +92 -270
  4. package/dist/.env.example +18 -15
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +530 -75
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{compression-recovery-CnhLQC6N.mjs → compression-recovery-CZh63Jmz.mjs} +174 -89
  9. package/dist/compression-recovery-CZh63Jmz.mjs.map +1 -0
  10. package/dist/{connect-BSLIAMpw.mjs → connect-BX3njQ3Y.mjs} +28 -14
  11. package/dist/connect-BX3njQ3Y.mjs.map +1 -0
  12. package/dist/hooks/_capture.d.mts +3 -2
  13. package/dist/hooks/_capture.d.mts.map +1 -1
  14. package/dist/hooks/_capture.mjs +87 -8
  15. package/dist/hooks/_capture.mjs.map +1 -1
  16. package/dist/hooks/post-commit.mjs +58 -5
  17. package/dist/hooks/post-commit.mjs.map +1 -1
  18. package/dist/hooks/post-tool-failure.mjs +7 -2
  19. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  20. package/dist/hooks/post-tool-use.mjs +7 -2
  21. package/dist/hooks/post-tool-use.mjs.map +1 -1
  22. package/dist/hooks/pre-compact.mjs +67 -12
  23. package/dist/hooks/pre-compact.mjs.map +1 -1
  24. package/dist/hooks/pre-tool-use.mjs +58 -5
  25. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  26. package/dist/hooks/session-end.mjs +14 -3
  27. package/dist/hooks/session-end.mjs.map +1 -1
  28. package/dist/hooks/session-start.mjs +58 -5
  29. package/dist/hooks/session-start.mjs.map +1 -1
  30. package/dist/hooks/stop.mjs +2 -1
  31. package/dist/hooks/stop.mjs.map +1 -1
  32. package/dist/hooks/subagent-stop.mjs +15 -5
  33. package/dist/hooks/subagent-stop.mjs.map +1 -1
  34. package/dist/hooks/task-completed.mjs +8 -1
  35. package/dist/hooks/task-completed.mjs.map +1 -1
  36. package/dist/index.mjs +724 -200
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/integrations/dsh.mjs +31 -4
  39. package/dist/integrations/dsh.mjs.map +1 -1
  40. package/dist/{secret-store-D9GwO0xp.mjs → secret-store-iYXRcrO8.mjs} +197 -55
  41. package/dist/secret-store-iYXRcrO8.mjs.map +1 -0
  42. package/dist/{src-DaMtDzt_.mjs → src-C23nMpda.mjs} +359 -62
  43. package/dist/src-C23nMpda.mjs.map +1 -0
  44. package/dist/{standalone-BlqshyZ5.mjs → standalone-BYo5USDb.mjs} +169 -10
  45. package/dist/standalone-BYo5USDb.mjs.map +1 -0
  46. package/dist/standalone.d.mts.map +1 -1
  47. package/dist/standalone.mjs +240 -10
  48. package/dist/standalone.mjs.map +1 -1
  49. package/dist/viewer/index.html +49 -28
  50. package/integrations/hermes/README.md +11 -1
  51. package/integrations/hermes/__init__.py +1 -0
  52. package/integrations/hermes/plugin.yaml +1 -1
  53. package/integrations/omp/dist/index.mjs +31 -4
  54. package/integrations/omp/package.json +1 -1
  55. package/integrations/omp/skills/agentmemory-agents/REFERENCE.md +1 -1
  56. package/integrations/omp/skills/agentmemory-architecture/SKILL.md +5 -1
  57. package/integrations/omp/skills/agentmemory-config/REFERENCE.md +2 -1
  58. package/integrations/omp/skills/agentmemory-config/SKILL.md +29 -9
  59. package/integrations/omp/skills/agentmemory-hooks/SKILL.md +17 -6
  60. package/integrations/omp/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  61. package/integrations/omp/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  62. package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  63. package/integrations/omp/skills/agentmemory-rest-api/SKILL.md +1 -1
  64. package/integrations/openclaw/README.md +10 -0
  65. package/integrations/openclaw/openclaw.plugin.json +1 -1
  66. package/integrations/openclaw/package.json +1 -1
  67. package/integrations/openclaw/plugin.yaml +1 -1
  68. package/integrations/openclaw/skills/agentmemory-agents/REFERENCE.md +1 -1
  69. package/integrations/openclaw/skills/agentmemory-architecture/SKILL.md +5 -1
  70. package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +2 -1
  71. package/integrations/openclaw/skills/agentmemory-config/SKILL.md +29 -9
  72. package/integrations/openclaw/skills/agentmemory-hooks/SKILL.md +17 -6
  73. package/integrations/openclaw/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  74. package/integrations/openclaw/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  75. package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  76. package/integrations/openclaw/skills/agentmemory-rest-api/SKILL.md +1 -1
  77. package/integrations/pi/README.md +1 -0
  78. package/integrations/pi/dist/index.mjs +92 -11
  79. package/integrations/pi/index.ts +56 -6
  80. package/integrations/pi/package.json +1 -1
  81. package/integrations/pi/skills/agentmemory-agents/REFERENCE.md +1 -1
  82. package/integrations/pi/skills/agentmemory-architecture/SKILL.md +5 -1
  83. package/integrations/pi/skills/agentmemory-config/REFERENCE.md +2 -1
  84. package/integrations/pi/skills/agentmemory-config/SKILL.md +29 -9
  85. package/integrations/pi/skills/agentmemory-hooks/SKILL.md +17 -6
  86. package/integrations/pi/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  87. package/integrations/pi/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  88. package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  89. package/integrations/pi/skills/agentmemory-rest-api/SKILL.md +1 -1
  90. package/package.json +4 -3
  91. package/plugin/.claude-plugin/plugin.json +16 -3
  92. package/plugin/.codex-plugin/plugin.json +1 -1
  93. package/plugin/.devin-plugin/plugin.json +3 -4
  94. package/plugin/.mcp.json +5 -1
  95. package/plugin/README.md +71 -0
  96. package/plugin/hooks/hooks.json +24 -12
  97. package/plugin/opencode/plugin.json +1 -1
  98. package/plugin/plugin.json +1 -1
  99. package/plugin/scripts/_capture.d.mts +3 -2
  100. package/plugin/scripts/_capture.d.mts.map +1 -1
  101. package/plugin/scripts/_capture.mjs +87 -8
  102. package/plugin/scripts/_capture.mjs.map +1 -1
  103. package/plugin/scripts/plugin-bridge.mjs +25 -6
  104. package/plugin/scripts/post-tool-failure.mjs +7 -2
  105. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  106. package/plugin/scripts/post-tool-use.mjs +7 -2
  107. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  108. package/plugin/scripts/pre-compact.mjs +67 -12
  109. package/plugin/scripts/pre-compact.mjs.map +1 -1
  110. package/plugin/scripts/pre-tool-use.mjs +58 -5
  111. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  112. package/plugin/scripts/session-end.mjs +14 -3
  113. package/plugin/scripts/session-end.mjs.map +1 -1
  114. package/plugin/scripts/session-start.mjs +58 -5
  115. package/plugin/scripts/session-start.mjs.map +1 -1
  116. package/plugin/scripts/stop.mjs +2 -1
  117. package/plugin/scripts/stop.mjs.map +1 -1
  118. package/plugin/scripts/subagent-stop.mjs +15 -5
  119. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  120. package/plugin/scripts/task-completed.mjs +8 -1
  121. package/plugin/scripts/task-completed.mjs.map +1 -1
  122. package/plugin/skills/agentmemory-agents/REFERENCE.md +1 -1
  123. package/plugin/skills/agentmemory-architecture/SKILL.md +5 -1
  124. package/plugin/skills/agentmemory-config/REFERENCE.md +2 -1
  125. package/plugin/skills/agentmemory-config/SKILL.md +29 -9
  126. package/plugin/skills/agentmemory-hooks/SKILL.md +17 -6
  127. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  128. package/plugin/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  129. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  130. package/plugin/skills/agentmemory-rest-api/SKILL.md +1 -1
  131. package/dist/compression-recovery-CnhLQC6N.mjs.map +0 -1
  132. package/dist/connect-BSLIAMpw.mjs.map +0 -1
  133. package/dist/secret-store-D9GwO0xp.mjs.map +0 -1
  134. package/dist/src-DaMtDzt_.mjs.map +0 -1
  135. package/dist/standalone-BlqshyZ5.mjs.map +0 -1
  136. package/plugin/scripts/post-commit.d.mts +0 -1
  137. package/plugin/scripts/post-commit.mjs +0 -204
  138. package/plugin/scripts/post-commit.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -80,53 +80,120 @@ function warnIfPremiumModel(env, envName, model) {
80
80
  warnPremiumModelShown = true;
81
81
  process.stderr.write(`[agentmemory] ${envName}=${model} is in the premium tier. Background compression on this model can cost $5+/day under active use. Cheaper alternatives with comparable quality for memory compression: deepseek/deepseek-v4-flash-0731, deepseek/deepseek-v4-pro, qwen/qwen3-coder. See README "Cost-aware model selection" for the full table. Set AGENTMEMORY_SUPPRESS_COST_WARNING=1 to silence.\n`);
82
82
  }
83
+ const LLM_PROVIDER_NAMES = [
84
+ "anthropic",
85
+ "openai",
86
+ "gemini",
87
+ "openrouter",
88
+ "minimax"
89
+ ];
90
+ const LLM_PROVIDER_KEYS = {
91
+ anthropic: ["ANTHROPIC_API_KEY"],
92
+ openai: ["OPENAI_API_KEY"],
93
+ gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
94
+ openrouter: ["OPENROUTER_API_KEY"],
95
+ minimax: ["MINIMAX_API_KEY"]
96
+ };
97
+ const REMOTE_EMBEDDING_PROVIDER_KEYS = {
98
+ gemini: ["GEMINI_API_KEY"],
99
+ openai: ["OPENAI_API_KEY", "OPENAI_EMBEDDING_API_KEY"],
100
+ voyage: ["VOYAGE_API_KEY"],
101
+ cohere: ["COHERE_API_KEY"],
102
+ openrouter: ["OPENROUTER_API_KEY"]
103
+ };
104
+ function isLlmProviderName(value) {
105
+ return LLM_PROVIDER_NAMES.includes(value);
106
+ }
107
+ function selectedLlmProviderValue(env) {
108
+ return (env["AGENTMEMORY_LLM_PROVIDER"] || "").trim().toLowerCase();
109
+ }
110
+ function llmProviderHasKey(env, name) {
111
+ return LLM_PROVIDER_KEYS[name].some((k) => hasRealValue(env[k]));
112
+ }
113
+ function envFileLabel(path = ENV_FILE) {
114
+ const home = homedir();
115
+ return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
116
+ }
117
+ function llmOptInHint(env = getMergedEnv()) {
118
+ const selected = selectedLlmProviderValue(env);
119
+ if (selected) {
120
+ if (!isLlmProviderName(selected)) return `AGENTMEMORY_LLM_PROVIDER=${selected} is not a known provider. Use one of: ${LLM_PROVIDER_NAMES.join(", ")}.`;
121
+ if (!llmProviderHasKey(env, selected)) return `AGENTMEMORY_LLM_PROVIDER=${selected} is set, but ${LLM_PROVIDER_KEYS[selected].join(" or ")} is not. Add the key to ${envFileLabel()}.`;
122
+ return null;
123
+ }
124
+ if (env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true") return null;
125
+ const withKey = LLM_PROVIDER_NAMES.filter((name) => llmProviderHasKey(env, name));
126
+ if (withKey.length === 0) return null;
127
+ return `Found ${withKey.flatMap((name) => LLM_PROVIDER_KEYS[name].filter((k) => hasRealValue(env[k]))).join(", ")}, but agentmemory sends nothing to an LLM provider until you choose one for it. To turn on LLM summaries and compression, add AGENTMEMORY_LLM_PROVIDER=${withKey[0]} to ${envFileLabel()}.`;
128
+ }
129
+ function embeddingOptInHint(env = getMergedEnv()) {
130
+ const chosen = (env["EMBEDDING_PROVIDER"] || "").trim().toLowerCase();
131
+ if (chosen) {
132
+ const keys = REMOTE_EMBEDDING_PROVIDER_KEYS[chosen];
133
+ if (keys && !keys.some((k) => hasRealValue(env[k]))) return `EMBEDDING_PROVIDER=${chosen} is set, but ${keys.join(" or ")} is not. Add the key to ${envFileLabel()}, or set EMBEDDING_PROVIDER=local for on-device embeddings.`;
134
+ return null;
135
+ }
136
+ const withKey = Object.keys(REMOTE_EMBEDDING_PROVIDER_KEYS).filter((name) => REMOTE_EMBEDDING_PROVIDER_KEYS[name].some((k) => hasRealValue(env[k])));
137
+ if (withKey.length === 0) return null;
138
+ return `A ${withKey.join("/")} key is present, but cloud embeddings stay off until you choose them. Add EMBEDDING_PROVIDER=${withKey[0]} to ${envFileLabel()}, or EMBEDDING_PROVIDER=local for on-device embeddings.`;
139
+ }
83
140
  function detectProvider(env) {
84
141
  const maxTokens = parseInt(env["MAX_TOKENS"] || "4096", 10);
85
- if (hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") {
86
- const model = env["OPENAI_MODEL"] || "gpt-5.6-luna";
87
- warnIfPremiumModel(env, "OPENAI_MODEL", model);
88
- return {
89
- provider: "openai",
90
- model,
91
- maxTokens,
92
- baseURL: env["OPENAI_BASE_URL"]
93
- };
94
- }
95
- if (hasRealValue(env["MINIMAX_API_KEY"])) return {
96
- provider: "minimax",
97
- model: env["MINIMAX_MODEL"] || "MiniMax-M3",
142
+ const noop = {
143
+ provider: "noop",
144
+ model: "noop",
98
145
  maxTokens
99
146
  };
100
- if (hasRealValue(env["ANTHROPIC_API_KEY"])) return {
101
- provider: "anthropic",
102
- model: env["ANTHROPIC_MODEL"] || "claude-sonnet-5",
103
- maxTokens,
104
- baseURL: env["ANTHROPIC_BASE_URL"]
105
- };
106
- if (hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"])) {
107
- if (!hasRealValue(env["GEMINI_API_KEY"]) && hasRealValue(env["GOOGLE_API_KEY"])) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
108
- return {
109
- provider: "gemini",
110
- model: env["GEMINI_MODEL"] || "gemini-3.7-flash",
111
- maxTokens
112
- };
113
- }
114
- if (hasRealValue(env["OPENROUTER_API_KEY"])) {
115
- const model = env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-5";
116
- warnIfPremiumModel(env, "OPENROUTER_MODEL", model);
117
- return {
118
- provider: "openrouter",
119
- model,
120
- maxTokens
121
- };
147
+ const selected = selectedLlmProviderValue(env);
148
+ if (selected) {
149
+ if (!isLlmProviderName(selected) || !llmProviderHasKey(env, selected)) {
150
+ writeProviderNoticeOnce(`[agentmemory] ${llmOptInHint(env)} Running zero-LLM.\n`);
151
+ return noop;
152
+ }
153
+ switch (selected) {
154
+ case "openai": {
155
+ const model = env["OPENAI_MODEL"] || "gpt-5.6-luna";
156
+ warnIfPremiumModel(env, "OPENAI_MODEL", model);
157
+ return {
158
+ provider: "openai",
159
+ model,
160
+ maxTokens,
161
+ baseURL: env["OPENAI_BASE_URL"]
162
+ };
163
+ }
164
+ case "minimax": return {
165
+ provider: "minimax",
166
+ model: env["MINIMAX_MODEL"] || "MiniMax-M3",
167
+ maxTokens
168
+ };
169
+ case "anthropic": return {
170
+ provider: "anthropic",
171
+ model: env["ANTHROPIC_MODEL"] || "claude-sonnet-5",
172
+ maxTokens,
173
+ baseURL: env["ANTHROPIC_BASE_URL"]
174
+ };
175
+ case "gemini":
176
+ if (!hasRealValue(env["GEMINI_API_KEY"])) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
177
+ return {
178
+ provider: "gemini",
179
+ model: env["GEMINI_MODEL"] || "gemini-3.7-flash",
180
+ maxTokens
181
+ };
182
+ case "openrouter": {
183
+ const model = env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-5";
184
+ warnIfPremiumModel(env, "OPENROUTER_MODEL", model);
185
+ return {
186
+ provider: "openrouter",
187
+ model,
188
+ maxTokens
189
+ };
190
+ }
191
+ }
122
192
  }
123
193
  if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
124
- writeProviderNoticeOnce(pc.dim("[agentmemory] No LLM provider key set — running zero-LLM: no LLM compression or summaries; search uses BM25 plus any configured embedding provider. Set EMBEDDING_PROVIDER=local for on-device semantic embeddings if none is configured. Set ANTHROPIC_API_KEY (or GEMINI/OPENAI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env for LLM compression and summaries. Agent-SDK fallback stays off by default to avoid a Stop-hook recursion loop; opt in with AGENTMEMORY_AUTO_COMPRESS=true + AGENTMEMORY_ALLOW_AGENT_SDK=true.\n"));
125
- return {
126
- provider: "noop",
127
- model: "noop",
128
- maxTokens
129
- };
194
+ const hint = llmOptInHint(env);
195
+ writeProviderNoticeOnce(hint ? `[agentmemory] ${hint}\n` : pc.dim("[agentmemory] No LLM provider chosen, so agentmemory runs zero-LLM: no LLM compression or summaries, and no session content leaves this machine for an LLM. Search uses BM25 plus any embedding provider you set with EMBEDDING_PROVIDER (EMBEDDING_PROVIDER=local runs on-device). To turn on LLM summaries, set AGENTMEMORY_LLM_PROVIDER (anthropic, openai, gemini, openrouter or minimax) and that provider's key in ~/.agentmemory/.env.\n"));
196
+ return noop;
130
197
  }
131
198
  writeProviderNoticeOnce("[agentmemory] WARNING: agent-sdk fallback enabled via AGENTMEMORY_ALLOW_AGENT_SDK=true. This spawns @anthropic-ai/claude-agent-sdk child sessions that can trigger the Stop-hook recursion loop. A SDK-child env marker is set to block re-entry, but prefer setting a real API key in ~/.agentmemory/.env instead.\n");
132
199
  return {
@@ -202,7 +269,9 @@ function getAuditMigrateMaxBytes() {
202
269
  return safeParseInt(getMergedEnv()["AGENTMEMORY_AUDIT_MIGRATE_MAX_BYTES"], 32 * 1024 * 1024);
203
270
  }
204
271
  function detectLlmProviderKind(env = getMergedEnv()) {
205
- 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" || env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true") return "llm";
272
+ const selected = selectedLlmProviderValue(env);
273
+ if (selected && isLlmProviderName(selected) && llmProviderHasKey(env, selected)) return "llm";
274
+ if (!selected && env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true") return "llm";
206
275
  return "noop";
207
276
  }
208
277
  function describeLlmRoute(provider) {
@@ -228,15 +297,8 @@ function loadEmbeddingConfig() {
228
297
  };
229
298
  }
230
299
  function detectEmbeddingProvider(env) {
231
- const source = env ?? getMergedEnv();
232
- const forced = source["EMBEDDING_PROVIDER"];
233
- if (forced) return forced;
234
- if (source["GEMINI_API_KEY"]) return "gemini";
235
- if (source["OPENAI_API_KEY"] || source["OPENAI_EMBEDDING_API_KEY"]) return "openai";
236
- if (source["VOYAGE_API_KEY"]) return "voyage";
237
- if (source["COHERE_API_KEY"]) return "cohere";
238
- if (source["OPENROUTER_API_KEY"]) return "openrouter";
239
- return null;
300
+ const forced = ((env ?? getMergedEnv())["EMBEDDING_PROVIDER"] || "").trim();
301
+ return forced ? forced : null;
240
302
  }
241
303
  function loadClaudeBridgeConfig() {
242
304
  const env = getMergedEnv();
@@ -754,9 +816,11 @@ var OpenAIProvider = class {
754
816
  throw new Error(`OpenAI API error (${response.status}): ${text}`);
755
817
  }
756
818
  const data = await response.json();
757
- const message = data.choices?.[0]?.message;
819
+ const choice = data.choices?.[0];
820
+ const message = choice?.message;
758
821
  const content = message?.content;
759
- if (content) return content;
822
+ if (content?.trim()) return content;
823
+ if (choice?.finish_reason === "length") throw new Error(`OpenAI response hit max_tokens (${this.maxTokens}) before any content. Raise MAX_TOKENS or lower OPENAI_REASONING_EFFORT.`);
760
824
  const reasoning = message?.reasoning ?? message?.reasoning_content;
761
825
  if (reasoning) return reasoning;
762
826
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
@@ -2699,8 +2763,84 @@ var StateKV = class {
2699
2763
  if (this.backend !== "redis" || !Array.isArray(values)) return values;
2700
2764
  return orderLikeInsertion(values);
2701
2765
  }
2766
+ async listGroups() {
2767
+ const groups = (await this.sdk.trigger({
2768
+ function_id: "state::list_groups",
2769
+ payload: {}
2770
+ }))?.groups;
2771
+ if (!Array.isArray(groups) || !groups.every((group) => typeof group === "string")) throw new Error("state::list_groups returned an unexpected shape");
2772
+ return groups;
2773
+ }
2702
2774
  };
2703
2775
  //#endregion
2776
+ //#region src/state/state-readiness.ts
2777
+ const STATE_READY_TIMEOUT_MS = 12e4;
2778
+ const STATE_READY_INTERVAL_MS = 100;
2779
+ const PROBE_TIMEOUT_MS = 1e4;
2780
+ const READINESS_PROBE_KEY = "__agentmemory_state_readiness__";
2781
+ function errorMessage$3(error) {
2782
+ return error instanceof Error ? error.message : String(error);
2783
+ }
2784
+ function isRetryableProbeError(error) {
2785
+ const code = error?.code;
2786
+ if (code === "function_not_found" || code === "invocation_timeout" || code === "timeout") return true;
2787
+ return /Function state::(?:get|list) not found|timed out|timeout|ETIMEDOUT/i.test(errorMessage$3(error));
2788
+ }
2789
+ async function waitForStateReady(sdk, options = {}) {
2790
+ const scope = options.scope ?? KV.config;
2791
+ const timeoutMs = options.timeoutMs ?? STATE_READY_TIMEOUT_MS;
2792
+ const intervalMs = options.intervalMs ?? STATE_READY_INTERVAL_MS;
2793
+ const now = options.now ?? (() => performance.now());
2794
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2795
+ const startedAt = now();
2796
+ let attempts = 0;
2797
+ let lastError;
2798
+ const probeTimeoutMs = () => Math.min(PROBE_TIMEOUT_MS, Math.max(1, timeoutMs - (now() - startedAt)));
2799
+ for (;;) {
2800
+ attempts += 1;
2801
+ try {
2802
+ await sdk.trigger({
2803
+ function_id: "state::list",
2804
+ payload: { scope },
2805
+ timeoutMs: probeTimeoutMs()
2806
+ });
2807
+ await sdk.trigger({
2808
+ function_id: "state::get",
2809
+ payload: {
2810
+ scope,
2811
+ key: READINESS_PROBE_KEY
2812
+ },
2813
+ timeoutMs: probeTimeoutMs()
2814
+ });
2815
+ return {
2816
+ ready: true,
2817
+ waitedMs: Math.round(now() - startedAt),
2818
+ attempts
2819
+ };
2820
+ } catch (error) {
2821
+ lastError = errorMessage$3(error);
2822
+ if (!isRetryableProbeError(error)) return {
2823
+ ready: false,
2824
+ waitedMs: Math.round(now() - startedAt),
2825
+ attempts,
2826
+ error: lastError
2827
+ };
2828
+ if (attempts === 1) options.onWaiting?.(lastError);
2829
+ }
2830
+ const elapsed = now() - startedAt;
2831
+ if (elapsed >= timeoutMs) return {
2832
+ ready: false,
2833
+ waitedMs: Math.round(elapsed),
2834
+ attempts,
2835
+ error: lastError
2836
+ };
2837
+ await sleep(Math.min(intervalMs, timeoutMs - elapsed));
2838
+ }
2839
+ }
2840
+ function assertStateReady(readiness) {
2841
+ if (!readiness.ready) throw new Error(`State worker did not become ready within ${readiness.waitedMs}ms: ${readiness.error ?? "timeout"}`);
2842
+ }
2843
+ //#endregion
2704
2844
  //#region src/state/search-layer.ts
2705
2845
  function isSearchLayer(value) {
2706
2846
  return value === "all" || value === "memory" || value === "observation";
@@ -2889,12 +3029,20 @@ var VectorIndex = class VectorIndex {
2889
3029
  };
2890
3030
  //#endregion
2891
3031
  //#region src/state/memory-utils.ts
3032
+ const MAX_DATE_MS = 864e13;
3033
+ function supersededAt(previous, now) {
3034
+ const prior = previous ? Date.parse(previous) : NaN;
3035
+ const current = Date.parse(now);
3036
+ if (Number.isNaN(prior) || current > prior) return now;
3037
+ return new Date(Math.min(prior + 1, MAX_DATE_MS)).toISOString();
3038
+ }
2892
3039
  function memoryToObservation(memory) {
2893
3040
  return {
2894
3041
  id: memory.id,
2895
3042
  sessionId: memory.sessionIds?.[0] ?? "memory",
2896
3043
  timestamp: memory.createdAt,
2897
3044
  type: "decision",
3045
+ memoryType: memory.type,
2898
3046
  title: memory.title,
2899
3047
  facts: [memory.content],
2900
3048
  narrative: memory.content,
@@ -2918,6 +3066,13 @@ function lessonToObservation(l) {
2918
3066
  importance: l.confidence
2919
3067
  };
2920
3068
  }
3069
+ function observationEmbedText(record) {
3070
+ return [
3071
+ record.title,
3072
+ ...record.concepts || [],
3073
+ record.narrative || ""
3074
+ ].filter((part) => part && part.trim()).join(" ").trim();
3075
+ }
2921
3076
  //#endregion
2922
3077
  //#region src/state/obs-index.ts
2923
3078
  const OBS_INDEX_SHARDS = 64;
@@ -3555,10 +3710,12 @@ function registerQueryExpansionFunction(sdk, provider) {
3555
3710
  }
3556
3711
  });
3557
3712
  }
3713
+ const CJK_RUN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu;
3558
3714
  function extractEntitiesFromQuery(query) {
3559
3715
  const entities = [];
3560
3716
  const quoted = query.match(/"([^"]+)"/g);
3561
3717
  if (quoted) for (const q of quoted) entities.push(q.replace(/"/g, ""));
3718
+ for (const run of query.matchAll(CJK_RUN)) for (const seg of segmentCjk(run[0])) if (seg.trim().length >= 2) entities.push(seg.trim());
3562
3719
  const capitalized = query.match(/\b[A-Z][a-zA-Z0-9_.-]+\b/g);
3563
3720
  if (capitalized) {
3564
3721
  const stopWords = new Set([
@@ -3701,8 +3858,8 @@ var HybridSearch = class {
3701
3858
  this.rerankEnabled = rerankEnabled;
3702
3859
  this.graphRetrieval = new GraphRetrieval(kv, (id) => this.bm25.layerOf(id), (id) => this.bm25.sessionOf(id));
3703
3860
  }
3704
- async search(query, limit = 20, targetLayer = "all", authorizeGraph) {
3705
- return this.tripleStreamSearch(query, limit, targetLayer, this.graphRetrieval.createQuery(authorizeGraph));
3861
+ async search(query, limit = 20, targetLayer = "all", authorizeGraph, request) {
3862
+ return this.tripleStreamSearch(query, limit, targetLayer, this.graphRetrieval.createQuery(authorizeGraph), void 0, true, request);
3706
3863
  }
3707
3864
  async searchWithExpansion(query, limit, expansion, targetLayer = "all", authorizeGraph) {
3708
3865
  const allQueries = [
@@ -3722,13 +3879,25 @@ var HybridSearch = class {
3722
3879
  const combined = Array.from(merged.values()).sort((a, b) => b.combinedScore - a.combinedScore || (a.observation.id < b.observation.id ? -1 : a.observation.id > b.observation.id ? 1 : 0));
3723
3880
  return this.rerankResults(query, combined, limit);
3724
3881
  }
3725
- async tripleStreamSearch(query, limit, targetLayer, graphQuery, entityHints, applyReranking = true) {
3882
+ async tripleStreamSearch(query, limit, targetLayer, graphQuery, entityHints, applyReranking = true, request) {
3726
3883
  const retrievalLimit = this.rerankEnabled ? Math.max(limit, 20) : limit;
3727
3884
  const bm25Results = this.bm25.search(query, retrievalLimit * 2, targetLayer);
3728
3885
  let vectorResults = [];
3729
3886
  let queryEmbedding = null;
3730
3887
  if (this.vector && this.embeddingProvider && this.vector.size > 0) try {
3731
- queryEmbedding = await this.embeddingProvider.embed(query);
3888
+ if (request) {
3889
+ if (!request.embedding || request.embedding.query !== query) request.embedding = {
3890
+ query,
3891
+ value: this.embeddingProvider.embed(query)
3892
+ };
3893
+ const embedding = request.embedding;
3894
+ try {
3895
+ queryEmbedding = await embedding.value;
3896
+ } catch (error) {
3897
+ if (request.embedding === embedding) request.embedding = void 0;
3898
+ throw error;
3899
+ }
3900
+ } else queryEmbedding = await this.embeddingProvider.embed(query);
3732
3901
  vectorResults = this.vector.search(queryEmbedding, retrievalLimit * 2, targetLayer, (id) => this.bm25.layerOf(id));
3733
3902
  } catch {}
3734
3903
  const entities = entityHints && entityHints.length > 0 ? entityHints : extractEntitiesFromQuery(query);
@@ -3803,6 +3972,7 @@ var HybridSearch = class {
3803
3972
  graphContext: s.graphContext,
3804
3973
  combinedScore
3805
3974
  }));
3975
+ if (request) request.candidateCount = combined.length;
3806
3976
  const retrievalDepth = Math.max(limit, 20);
3807
3977
  const diversified = this.diversifyBySession(combined, retrievalDepth);
3808
3978
  const enriched = await this.enrichResults(diversified, retrievalDepth);
@@ -4332,6 +4502,10 @@ const PENDING_LOG_SAVE_THRESHOLD = 500;
4332
4502
  const EARLY_SAVE_MIN_GAP_MS = 5e3;
4333
4503
  const BACKFILL_MARKER_KEY = "vectors:backfill";
4334
4504
  const MEMORY_ID_PREFIX = "mem_";
4505
+ const LEGACY_SHARD_SCOPE_PREFIXES = [`${KV.bm25Index}:bm25:`, `${KV.bm25Index}:vectors:`];
4506
+ const LEGACY_SHARD_KEY = "data";
4507
+ const LEGACY_SHARD_SUFFIX = /^((?:[0-9a-z]+-[0-9a-f]{32})|(?:idx_[0-9a-z]+_[0-9a-f]{12})):\d{5}$/;
4508
+ const ORPHAN_SWEEP_KEY = "legacy:sweep";
4335
4509
  function auditIndexPersistEnabled() {
4336
4510
  const raw = process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST;
4337
4511
  if (!raw) return false;
@@ -4366,6 +4540,11 @@ function isValidShardDescriptor(shard) {
4366
4540
  const candidate = shard;
4367
4541
  return typeof candidate.scope === "string" && candidate.scope.length > 0 && typeof candidate.key === "string" && candidate.key.length > 0 && Number.isInteger(candidate.chars) && candidate.chars >= 0;
4368
4542
  }
4543
+ function legacyShardGeneration(scope) {
4544
+ const prefix = LEGACY_SHARD_SCOPE_PREFIXES.find((candidate) => scope.startsWith(candidate));
4545
+ if (!prefix) return void 0;
4546
+ return LEGACY_SHARD_SUFFIX.exec(scope.slice(prefix.length))?.[1] ?? null;
4547
+ }
4369
4548
  function isPersistedVector(row) {
4370
4549
  if (!row || typeof row !== "object") return false;
4371
4550
  const candidate = row;
@@ -4736,7 +4915,7 @@ var IndexPersistence = class {
4736
4915
  }
4737
4916
  async load() {
4738
4917
  if (!this.vector) {
4739
- await this.removeLegacyBm25Snapshot();
4918
+ await this.finishLegacyCleanup();
4740
4919
  return {
4741
4920
  vector: null,
4742
4921
  state: "none",
@@ -4764,7 +4943,7 @@ var IndexPersistence = class {
4764
4943
  this.hasVectorCheckpoint = loaded.size > 0;
4765
4944
  this.checkpointBackfillSince = meta.backfillSince;
4766
4945
  await this.removeLegacyVectorSnapshotIfPresent();
4767
- await this.removeLegacyBm25Snapshot();
4946
+ await this.finishLegacyCleanup();
4768
4947
  const expectedCount = Number.isInteger(meta.count) ? meta.count : void 0;
4769
4948
  this.vectorCountShortfall = expectedCount !== void 0 && loaded.size < expectedCount ? {
4770
4949
  expected: expectedCount,
@@ -4950,7 +5129,7 @@ var IndexPersistence = class {
4950
5129
  savedAt: null
4951
5130
  };
4952
5131
  if (!(manifest.value != null || typeof legacyKey.value === "string" && legacyKey.value.length > 0)) {
4953
- await this.removeLegacyBm25Snapshot();
5132
+ await this.finishLegacyCleanup();
4954
5133
  return {
4955
5134
  vector: null,
4956
5135
  state: "none",
@@ -4983,7 +5162,7 @@ var IndexPersistence = class {
4983
5162
  };
4984
5163
  }
4985
5164
  await this.removeLegacyVectorSnapshot(manifest.value);
4986
- await this.removeLegacyBm25Snapshot();
5165
+ await this.finishLegacyCleanup();
4987
5166
  await this.auditIndexPersistence("migrate", [statePath(KV.bm25Index, VECTOR_META_KEY)], {
4988
5167
  vectors: legacy.size,
4989
5168
  buckets: this.bucketCountInUse()
@@ -5023,6 +5202,71 @@ var IndexPersistence = class {
5023
5202
  if (manifest != null) await this.deleteKey(KV.bm25Index, LEGACY_BM25_MANIFEST_KEY, "legacy_bm25_cleanup");
5024
5203
  if (legacy != null) await this.deleteKey(KV.bm25Index, LEGACY_BM25_KEY, "legacy_bm25_cleanup");
5025
5204
  }
5205
+ async finishLegacyCleanup() {
5206
+ await this.removeLegacyBm25Snapshot();
5207
+ await this.sweepOrphanedGenerations();
5208
+ }
5209
+ async sweepOrphanedGenerations() {
5210
+ try {
5211
+ if (await this.kv.get(KV.bm25Index, ORPHAN_SWEEP_KEY) != null) return;
5212
+ const candidates = /* @__PURE__ */ new Map();
5213
+ for (const scope of await this.kv.listGroups()) {
5214
+ const generation = legacyShardGeneration(scope);
5215
+ if (generation === void 0) continue;
5216
+ if (generation === null) {
5217
+ logger.warn("index persistence: orphan sweep skipped, unrecognized legacy shard scope", { scope });
5218
+ return;
5219
+ }
5220
+ candidates.set(scope, generation);
5221
+ }
5222
+ const named = await this.namedLegacyGenerations();
5223
+ if (!named) return;
5224
+ const orphans = [...candidates].filter(([, generation]) => !named.has(generation));
5225
+ const failures = await inBatches(orphans, WRITE_CONCURRENCY, async ([scope]) => {
5226
+ if (!await this.deleteKey(scope, LEGACY_SHARD_KEY, "orphan_generation_sweep")) throw new Error(scope);
5227
+ });
5228
+ const summary = {
5229
+ generations: new Set(orphans.map(([, generation]) => generation)).size,
5230
+ shards: orphans.length,
5231
+ failed: failures.length
5232
+ };
5233
+ if (failures.length > 0) {
5234
+ logger.warn("index persistence: orphan sweep left shards behind; the next boot retries", summary);
5235
+ return;
5236
+ }
5237
+ if (candidates.size > orphans.length) {
5238
+ if (orphans.length > 0) logger.info("index persistence: swept orphaned legacy index generations", summary);
5239
+ return;
5240
+ }
5241
+ await this.kv.set(KV.bm25Index, ORPHAN_SWEEP_KEY, {
5242
+ v: 1,
5243
+ at: new Date(this.now()).toISOString(),
5244
+ ...summary
5245
+ });
5246
+ await this.auditIndexPersistence("sweep", [statePath(KV.bm25Index, ORPHAN_SWEEP_KEY)], summary);
5247
+ logger.info("index persistence: swept orphaned legacy index generations", summary);
5248
+ } catch (err) {
5249
+ logger.warn("index persistence: orphan sweep failed; the next boot retries", { message: errorMessage$2(err) });
5250
+ }
5251
+ }
5252
+ async namedLegacyGenerations() {
5253
+ const named = /* @__PURE__ */ new Set();
5254
+ for (const [key, label] of [[LEGACY_BM25_MANIFEST_KEY, "bm25"], [LEGACY_VECTOR_MANIFEST_KEY, "vector"]]) {
5255
+ const read = await this.readIndexValue(KV.bm25Index, key, label, "manifest");
5256
+ if (!read.ok) return null;
5257
+ const manifest = read.value;
5258
+ if (manifest == null) continue;
5259
+ const generations = typeof manifest === "object" && Array.isArray(manifest.shards) ? manifest.shards.filter((shard) => typeof shard?.scope === "string" && shard.scope.length > 0).map((shard) => legacyShardGeneration(shard.scope)) : [];
5260
+ if (typeof manifest === "object" && typeof manifest.generation === "string") generations.push(manifest.generation);
5261
+ const usable = generations.filter((generation) => !!generation);
5262
+ for (const generation of usable) named.add(generation);
5263
+ if (usable.length === 0) {
5264
+ logger.warn("index persistence: orphan sweep skipped, a legacy manifest names no generation", { key });
5265
+ return null;
5266
+ }
5267
+ }
5268
+ return named;
5269
+ }
5026
5270
  logFailure(err) {
5027
5271
  const now = this.now();
5028
5272
  if (now - this.lastFailureLogAt < FAILURE_LOG_THROTTLE_MS) return;
@@ -5057,9 +5301,12 @@ var IndexPersistence = class {
5057
5301
  result,
5058
5302
  error
5059
5303
  });
5304
+ return error === void 0;
5060
5305
  }
5061
5306
  async deleteShards(shards, reason) {
5062
- await inBatches(shards, WRITE_CONCURRENCY, (shard) => this.deleteKey(shard.scope, shard.key, reason));
5307
+ await inBatches(shards, WRITE_CONCURRENCY, async (shard) => {
5308
+ await this.deleteKey(shard.scope, shard.key, reason);
5309
+ });
5063
5310
  }
5064
5311
  async loadShardedData(legacyKey, manifestKey, label) {
5065
5312
  const manifest = await this.readIndexValue(KV.bm25Index, manifestKey, label, "manifest");
@@ -5157,7 +5404,29 @@ async function settleWithin(work, ms) {
5157
5404
  }
5158
5405
  //#endregion
5159
5406
  //#region src/functions/privacy.ts
5160
- const PRIVATE_TAG_RE = /<private>[\s\S]*?<\/private>/gi;
5407
+ const PRIVATE_TAG_TOKEN_RE = /<private(?=\s|>)[^>]*>|<\/private\s*>/gi;
5408
+ function redactPrivateBlocks(input) {
5409
+ const tokens = new RegExp(PRIVATE_TAG_TOKEN_RE.source, PRIVATE_TAG_TOKEN_RE.flags);
5410
+ let out = "";
5411
+ let cursor = 0;
5412
+ let depth = 0;
5413
+ for (let match = tokens.exec(input); match; match = tokens.exec(input)) {
5414
+ const closing = match[0].startsWith("</");
5415
+ if (depth === 0) {
5416
+ if (closing) continue;
5417
+ out += input.slice(cursor, match.index);
5418
+ depth = 1;
5419
+ } else if (closing) {
5420
+ depth -= 1;
5421
+ if (depth === 0) {
5422
+ out += "[REDACTED]";
5423
+ cursor = match.index + match[0].length;
5424
+ }
5425
+ } else depth += 1;
5426
+ }
5427
+ if (depth > 0) return `${out}[REDACTED]`;
5428
+ return out + input.slice(cursor);
5429
+ }
5161
5430
  const SECRET_PATTERN_SOURCES = [
5162
5431
  /(?:api[_-]?key|secret|token|password|credential|auth)[\s]*[=:]\s*["']?[A-Za-z0-9_\-/.+]{20,}["']?/gi,
5163
5432
  /Bearer\s+[A-Za-z0-9._\-+/=]{20,}/gi,
@@ -5177,7 +5446,7 @@ const SECRET_PATTERN_SOURCES = [
5177
5446
  const PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/g;
5178
5447
  const URL_CREDENTIALS_RE = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@:"'<>]*:[^\s/?#@"'<>]+@/gi;
5179
5448
  function stripPrivateData(input) {
5180
- let result = input.replace(PRIVATE_TAG_RE, "[REDACTED]");
5449
+ let result = redactPrivateBlocks(input);
5181
5450
  result = result.replace(new RegExp(PRIVATE_KEY_BLOCK_RE.source, PRIVATE_KEY_BLOCK_RE.flags), "[REDACTED_SECRET]");
5182
5451
  result = result.replace(new RegExp(URL_CREDENTIALS_RE.source, URL_CREDENTIALS_RE.flags), "$1[REDACTED_SECRET]@");
5183
5452
  for (const source of SECRET_PATTERN_SOURCES) {
@@ -6151,6 +6420,9 @@ function getSynonyms(stemmedTerm) {
6151
6420
  }
6152
6421
  //#endregion
6153
6422
  //#region src/state/search-index.ts
6423
+ const USER_PROMPT_TERM_CAP = 2e3;
6424
+ const ASSISTANT_RESPONSE_TERM_CAP = 1e3;
6425
+ const TOOL_OUTPUT_TERM_CAP = 1e3;
6154
6426
  var SearchIndex = class SearchIndex {
6155
6427
  entries = /* @__PURE__ */ new Map();
6156
6428
  invertedIndex = /* @__PURE__ */ new Map();
@@ -6409,6 +6681,9 @@ var SearchIndex = class SearchIndex {
6409
6681
  ...obs.files,
6410
6682
  obs.type
6411
6683
  ];
6684
+ if (obs.source?.userPrompt) parts.push(obs.source.userPrompt.slice(0, USER_PROMPT_TERM_CAP));
6685
+ if (obs.source?.assistantResponse) parts.push(obs.source.assistantResponse.slice(0, ASSISTANT_RESPONSE_TERM_CAP));
6686
+ if (obs.source?.toolName === "conversation" && typeof obs.source.toolOutput === "string") parts.push(obs.source.toolOutput.slice(0, TOOL_OUTPUT_TERM_CAP));
6412
6687
  return this.tokenize(parts.join(" ").toLowerCase());
6413
6688
  }
6414
6689
  tokenize(text) {
@@ -6465,6 +6740,7 @@ function formatResult(result, format) {
6465
6740
  sessionId: result.sessionId,
6466
6741
  title: observation.title,
6467
6742
  type: observation.type,
6743
+ ...observation.memoryType !== void 0 && { memoryType: observation.memoryType },
6468
6744
  score: result.score,
6469
6745
  timestamp: observation.timestamp,
6470
6746
  ...result.layer !== void 0 && { layer: result.layer }
@@ -6485,6 +6761,7 @@ function clipResult(result, format, budget) {
6485
6761
  sessionId: result.sessionId,
6486
6762
  title: "",
6487
6763
  type: observation.type,
6764
+ ...observation.memoryType !== void 0 && { memoryType: observation.memoryType },
6488
6765
  score: result.score,
6489
6766
  timestamp: observation.timestamp,
6490
6767
  ...layer,
@@ -6506,6 +6783,7 @@ function clipResult(result, format, budget) {
6506
6783
  sessionId: result.sessionId,
6507
6784
  title,
6508
6785
  narrative,
6786
+ ...observation.memoryType !== void 0 && { memoryType: observation.memoryType },
6509
6787
  score: result.score,
6510
6788
  timestamp: observation.timestamp,
6511
6789
  ...layer,
@@ -6517,6 +6795,7 @@ function clipResult(result, format, budget) {
6517
6795
  sessionId: observation.sessionId,
6518
6796
  timestamp: observation.timestamp,
6519
6797
  type: observation.type,
6798
+ ...observation.memoryType !== void 0 && { memoryType: observation.memoryType },
6520
6799
  title,
6521
6800
  facts: [],
6522
6801
  narrative,
@@ -6967,7 +7246,11 @@ async function indexRecords(observations, memories) {
6967
7246
  id: memory.id,
6968
7247
  revision,
6969
7248
  sessionId: memory.sessionIds?.[0] ?? "memory",
6970
- text: memory.title + " " + memory.content,
7249
+ text: observationEmbedText({
7250
+ title: memory.title,
7251
+ narrative: memory.content,
7252
+ concepts: memory.concepts
7253
+ }),
6971
7254
  context: {
6972
7255
  kind: "memory",
6973
7256
  logId: memory.id
@@ -6981,7 +7264,7 @@ async function indexRecords(observations, memories) {
6981
7264
  id: obs.id,
6982
7265
  revision,
6983
7266
  sessionId: obs.sessionId,
6984
- text: obs.title + " " + obs.narrative,
7267
+ text: observationEmbedText(obs),
6985
7268
  context: {
6986
7269
  kind: "observation",
6987
7270
  logId: obs.id
@@ -7014,21 +7297,36 @@ async function findUnindexedObservations(kv) {
7014
7297
  missing
7015
7298
  };
7016
7299
  }
7017
- const SESSIONS_LIST_RETRY_ATTEMPTS = 3;
7018
- const SESSIONS_LIST_RETRY_BASE_MS = 250;
7300
+ const LIST_RETRY_ATTEMPTS = 8;
7301
+ const LIST_RETRY_BASE_MS = 2500;
7302
+ const LIST_RETRY_BUDGET_MS = 7e4;
7019
7303
  function delay(ms) {
7020
7304
  return new Promise((resolve) => setTimeout(resolve, ms));
7021
7305
  }
7022
- async function listSessionsWithRetry(kv) {
7306
+ function isStateNotReadyError(err) {
7307
+ const code = err?.code;
7308
+ if (code === "function_not_found" || code === "invocation_timeout" || code === "timeout" || code === "ECONNRESET" || code === "ECONNREFUSED" || code === "EPIPE") return true;
7309
+ const message = err instanceof Error ? err.message : String(err);
7310
+ return /function_not_found|Function state::\w+ not found|timed out|timeout|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE|socket hang up|connection (?:closed|reset)/i.test(message);
7311
+ }
7312
+ async function listWithRetry(kv, scope, label, deadline) {
7023
7313
  let lastErr;
7024
- for (let attempt = 0; attempt < SESSIONS_LIST_RETRY_ATTEMPTS; attempt++) try {
7025
- return await kv.list(KV.sessions);
7026
- } catch (err) {
7027
- lastErr = err;
7028
- if (attempt < SESSIONS_LIST_RETRY_ATTEMPTS - 1) await delay(SESSIONS_LIST_RETRY_BASE_MS * (attempt + 1));
7314
+ let attempts = 0;
7315
+ while (attempts < LIST_RETRY_ATTEMPTS) {
7316
+ attempts++;
7317
+ try {
7318
+ return await kv.list(scope);
7319
+ } catch (err) {
7320
+ lastErr = err;
7321
+ if (!isStateNotReadyError(err)) break;
7322
+ const remaining = deadline - performance.now();
7323
+ const wait = LIST_RETRY_BASE_MS * attempts;
7324
+ if (attempts >= LIST_RETRY_ATTEMPTS || wait >= remaining) break;
7325
+ await delay(wait);
7326
+ }
7029
7327
  }
7030
- logger.warn("rebuildKeywordIndex: failed to load sessions after retries", {
7031
- attempts: SESSIONS_LIST_RETRY_ATTEMPTS,
7328
+ logger.warn(`${label}: failed to load`, {
7329
+ attempts,
7032
7330
  error: lastErr instanceof Error ? lastErr.message : String(lastErr)
7033
7331
  });
7034
7332
  return null;
@@ -7078,20 +7376,24 @@ async function runKeywordRebuild(kv, vectorBackfillSince) {
7078
7376
  };
7079
7377
  let documents = 0;
7080
7378
  let memoriesLoaded = false;
7081
- try {
7082
- const memories = await kv.list(KV.memories);
7379
+ const listDeadline = performance.now() + LIST_RETRY_BUDGET_MS;
7380
+ const memories = await listWithRetry(kv, KV.memories, "rebuildKeywordIndex: memories", listDeadline);
7381
+ const sessions = await listWithRetry(kv, KV.sessions, "rebuildKeywordIndex: sessions", listDeadline);
7382
+ if (memories === null) bm25RebuildIncomplete = true;
7383
+ else {
7083
7384
  memoriesLoaded = true;
7084
7385
  for (const memory of memories) {
7085
7386
  if (memory.isLatest === false) continue;
7086
7387
  if (!memory.title || !memory.content) continue;
7087
7388
  idx.add(memoryToObservation(memory), "memory");
7088
- consider(memory.id, memory.sessionIds?.[0] ?? "memory", memory.title + " " + memory.content, memory.createdAt, "memory");
7389
+ consider(memory.id, memory.sessionIds?.[0] ?? "memory", observationEmbedText({
7390
+ title: memory.title,
7391
+ narrative: memory.content,
7392
+ concepts: memory.concepts
7393
+ }), memory.createdAt, "memory");
7089
7394
  documents++;
7090
7395
  }
7091
- } catch (err) {
7092
- logger.warn("rebuildKeywordIndex: failed to load memories", { error: err instanceof Error ? err.message : String(err) });
7093
7396
  }
7094
- const sessions = await listSessionsWithRetry(kv);
7095
7397
  if (sessions === null) {
7096
7398
  bm25RebuildIncomplete = true;
7097
7399
  if (memoriesLoaded) memoryIndexReady = true;
@@ -7115,7 +7417,7 @@ async function runKeywordRebuild(kv, vectorBackfillSince) {
7115
7417
  for (const obs of results.flat()) {
7116
7418
  if (!obs.title || !obs.narrative) continue;
7117
7419
  idx.add(obs);
7118
- consider(obs.id, obs.sessionId, obs.title + " " + obs.narrative, obs.timestamp, "observation");
7420
+ consider(obs.id, obs.sessionId, observationEmbedText(obs), obs.timestamp, "observation");
7119
7421
  documents++;
7120
7422
  }
7121
7423
  }
@@ -7222,6 +7524,7 @@ function registerSearchFunction(sdk, kv) {
7222
7524
  if (data.targetLayer !== void 0 && !isSearchLayer(data.targetLayer)) throw new Error("mem::search: targetLayer must be one of 'all', 'memory', or 'observation'");
7223
7525
  const targetLayer = data.targetLayer ?? "all";
7224
7526
  const MAX_LIMIT = 100;
7527
+ const MAX_CANDIDATES = 1e3;
7225
7528
  let effectiveLimit = 20;
7226
7529
  if (data.limit !== void 0) {
7227
7530
  if (!Number.isInteger(data.limit) || data.limit < 1) throw new Error("mem::search: limit must be a positive integer");
@@ -7259,7 +7562,7 @@ function registerSearchFunction(sdk, kv) {
7259
7562
  await rebuildPromise;
7260
7563
  }
7261
7564
  const filtering = !!(projectFilter || cwdFilter || filterAgentId);
7262
- const fetchLimit = filtering ? Math.max(effectiveLimit * 10, 100) : effectiveLimit;
7565
+ let fetchLimit = filtering ? Math.max(effectiveLimit * 10, 100) : effectiveLimit;
7263
7566
  let results;
7264
7567
  const authorizeGraph = async (record, sessionId, read) => {
7265
7568
  if (filterAgentId !== void 0 && record.agentId !== filterAgentId) return false;
@@ -7273,25 +7576,12 @@ function registerSearchFunction(sdk, kv) {
7273
7576
  }
7274
7577
  return true;
7275
7578
  };
7276
- if (hybridRanker) try {
7277
- results = (await hybridRanker(query, fetchLimit, targetLayer, authorizeGraph)).map((r) => ({
7278
- obsId: r.observation.id,
7279
- sessionId: r.sessionId,
7280
- score: r.combinedScore,
7281
- observation: r.observation,
7282
- layer: r.layer
7283
- }));
7284
- } catch (err) {
7285
- logger.warn("hybrid ranking failed, falling back to keyword search", { error: err instanceof Error ? err.message : String(err) });
7286
- results = idx.search(query, fetchLimit, targetLayer);
7287
- }
7288
- else results = idx.search(query, fetchLimit, targetLayer);
7289
7579
  const sessionCache = /* @__PURE__ */ new Map();
7290
7580
  const loadSession = async (sessionId) => {
7291
7581
  if (sessionCache.has(sessionId)) return sessionCache.get(sessionId);
7292
- const s = await kv.get(KV.sessions, sessionId);
7293
- sessionCache.set(sessionId, s ?? null);
7294
- return s ?? null;
7582
+ const pending = kv.get(KV.sessions, sessionId).then((session) => session ?? null);
7583
+ sessionCache.set(sessionId, pending);
7584
+ return pending;
7295
7585
  };
7296
7586
  const memoryProjectCache = /* @__PURE__ */ new Map();
7297
7587
  const loadMemoryProject = async (obsId) => {
@@ -7300,53 +7590,88 @@ function registerSearchFunction(sdk, kv) {
7300
7590
  memoryProjectCache.set(obsId, proj);
7301
7591
  return proj;
7302
7592
  };
7303
- const earlyCap = filterAgentId ? fetchLimit : effectiveLimit;
7304
- const candidates = [];
7305
- for (const r of results) {
7306
- if (candidates.length >= earlyCap) break;
7307
- const layer = r.layer ?? idx.layerOf(r.obsId) ?? getSearchResultLayer(r.obsId, r.sessionId);
7308
- if (!matchesSearchLayer(r.obsId, r.sessionId, targetLayer, layer)) continue;
7309
- if (filtering) {
7310
- const memoryProject = projectFilter && layer === "memory" ? await loadMemoryProject(r.obsId) : null;
7311
- if (projectFilter && memoryProject !== null && memoryProject !== projectFilter) continue;
7312
- const session = await loadSession(r.sessionId);
7313
- if (session) {
7314
- if (projectFilter && memoryProject === null && session.project !== projectFilter) continue;
7315
- if (cwdFilter && session.cwd !== cwdFilter) continue;
7316
- }
7317
- }
7318
- candidates.push(r);
7319
- }
7320
- const obsResults = await Promise.all(candidates.map(async (r) => {
7321
- if (r.observation) return {
7322
- observation: r.observation,
7323
- layer: r.layer ?? idx.layerOf(r.obsId) ?? getSearchResultLayer(r.obsId, r.sessionId)
7324
- };
7325
- const obs = await kv.get(KV.observations(r.sessionId), r.obsId).catch(() => null);
7326
- if (obs) return {
7327
- observation: obs,
7328
- layer: "observation"
7329
- };
7330
- const mem = await kv.get(KV.memories, r.obsId).catch(() => null);
7331
- return mem ? {
7332
- observation: memoryToObservation(mem),
7333
- layer: "memory"
7334
- } : null;
7335
- }));
7336
- const enriched = [];
7337
- for (let i = 0; i < candidates.length; i++) {
7338
- const resolved = obsResults[i];
7339
- if (!resolved) continue;
7340
- const obs = resolved.observation;
7341
- if (!matchesSearchLayer(obs.id, obs.sessionId, targetLayer, resolved.layer)) continue;
7342
- if (filterAgentId !== void 0 && obs.agentId !== filterAgentId) continue;
7343
- if (enriched.length >= effectiveLimit) break;
7344
- enriched.push({
7345
- observation: withoutObservationSource(obs),
7346
- layer: resolved.layer,
7347
- score: candidates[i].score,
7348
- sessionId: candidates[i].sessionId
7349
- });
7593
+ const observationCache = /* @__PURE__ */ new Map();
7594
+ const hybridRequest = {};
7595
+ let enriched = [];
7596
+ let useHybridRanker = hybridRanker !== null;
7597
+ while (true) {
7598
+ hybridRequest.candidateCount = void 0;
7599
+ if (useHybridRanker && hybridRanker) try {
7600
+ results = (await hybridRanker(query, fetchLimit, targetLayer, authorizeGraph, hybridRequest)).map((r) => ({
7601
+ obsId: r.observation.id,
7602
+ sessionId: r.sessionId,
7603
+ score: r.combinedScore,
7604
+ observation: r.observation,
7605
+ layer: r.layer
7606
+ }));
7607
+ } catch (err) {
7608
+ hybridRequest.candidateCount = void 0;
7609
+ logger.warn("hybrid ranking failed", { error: err instanceof Error ? err.message : String(err) });
7610
+ if (enriched.length > 0) break;
7611
+ useHybridRanker = false;
7612
+ results = idx.search(query, fetchLimit, targetLayer);
7613
+ }
7614
+ else results = idx.search(query, fetchLimit, targetLayer);
7615
+ enriched = [];
7616
+ let batchSize = Math.min(10, effectiveLimit);
7617
+ for (let offset = 0; offset < Math.min(results.length, fetchLimit) && enriched.length < effectiveLimit;) {
7618
+ const batch = results.slice(offset, Math.min(offset + batchSize, fetchLimit));
7619
+ offset += batch.length;
7620
+ const obsResults = await Promise.all(batch.map(async (r) => {
7621
+ const layer = r.layer ?? idx.layerOf(r.obsId) ?? getSearchResultLayer(r.obsId, r.sessionId);
7622
+ if (!matchesSearchLayer(r.obsId, r.sessionId, targetLayer, layer)) return null;
7623
+ if (filtering) {
7624
+ const memoryProject = projectFilter && layer === "memory" ? await loadMemoryProject(r.obsId) : null;
7625
+ if (projectFilter && memoryProject !== null && memoryProject !== projectFilter) return null;
7626
+ const session = projectFilter || cwdFilter ? await loadSession(r.sessionId) : null;
7627
+ if (projectFilter && memoryProject === null && !session) return null;
7628
+ if (cwdFilter && layer === "observation" && !session) return null;
7629
+ if (session) {
7630
+ if (projectFilter && memoryProject === null && session.project !== projectFilter) return null;
7631
+ if (cwdFilter && session.cwd !== cwdFilter) return null;
7632
+ }
7633
+ }
7634
+ const key = JSON.stringify([r.obsId, r.sessionId]);
7635
+ let pending = observationCache.get(key);
7636
+ if (!pending) {
7637
+ pending = (async () => {
7638
+ if (r.observation) return {
7639
+ observation: r.observation,
7640
+ layer: r.layer ?? idx.layerOf(r.obsId) ?? getSearchResultLayer(r.obsId, r.sessionId)
7641
+ };
7642
+ const obs = await kv.get(KV.observations(r.sessionId), r.obsId).catch(() => null);
7643
+ if (obs) return {
7644
+ observation: obs,
7645
+ layer: "observation"
7646
+ };
7647
+ const mem = await kv.get(KV.memories, r.obsId).catch(() => null);
7648
+ return mem ? {
7649
+ observation: memoryToObservation(mem),
7650
+ layer: "memory"
7651
+ } : null;
7652
+ })();
7653
+ observationCache.set(key, pending);
7654
+ }
7655
+ return pending;
7656
+ }));
7657
+ for (let i = 0; i < batch.length; i++) {
7658
+ const resolved = obsResults[i];
7659
+ if (!resolved) continue;
7660
+ const obs = resolved.observation;
7661
+ if (!matchesSearchLayer(obs.id, obs.sessionId, targetLayer, resolved.layer)) continue;
7662
+ if (filterAgentId !== void 0 && obs.agentId !== filterAgentId) continue;
7663
+ if (enriched.length >= effectiveLimit) break;
7664
+ enriched.push({
7665
+ observation: withoutObservationSource(obs),
7666
+ layer: resolved.layer,
7667
+ score: batch[i].score,
7668
+ sessionId: batch[i].sessionId
7669
+ });
7670
+ }
7671
+ batchSize = Math.min(10, Math.max(effectiveLimit - enriched.length, batchSize * 2));
7672
+ }
7673
+ if (!filtering || enriched.length >= effectiveLimit || (hybridRequest.candidateCount ?? results.length) < fetchLimit || fetchLimit >= MAX_CANDIDATES) break;
7674
+ fetchLimit = Math.min(fetchLimit * 2, MAX_CANDIDATES);
7350
7675
  }
7351
7676
  const response = buildRecallResponse(enriched, format, tokenBudget);
7352
7677
  recordAccessBatch(kv, enriched.slice(0, response.results.length).map((result) => result.observation.id));
@@ -7747,7 +8072,7 @@ async function restoreIndexEntries(obs) {
7747
8072
  scheduleIndexSave();
7748
8073
  }
7749
8074
  const vectors = getVectorIndex();
7750
- if (vectors && !vectors.has(obs.id)) await vectorIndexAddGuarded(obs.id, obs.sessionId, obs.title + " " + (obs.narrative || ""), {
8075
+ if (vectors && !vectors.has(obs.id)) await vectorIndexAddGuarded(obs.id, obs.sessionId, observationEmbedText(obs), {
7751
8076
  kind: "synthetic",
7752
8077
  logId: obs.id
7753
8078
  });
@@ -7969,7 +8294,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
7969
8294
  }
7970
8295
  getSearchIndex().add(synthetic);
7971
8296
  scheduleIndexSave();
7972
- await vectorIndexAddGuarded(synthetic.id, synthetic.sessionId, synthetic.title + " " + (synthetic.narrative || ""), {
8297
+ await vectorIndexAddGuarded(synthetic.id, synthetic.sessionId, observationEmbedText(synthetic), {
7973
8298
  kind: "synthetic",
7974
8299
  logId: synthetic.id
7975
8300
  });
@@ -9848,7 +10173,7 @@ Rules:
9848
10173
  - Be concise but preserve ALL technically relevant details
9849
10174
  - File paths must be exact
9850
10175
  - Importance: 1-3 for routine reads, 4-6 for edits/commands, 7-9 for architectural decisions, 10 for breaking changes
9851
- - Concepts should be reusable search terms (e.g., "React hooks", "SQL migration", "auth middleware")
10176
+ - Concepts should be reusable search terms (e.g., "React hooks", "SQL migration", "auth middleware"); keep them in the user's language: when the source content is not English, include at least one concept in that original language, and when the observation mixes languages include both the original-language and English terms so search works in either
9852
10177
  - Strip any secrets, tokens, or credentials from the output`;
9853
10178
  function buildCompressionPrompt(observation) {
9854
10179
  const parts = [`Timestamp: ${observation.timestamp}`, `Hook: ${observation.hookType}`];
@@ -10572,7 +10897,7 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
10572
10897
  retryable: false
10573
10898
  };
10574
10899
  const committedRevision = observationRevision(compressed);
10575
- await bounded(() => vectorIndexAddGuarded(observationId, sessionId, compressed.title + " " + compressed.narrative, {
10900
+ await bounded(() => vectorIndexAddGuarded(observationId, sessionId, observationEmbedText(compressed), {
10576
10901
  kind: "observation",
10577
10902
  logId: observationId
10578
10903
  }, (embedding, isCurrent) => withKeyedLock(`obs:${sessionId}`, async () => {
@@ -11192,7 +11517,7 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
11192
11517
  return {
11193
11518
  success: false,
11194
11519
  error: "no_provider",
11195
- reason: "No LLM provider key set; Summarize is a no-op. Set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env to enable."
11520
+ reason: "No LLM provider chosen; Summarize is a no-op. Set AGENTMEMORY_LLM_PROVIDER and that provider's key in ~/.agentmemory/.env to enable."
11196
11521
  };
11197
11522
  }
11198
11523
  const boundedProvider = {
@@ -11803,6 +12128,7 @@ function registerConsolidateFunction(sdk, kv, provider) {
11803
12128
  const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase() && (!scopedProject || !m.project || m.project === scopedProject));
11804
12129
  if (existingMatch) {
11805
12130
  existingMatch.isLatest = false;
12131
+ existingMatch.updatedAt = supersededAt(existingMatch.updatedAt, now);
11806
12132
  await kv.set(KV.memories, existingMatch.id, existingMatch);
11807
12133
  await recordAudit(kv, "evolve", "mem::consolidate", [existingMatch.id], {
11808
12134
  action: "mark_non_latest",
@@ -12001,25 +12327,52 @@ function registerPatternsFunction(sdk, kv) {
12001
12327
  }
12002
12328
  //#endregion
12003
12329
  //#region src/functions/facets.ts
12004
- async function deleteFacetsForTargets(kv, targetIds) {
12330
+ async function targetLiveness(kv, id) {
12331
+ let observation = false;
12332
+ if (id.startsWith("obs_")) {
12333
+ const sessionId = await lookupObservationSession(kv, id);
12334
+ observation = !!sessionId && !!await kv.get(KV.observations(sessionId), id);
12335
+ }
12336
+ if (id.startsWith("lsn_") || id.startsWith("lesson_")) {
12337
+ const lesson = await kv.get(KV.lessons, id);
12338
+ if (lesson && !lesson.deleted) return {
12339
+ observation,
12340
+ record: true
12341
+ };
12342
+ }
12343
+ const [mem, sem] = await Promise.all([kv.get(KV.memories, id), kv.get(KV.semantic, id)]);
12344
+ return {
12345
+ observation,
12346
+ record: !!(mem || sem)
12347
+ };
12348
+ }
12349
+ function facetTargetLive(facet, liveness) {
12350
+ if (!liveness) return false;
12351
+ if (facet.targetType === "observation") return liveness.observation;
12352
+ return liveness.record || liveness.observation;
12353
+ }
12354
+ async function deleteFacetsForTargets(kv, targetIds, options) {
12005
12355
  const ids = new Set(targetIds);
12006
12356
  ids.delete("");
12007
12357
  if (ids.size === 0) return;
12358
+ const targetTypes = new Set(options?.targetTypes ?? ["memory"]);
12008
12359
  try {
12009
- const live = /* @__PURE__ */ new Set();
12360
+ const live = /* @__PURE__ */ new Map();
12010
12361
  await Promise.all([...ids].map(async (id) => {
12011
12362
  for (let attempt = 0; attempt < 2; attempt++) try {
12012
- const [mem, sem] = await Promise.all([kv.get(KV.memories, id), kv.get(KV.semantic, id)]);
12013
- if (mem || sem) live.add(id);
12363
+ live.set(id, await targetLiveness(kv, id));
12014
12364
  return;
12015
12365
  } catch {
12016
12366
  if (attempt === 1) {
12017
- live.add(id);
12367
+ live.set(id, {
12368
+ observation: true,
12369
+ record: true
12370
+ });
12018
12371
  logger.warn("Facet cleanup skipped unverifiable target", { targetId: id });
12019
12372
  }
12020
12373
  }
12021
12374
  }));
12022
- const matches = (await kv.list(KV.facets)).filter((f) => ids.has(f.targetId) && !live.has(f.targetId) && f.targetType === "memory");
12375
+ const matches = (await kv.list(KV.facets)).filter((f) => ids.has(f.targetId) && targetTypes.has(f.targetType) && !facetTargetLive(f, live.get(f.targetId)));
12023
12376
  await Promise.all(matches.map((f) => kv.delete(KV.facets, f.id)));
12024
12377
  } catch (err) {
12025
12378
  logger.warn("Facet cleanup failed", {
@@ -12295,6 +12648,7 @@ function registerRememberFunction(sdk, kv) {
12295
12648
  if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
12296
12649
  if (supersededMemory) {
12297
12650
  supersededMemory.isLatest = false;
12651
+ supersededMemory.updatedAt = supersededAt(supersededMemory.updatedAt, now);
12298
12652
  await kv.set(KV.memories, supersededMemory.id, supersededMemory);
12299
12653
  try {
12300
12654
  getSearchIndex().remove(supersededMemory.id);
@@ -12317,7 +12671,11 @@ function registerRememberFunction(sdk, kv) {
12317
12671
  nearMatch
12318
12672
  };
12319
12673
  });
12320
- await vectorIndexAddGuarded(memory.id, memory.sessionIds?.[0] ?? "memory", memory.title + " " + memory.content, {
12674
+ await vectorIndexAddGuarded(memory.id, memory.sessionIds?.[0] ?? "memory", observationEmbedText({
12675
+ title: memory.title,
12676
+ narrative: memory.content,
12677
+ concepts: memory.concepts
12678
+ }), {
12321
12679
  kind: "memory",
12322
12680
  logId: memory.id
12323
12681
  }, (embedding, isCurrent) => withKeyedLock("mem:remember", async () => {
@@ -12459,6 +12817,7 @@ function registerRememberFunction(sdk, kv) {
12459
12817
  }
12460
12818
  });
12461
12819
  }
12820
+ if (deletedObservationIds.length > 0) await deleteFacetsForTargets(kv, deletedObservationIds, { targetTypes: ["memory", "observation"] });
12462
12821
  if (deleted > 0 || indexCleaned) await flushIndexSave();
12463
12822
  if (deleted > 0 || failures.length > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
12464
12823
  sessionId: data.sessionId,
@@ -12899,6 +13258,7 @@ function registerRelationsFunction(sdk, kv) {
12899
13258
  isLatest: true
12900
13259
  };
12901
13260
  existing.isLatest = false;
13261
+ existing.updatedAt = supersededAt(existing.updatedAt, now);
12902
13262
  await kv.set(KV.memories, existing.id, existing);
12903
13263
  try {
12904
13264
  getSearchIndex().remove(existing.id);
@@ -12919,7 +13279,11 @@ function registerRelationsFunction(sdk, kv) {
12919
13279
  error: err instanceof Error ? err.message : String(err)
12920
13280
  });
12921
13281
  }
12922
- await vectorIndexAddGuarded(evolved.id, evolved.sessionIds?.[0] ?? "memory", evolved.title + " " + evolved.content, {
13282
+ await vectorIndexAddGuarded(evolved.id, evolved.sessionIds?.[0] ?? "memory", observationEmbedText({
13283
+ title: evolved.title,
13284
+ narrative: evolved.content,
13285
+ concepts: evolved.concepts
13286
+ }), {
12923
13287
  kind: "memory",
12924
13288
  logId: evolved.id
12925
13289
  });
@@ -13094,7 +13458,7 @@ async function findByKeyword(kv, keyword, project) {
13094
13458
  }
13095
13459
  //#endregion
13096
13460
  //#region src/version.ts
13097
- const VERSION = "0.9.31";
13461
+ const VERSION = "0.9.32";
13098
13462
  const III_PINNED_VERSION = "0.25.0";
13099
13463
  //#endregion
13100
13464
  //#region src/telemetry/setup.ts
@@ -13166,6 +13530,7 @@ function getFollowupStats() {
13166
13530
  };
13167
13531
  }
13168
13532
  const LESSON_CONTENT_PREVIEW_CHARS = 240;
13533
+ const SCOPED_RETRY_CANDIDATES = 1e3;
13169
13534
  function registerSmartSearchFunction(sdk, kv, searchFn) {
13170
13535
  sdk.registerFunction("mem::smart-search", async (data) => {
13171
13536
  if (data.targetLayer !== void 0 && !isSearchLayer(data.targetLayer)) throw new Error("mem::smart-search: targetLayer must be one of 'all', 'memory', or 'observation'");
@@ -13242,16 +13607,32 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
13242
13607
  return (await read(KV.sessions, sessionId))?.project === project;
13243
13608
  };
13244
13609
  const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit, targetLayer, authorizeGraph), includeLessons ? recallLessons(sdk, data.query, lessonLimit, project) : Promise.resolve([])]);
13245
- const layerResults = hybridResults.filter((result) => matchesSearchLayer(result.observation.id, result.sessionId, targetLayer, result.layer ?? getSearchIndex().layerOf(result.observation.id)));
13246
- const projectResults = matchesProject ? await filterProjectResults(layerResults, (result) => matchesProject(result.observation.id, result.sessionId)) : layerResults;
13247
- const compact = (filterAgentId ? projectResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : projectResults.slice(0, limit)).map((r) => ({
13248
- obsId: r.observation.id,
13249
- layer: r.layer ?? getSearchIndex().layerOf(r.observation.id) ?? getSearchResultLayer(r.observation.id, r.sessionId),
13250
- sessionId: r.sessionId,
13251
- title: r.observation.title,
13252
- type: r.observation.type,
13253
- score: r.combinedScore,
13254
- timestamp: r.observation.timestamp
13610
+ const scopeResults = async (results) => {
13611
+ const layerResults = results.filter((result) => matchesSearchLayer(result.observation.id, result.sessionId, targetLayer, result.layer ?? getSearchIndex().layerOf(result.observation.id)));
13612
+ const projectResults = matchesProject ? await filterProjectResults(layerResults, (result) => matchesProject(result.observation.id, result.sessionId)) : layerResults;
13613
+ return filterAgentId ? projectResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : projectResults.slice(0, limit);
13614
+ };
13615
+ let filteredHybrid = await scopeResults(hybridResults);
13616
+ if ((filterAgentId || project) && filteredHybrid.length < limit && getSearchIndex().size > overFetchLimit) try {
13617
+ const deeper = await scopeResults(await searchFn(data.query, SCOPED_RETRY_CANDIDATES, targetLayer, authorizeGraph));
13618
+ const seen = new Set(deeper.map((result) => result.observation.id));
13619
+ filteredHybrid = [...deeper, ...filteredHybrid.filter((result) => !seen.has(result.observation.id))].slice(0, limit);
13620
+ } catch (err) {
13621
+ logger.warn("Smart search deeper scoped ranking failed; keeping first-pass results", { error: err instanceof Error ? err.message : String(err) });
13622
+ }
13623
+ const compact = await Promise.all(filteredHybrid.map(async (r) => {
13624
+ const layer = r.layer ?? getSearchIndex().layerOf(r.observation.id) ?? getSearchResultLayer(r.observation.id, r.sessionId);
13625
+ const memoryType = layer === "memory" ? (await kv.get(KV.memories, r.observation.id).catch(() => null))?.type ?? r.observation.memoryType : void 0;
13626
+ return {
13627
+ obsId: r.observation.id,
13628
+ layer,
13629
+ sessionId: r.sessionId,
13630
+ title: r.observation.title,
13631
+ type: r.observation.type,
13632
+ ...memoryType !== void 0 && { memoryType },
13633
+ score: r.combinedScore,
13634
+ timestamp: r.observation.timestamp
13635
+ };
13255
13636
  }));
13256
13637
  recordAccessBatch(kv, compact.map((r) => r.obsId));
13257
13638
  if (data.sessionId && typeof data.sessionId === "string" && data.source !== "viewer" && compact.length > 0) {
@@ -13452,6 +13833,7 @@ function registerRecentSearchesSweepFunction(sdk, kv) {
13452
13833
  const HOUR_MS = 3600 * 1e3;
13453
13834
  function isStaleActive(session, cutoff) {
13454
13835
  if (!session || session.status !== "active") return false;
13836
+ if (typeof session.id !== "string" || session.id.length === 0) return false;
13455
13837
  const freshness = session.updatedAt ?? session.startedAt;
13456
13838
  if (!freshness) return false;
13457
13839
  const ts = new Date(freshness).getTime();
@@ -13747,6 +14129,7 @@ function registerAutoForgetFunction(sdk, kv) {
13747
14129
  if (!dryRun) {
13748
14130
  const older = new Date(memA.createdAt).getTime() < new Date(memB.createdAt).getTime() ? memA : memB;
13749
14131
  older.isLatest = false;
14132
+ older.updatedAt = supersededAt(older.updatedAt, (/* @__PURE__ */ new Date()).toISOString());
13750
14133
  await kv.set(KV.memories, older.id, older);
13751
14134
  await recordAudit(kv, "forget", "mem::auto-forget", [older.id], {
13752
14135
  resource: "memory",
@@ -14269,6 +14652,7 @@ function registerLessonsFunctions(sdk, kv) {
14269
14652
  lessonRecords.delete(lesson.id);
14270
14653
  if (lessonIndex) lessonIndex.remove(lesson.id);
14271
14654
  noteLessonMutation();
14655
+ await deleteFacetsForTargets(kv, [lesson.id]);
14272
14656
  try {
14273
14657
  await recordAudit(kv, "lesson_delete", "mem::lesson-delete", [lesson.id]);
14274
14658
  } catch {}
@@ -14314,11 +14698,14 @@ function registerLessonsFunctions(sdk, kv) {
14314
14698
  }
14315
14699
  }
14316
14700
  await Promise.all(dirty.map((l) => kv.set(KV.lessons, l.id, l)));
14701
+ const softDeletedIds = [];
14317
14702
  for (const l of dirty) if (l.deleted) {
14703
+ softDeletedIds.push(l.id);
14318
14704
  lessonRecords.delete(l.id);
14319
14705
  if (lessonIndex) lessonIndex.remove(l.id);
14320
14706
  } else lessonRecords.set(l.id, l);
14321
14707
  if (dirty.length > 0) noteLessonMutation();
14708
+ if (softDeletedIds.length > 0) await deleteFacetsForTargets(kv, softDeletedIds);
14322
14709
  await Promise.all(auditEvents.map((event) => recordAudit(kv, "lesson_strengthen", "mem::lesson-decay-sweep", [event.id], {
14323
14710
  action: event.action,
14324
14711
  actor: "system",
@@ -14567,7 +14954,8 @@ function registerExportImportFunction(sdk, state) {
14567
14954
  "0.9.28",
14568
14955
  "0.9.29",
14569
14956
  "0.9.30",
14570
- "0.9.31"
14957
+ "0.9.31",
14958
+ "0.9.32"
14571
14959
  ]).has(importData.version)) return {
14572
14960
  success: false,
14573
14961
  error: `Unsupported export version: ${importData.version}`
@@ -17034,7 +17422,7 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
17034
17422
  if (!data?.force && !isConsolidationEnabled()) return {
17035
17423
  success: false,
17036
17424
  skipped: true,
17037
- reason: "Consolidation disabled: set CONSOLIDATION_ENABLED=true or configure an LLM provider (ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GEMINI_API_KEY / GOOGLE_API_KEY / MINIMAX_API_KEY, or AGENTMEMORY_ALLOW_AGENT_SDK=true). OPENAI_BASE_URL alone only points an OpenAI key at another server; it is not a provider."
17425
+ reason: "Consolidation disabled: set CONSOLIDATION_ENABLED=true or choose an LLM provider (AGENTMEMORY_LLM_PROVIDER=anthropic, openai, gemini, openrouter or minimax with that provider's key, or AGENTMEMORY_ALLOW_AGENT_SDK=true). A provider key alone does not turn on LLM use."
17038
17426
  };
17039
17427
  const tier = data?.tier || "all";
17040
17428
  const decayDays = getConsolidationDecayDays();
@@ -24106,7 +24494,7 @@ const FUNCTION_FIXES = {
24106
24494
  "mem::compress": "Compression calls your LLM provider. Check the provider key and model, or set AGENTMEMORY_AUTO_COMPRESS=false to use zero-LLM compression.",
24107
24495
  "mem::graph-extract": "Graph extraction calls your LLM provider. Check the provider key and model, or set GRAPH_EXTRACTION_ENABLED=false."
24108
24496
  };
24109
- const LLM_KEY_FIX = "Add one LLM provider key (for example ANTHROPIC_API_KEY or OPENAI_API_KEY) to ~/.agentmemory/.env and restart.";
24497
+ const LLM_KEY_FIX = "Set AGENTMEMORY_LLM_PROVIDER and that provider's key (for example AGENTMEMORY_LLM_PROVIDER=anthropic and ANTHROPIC_API_KEY) in ~/.agentmemory/.env and restart.";
24110
24498
  const OFF_WITHOUT_LLM = [
24111
24499
  "session summaries",
24112
24500
  "consolidation into semantic facts and procedures",
@@ -24215,8 +24603,17 @@ function evaluateStatus(input) {
24215
24603
  level: "info",
24216
24604
  code: "no-llm-provider",
24217
24605
  message: "No LLM provider is configured, so summaries, consolidation and graph extraction stay off. Search still works.",
24218
- fix: LLM_KEY_FIX
24606
+ fix: input.providerHint || "Set AGENTMEMORY_LLM_PROVIDER and that provider's key (for example AGENTMEMORY_LLM_PROVIDER=anthropic and ANTHROPIC_API_KEY) in ~/.agentmemory/.env and restart."
24219
24607
  });
24608
+ if (input.embeddingHint) {
24609
+ const keyMissing = input.embeddingHint.startsWith("EMBEDDING_PROVIDER=");
24610
+ problems.push({
24611
+ level: "info",
24612
+ code: keyMissing ? "embedding-provider-key-missing" : "embedding-provider-not-chosen",
24613
+ message: keyMissing ? "EMBEDDING_PROVIDER names a cloud provider whose key is missing, so search is keyword and graph only." : "A cloud embedding key is present, but no embedding provider is chosen, so search is keyword and graph only.",
24614
+ fix: input.embeddingHint
24615
+ });
24616
+ }
24220
24617
  const noLlm = input.provider === "noop";
24221
24618
  const functions = markLlmFunctions(input.functionMetrics, input.provider).map((m) => ({
24222
24619
  ...m,
@@ -24397,7 +24794,9 @@ function evaluateStatus(input) {
24397
24794
  provider: {
24398
24795
  llm: input.provider,
24399
24796
  route: input.provider === "noop" ? null : input.llmRoute ?? null,
24797
+ hint: input.providerHint ?? null,
24400
24798
  embeddings: input.embeddingProvider,
24799
+ embeddingHint: input.embeddingHint ?? null,
24401
24800
  circuitBreaker: input.circuitBreaker,
24402
24801
  offWithoutLlm: noLlm ? OFF_WITHOUT_LLM : []
24403
24802
  },
@@ -25694,7 +26093,9 @@ function createStatusReporter(sdk, kv, deps) {
25694
26093
  llmRoute: describeLlmRoute(config.provider),
25695
26094
  functionMetrics: functionMetrics ?? [],
25696
26095
  provider: detectLlmProviderKind(),
26096
+ providerHint: llmOptInHint(),
25697
26097
  embeddingProvider: describeEmbeddingProvider(),
26098
+ embeddingHint: embeddingOptInHint(),
25698
26099
  flags: buildConfigFlags(),
25699
26100
  index: {
25700
26101
  bm25Documents: idx.size,
@@ -25839,13 +26240,18 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
25839
26240
  sdk.registerFunction("api::config-flags", async (req) => {
25840
26241
  const authErr = checkAuth(req, secret);
25841
26242
  if (authErr) return authErr;
26243
+ const providerKind = detectLlmProviderKind();
26244
+ const embeddingProvider = describeEmbeddingProvider();
26245
+ const flags = buildConfigFlags();
25842
26246
  return {
25843
26247
  status_code: 200,
25844
26248
  body: {
25845
26249
  version: VERSION,
25846
- provider: detectLlmProviderKind(),
25847
- embeddingProvider: describeEmbeddingProvider(),
25848
- flags: buildConfigFlags()
26250
+ provider: providerKind,
26251
+ providerHint: llmOptInHint(),
26252
+ embeddingProvider,
26253
+ embeddingHint: embeddingOptInHint(),
26254
+ flags
25849
26255
  }
25850
26256
  };
25851
26257
  });
@@ -29696,6 +30102,45 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
29696
30102
  http_method: "POST"
29697
30103
  }
29698
30104
  });
30105
+ sdk.registerFunction("api::lesson-get", async (req) => {
30106
+ const denied = checkAuth(req, secret);
30107
+ if (denied) return denied;
30108
+ const body = req.body;
30109
+ const lessonId = typeof body?.lessonId === "string" ? body.lessonId.trim() : "";
30110
+ if (!lessonId) return {
30111
+ status_code: 400,
30112
+ body: { error: "lessonId is required" }
30113
+ };
30114
+ if (body.project !== void 0 && typeof body.project !== "string") return {
30115
+ status_code: 400,
30116
+ body: { error: "project must be a string" }
30117
+ };
30118
+ const project = typeof body.project === "string" ? body.project.trim() : "";
30119
+ const lesson = await kv.get(KV.lessons, lessonId);
30120
+ if (!lesson || lesson.deleted) return {
30121
+ status_code: 404,
30122
+ body: { error: "lesson not found" }
30123
+ };
30124
+ if (lesson.project !== void 0 && lesson.project !== project) return {
30125
+ status_code: 404,
30126
+ body: { error: "lesson not found" }
30127
+ };
30128
+ return {
30129
+ status_code: 200,
30130
+ body: {
30131
+ success: true,
30132
+ lesson
30133
+ }
30134
+ };
30135
+ });
30136
+ sdk.registerTrigger({
30137
+ type: "http",
30138
+ function_id: "api::lesson-get",
30139
+ config: {
30140
+ api_path: "/agentmemory/lessons/get",
30141
+ http_method: "POST"
30142
+ }
30143
+ });
29699
30144
  sdk.registerFunction("api::lesson-strengthen", async (req) => {
29700
30145
  const denied = checkAuth(req, secret);
29701
30146
  if (denied) return denied;
@@ -30461,6 +30906,59 @@ function registerViewerSse(sdk) {
30461
30906
  }
30462
30907
  //#endregion
30463
30908
  //#region src/mcp/tools-registry.ts
30909
+ const READ_ONLY_TOOLS = new Set([
30910
+ "memory_recall",
30911
+ "memory_file_history",
30912
+ "memory_patterns",
30913
+ "memory_sessions",
30914
+ "memory_smart_search",
30915
+ "memory_vision_search",
30916
+ "memory_timeline",
30917
+ "memory_export",
30918
+ "memory_relations",
30919
+ "memory_commit_lookup",
30920
+ "memory_commits",
30921
+ "memory_graph_query",
30922
+ "memory_team_feed",
30923
+ "memory_audit",
30924
+ "memory_frontier",
30925
+ "memory_next",
30926
+ "memory_diagnose",
30927
+ "memory_facet_query",
30928
+ "memory_verify",
30929
+ "memory_lesson_recall",
30930
+ "memory_insight_list",
30931
+ "memory_slot_list",
30932
+ "memory_slot_get"
30933
+ ]);
30934
+ const DESTRUCTIVE_TOOLS = new Set([
30935
+ "memory_action_update",
30936
+ "memory_compress_file",
30937
+ "memory_claude_bridge_sync",
30938
+ "memory_governance_delete",
30939
+ "memory_heal",
30940
+ "memory_lesson_delete",
30941
+ "memory_obsidian_export",
30942
+ "memory_slot_replace",
30943
+ "memory_slot_delete"
30944
+ ]);
30945
+ const IDEMPOTENT_WRITE_TOOLS = new Set([
30946
+ "memory_governance_delete",
30947
+ "memory_lesson_delete",
30948
+ "memory_slot_replace",
30949
+ "memory_slot_delete",
30950
+ "memory_claude_bridge_sync"
30951
+ ]);
30952
+ const OPEN_WORLD_TOOLS = new Set(["memory_mesh_sync"]);
30953
+ function toolAnnotations(name) {
30954
+ const readOnly = READ_ONLY_TOOLS.has(name);
30955
+ return {
30956
+ readOnlyHint: readOnly,
30957
+ destructiveHint: !readOnly && DESTRUCTIVE_TOOLS.has(name),
30958
+ idempotentHint: readOnly || IDEMPOTENT_WRITE_TOOLS.has(name),
30959
+ openWorldHint: OPEN_WORLD_TOOLS.has(name)
30960
+ };
30961
+ }
30464
30962
  const CORE_TOOLS = [
30465
30963
  {
30466
30964
  name: "memory_recall",
@@ -31441,6 +31939,11 @@ const V070_TOOLS = [
31441
31939
  tags: {
31442
31940
  type: "string",
31443
31941
  description: "Comma-separated tags"
31942
+ },
31943
+ sourceIds: {
31944
+ type: "array",
31945
+ items: { type: "string" },
31946
+ description: "Ids of the session, memory, observation or crystal this lesson came from (max 50)"
31444
31947
  }
31445
31948
  },
31446
31949
  required: ["content"]
@@ -31663,7 +32166,10 @@ function getAllTools() {
31663
32166
  ...V070_TOOLS,
31664
32167
  ...V073_TOOLS,
31665
32168
  ...V010_SLOTS_TOOLS
31666
- ];
32169
+ ].map((tool) => ({
32170
+ ...tool,
32171
+ annotations: toolAnnotations(tool.name)
32172
+ }));
31667
32173
  }
31668
32174
  function getVisibleTools() {
31669
32175
  if ((process.env["AGENTMEMORY_TOOLS"] || "all") === "core") return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
@@ -31719,13 +32225,18 @@ function parseCsvList(value) {
31719
32225
  if (Array.isArray(value)) return value.map((v) => typeof v === "string" ? v.trim() : "").filter(Boolean);
31720
32226
  return [];
31721
32227
  }
32228
+ const SLOT_TOOL_NAMES = new Set(getAllTools().filter((t) => t.name.startsWith("memory_slot_")).map((t) => t.name));
32229
+ function isSlotTool(name) {
32230
+ return SLOT_TOOL_NAMES.has(name);
32231
+ }
31722
32232
  function registerMcpEndpoints(sdk, kv, secret) {
32233
+ const slotsEnabled = isSlotsEnabled();
31723
32234
  sdk.registerFunction("mcp::tools::list", async (req) => {
31724
32235
  const authErr = checkAuth(req, secret);
31725
32236
  if (authErr) return authErr;
31726
32237
  return {
31727
32238
  status_code: 200,
31728
- body: { tools: getVisibleTools() }
32239
+ body: { tools: getVisibleTools().filter((t) => slotsEnabled || !isSlotTool(t.name)) }
31729
32240
  };
31730
32241
  });
31731
32242
  sdk.registerTrigger({
@@ -31744,6 +32255,7 @@ function registerMcpEndpoints(sdk, kv, secret) {
31744
32255
  body: { error: "name is required" }
31745
32256
  };
31746
32257
  const { name, arguments: args = {} } = req.body;
32258
+ if (isSlotTool(name) && !slotsEnabled) return slotsDisabledResponse();
31747
32259
  try {
31748
32260
  switch (name) {
31749
32261
  case "memory_recall": {
@@ -32765,6 +33277,11 @@ function registerMcpEndpoints(sdk, kv, secret) {
32765
33277
  body: { error: "content is required" }
32766
33278
  };
32767
33279
  const lessonTags = typeof args.tags === "string" && args.tags.trim() ? args.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
33280
+ const lessonSourceIds = normalizeLessonSourceIds(args.sourceIds);
33281
+ if (!lessonSourceIds) return {
33282
+ status_code: 400,
33283
+ body: { error: `sourceIds must be an array of at most 50 ids (session, memory, observation or crystal ids) without spaces` }
33284
+ };
32768
33285
  const lessonSaveResult = await sdk.trigger({
32769
33286
  function_id: "mem::lesson-save",
32770
33287
  payload: {
@@ -32773,7 +33290,8 @@ function registerMcpEndpoints(sdk, kv, secret) {
32773
33290
  confidence: args.confidence,
32774
33291
  project: args.project,
32775
33292
  tags: lessonTags,
32776
- source: "manual"
33293
+ source: "manual",
33294
+ sourceIds: lessonSourceIds
32777
33295
  }
32778
33296
  });
32779
33297
  return {
@@ -33195,24 +33713,16 @@ function registerMcpEndpoints(sdk, kv, secret) {
33195
33713
  };
33196
33714
  }
33197
33715
  if (uri === "agentmemory://graph/stats") try {
33198
- const graphState = await pinGraphState(kv);
33199
- const nodes = await graphState.list(KV.graphNodes);
33200
- const edges = await graphState.list(KV.graphEdges);
33201
- const nodesByType = {};
33202
- for (const n of nodes) nodesByType[n.type] = (nodesByType[n.type] || 0) + 1;
33203
- const edgesByType = {};
33204
- for (const e of edges) edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
33716
+ const stats = await sdk.trigger({
33717
+ function_id: "mem::graph-stats",
33718
+ payload: {}
33719
+ });
33205
33720
  return {
33206
33721
  status_code: 200,
33207
33722
  body: { contents: [{
33208
33723
  uri,
33209
33724
  mimeType: "application/json",
33210
- text: JSON.stringify({
33211
- totalNodes: nodes.length,
33212
- totalEdges: edges.length,
33213
- nodesByType,
33214
- edgesByType
33215
- })
33725
+ text: JSON.stringify(stats)
33216
33726
  }] }
33217
33727
  };
33218
33728
  } catch {
@@ -33222,8 +33732,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
33222
33732
  uri,
33223
33733
  mimeType: "application/json",
33224
33734
  text: JSON.stringify({
33225
- totalNodes: 0,
33226
- totalEdges: 0
33735
+ totalNodes: null,
33736
+ totalEdges: null,
33737
+ nodesByType: {},
33738
+ edgesByType: {},
33739
+ fromSnapshot: false,
33740
+ fromIndex: false,
33741
+ available: false,
33742
+ coverage: "unknown",
33743
+ warning: "Graph stats are unavailable. Check agentmemory graph-index status."
33227
33744
  })
33228
33745
  }] }
33229
33746
  };
@@ -33652,14 +34169,18 @@ async function main() {
33652
34169
  const config = loadConfig();
33653
34170
  const embeddingConfig = loadEmbeddingConfig();
33654
34171
  const fallbackConfig = loadFallbackConfig();
33655
- const provider = fallbackConfig.providers.length > 0 ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
34172
+ const provider = fallbackConfig.providers.length > 0 && config.provider.provider !== "noop" ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
33656
34173
  const embeddingProvider = createEmbeddingProvider();
33657
34174
  const imageEmbeddingProvider = createImageEmbeddingProvider();
33658
34175
  bootLog(`Starting worker v${VERSION}...`);
33659
34176
  bootLog(`Engine: ${config.engineUrl}`);
33660
34177
  bootLog(`Provider: ${config.provider.provider} (${config.provider.model})`);
33661
34178
  if (embeddingProvider) bootLog(`Embedding provider: ${embeddingProvider.name} (${embeddingProvider.dimensions} dims)`);
33662
- else bootLog(`Embedding provider: none (BM25-only mode)`);
34179
+ else {
34180
+ bootLog(`Embedding provider: none (BM25-only mode)`);
34181
+ const embeddingHint = embeddingOptInHint();
34182
+ if (embeddingHint) bootLog(embeddingHint);
34183
+ }
33663
34184
  if (imageEmbeddingProvider) bootLog(`Image embedding provider: ${imageEmbeddingProvider.name} (${imageEmbeddingProvider.dimensions} dims) — vision-search active`);
33664
34185
  bootLog(`REST API: http://localhost:${config.restPort}/agentmemory/*`);
33665
34186
  const sdk = withWriteScrubbing(registerWorker(config.engineUrl, {
@@ -33682,6 +34203,9 @@ async function main() {
33682
34203
  stateBackend = getStateBackend();
33683
34204
  } catch {}
33684
34205
  const kv = new StateKV(sdk, { backend: stateBackend });
34206
+ const stateReadiness = await waitForStateReady(sdk, { onWaiting: (error) => bootWarn(`Waiting for the state worker to register state::get and state::list (${error})`) });
34207
+ assertStateReady(stateReadiness);
34208
+ if (stateReadiness.attempts > 1) bootLog(`State worker ready after ${stateReadiness.waitedMs}ms (${stateReadiness.attempts} attempts)`);
33685
34209
  const secret = resolveWorkerSecret();
33686
34210
  const metricsStore = new MetricsStore(kv);
33687
34211
  const dedupMap = new DedupMap();
@@ -33786,7 +34310,7 @@ async function main() {
33786
34310
  const bm25Index = getSearchIndex();
33787
34311
  const graphWeight = parseFloat(getEnvVar("AGENTMEMORY_GRAPH_WEIGHT") || "0.3");
33788
34312
  const hybridSearch = new HybridSearch(bm25Index, vectorIndex, embeddingProvider, kv, embeddingConfig.bm25Weight, embeddingConfig.vectorWeight, graphWeight);
33789
- const hybridRanker = (query, limit, targetLayer, authorizeGraph) => hybridSearch.search(query, limit, targetLayer, authorizeGraph);
34313
+ const hybridRanker = (query, limit, targetLayer, authorizeGraph, request) => hybridSearch.search(query, limit, targetLayer, authorizeGraph, request);
33790
34314
  registerSmartSearchFunction(sdk, kv, hybridRanker);
33791
34315
  setHybridRanker(hybridRanker);
33792
34316
  registerRecentSearchesSweepFunction(sdk, kv);
@@ -33884,7 +34408,7 @@ async function main() {
33884
34408
  console.warn(`[agentmemory] Failed to rebuild the BM25 index:`, err);
33885
34409
  }
33886
34410
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
33887
- bootLog(`REST API: 146 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
34411
+ bootLog(`REST API: 147 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
33888
34412
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
33889
34413
  (async () => {
33890
34414
  try {