hippo-memory 1.64.0 → 1.64.1

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/README.md CHANGED
@@ -994,7 +994,7 @@ Three benchmarks testing three different things. Full details in [`benchmarks/`]
994
994
 
995
995
  [LongMemEval](https://arxiv.org/abs/2410.10813) (ICLR 2025) is the industry-standard benchmark: 500 questions across 5 memory abilities, embedded in 115k+ token chat histories.
996
996
 
997
- **`hippo recall` (measured 2026-09-28 on hippo-memory 1.52.5).** On a default install, `hippo recall` puts an answer session in its top 5 for 85.6% of the 500 `_s` questions (95% CI 82.4 to 88.6), and 87.6% with the optional MiniLM embedder (84.6 to 90.4). Most of the gap to the scripts below is the default 4,000-token budget. Each memory in this test is a whole session, about 2,600 tokens at the median, so recall returns a median of 2 sessions. With the budget lifted, the same rankings score 96.8% (95.2 to 98.2) and 97.4% (96.0 to 98.6). A lifted call returns every candidate, a median of 47 sessions and 123,491 tokens per question, so those two figures measure the ranking, not an amount of text an agent could take in. Result: [`docs/evals/2026-09-28-recall-cli-longmemeval-result.md`](docs/evals/2026-09-28-recall-cli-longmemeval-result.md).
997
+ **`hippo recall` (measured 2026-09-28 on hippo-memory 1.52.5, re-run 2026-10-06 on 1.64.0).** On a default install, `hippo recall` puts an answer session in its top 5 for 85.6% of the 500 `_s` questions (95% CI 82.4 to 88.6), and 87.8% with the optional MiniLM embedder (84.8 to 90.6; 87.6% on 1.52.5). Most of the gap to the scripts below is the default 4,000-token budget. Each memory in this test is a whole session, about 2,600 tokens at the median, so recall returns a median of 2 sessions. With the budget lifted, the same rankings score 96.8% (95.2 to 98.2) and 97.4% (96.0 to 98.6). A lifted call returns every candidate, a median of 47 sessions and 123,491 tokens per question, so those two figures measure the ranking, not an amount of text an agent could take in. Result: [`docs/evals/2026-09-28-recall-cli-longmemeval-result.md`](docs/evals/2026-09-28-recall-cli-longmemeval-result.md); re-run: [`docs/evals/2026-10-06-recall-cli-longmemeval-rerun-1.64.0.md`](docs/evals/2026-10-06-recall-cli-longmemeval-rerun-1.64.0.md).
998
998
 
999
999
  **The benchmark scripts (`_s` split; MiniLM re-measured 2026-09-23, voyage measured 2026-06-09).** Each question is scored against its own ~48-session haystack, the same way gbrain and other published systems report. The v1.23.0 pluggable embedding provider lets you choose the embedder:
1000
1000
 
@@ -1,4 +1,5 @@
1
+ import { type EmbeddingProvider } from '../embedding-provider.js';
1
2
  export declare function cmdRefine(hippoRoot: string, flags: Record<string, string | boolean | string[]>): Promise<void>;
2
3
  export declare function cmdDedup(hippoRoot: string, flags: Record<string, string | boolean | string[]>): void;
3
- export declare function cmdEmbed(hippoRoot: string, flags: Record<string, string | boolean | string[]>): Promise<void>;
4
+ export declare function cmdEmbed(hippoRoot: string, flags: Record<string, string | boolean | string[]>, given?: EmbeddingProvider): Promise<void>;
4
5
  //# sourceMappingURL=maintenance.d.ts.map
@@ -3,7 +3,6 @@ import { envAnthropicApiKey } from '../env.js';
3
3
  import { loadAllEntries } from '../store/entry-reads.js';
4
4
  import { deduplicateStore } from '../dedupe.js';
5
5
  import { embedAll, loadEmbeddingIndex } from '../embeddings.js';
6
- import { resolveEmbeddingModel } from '../local-embedding.js';
7
6
  import { resolveEmbeddingProvider } from '../embedding-provider.js';
8
7
  import { resetAllPhysicsState } from '../physics-state.js';
9
8
  import { loadConfig } from '../config.js';
@@ -93,7 +92,7 @@ export function cmdDedup(hippoRoot, flags) {
93
92
  // ---------------------------------------------------------------------------
94
93
  // Embed command
95
94
  // ---------------------------------------------------------------------------
96
- export async function cmdEmbed(hippoRoot, flags) {
95
+ export async function cmdEmbed(hippoRoot, flags, given) {
97
96
  // --global mirrors resolveAuthRoot (cli.ts:6900): initGlobal() + the global
98
97
  // root, skipping the local requireInit entirely, so this is the healing
99
98
  // path for pre-1.27.0 global stores from a directory with no local .hippo.
@@ -109,12 +108,13 @@ export async function cmdEmbed(hippoRoot, flags) {
109
108
  printEmbedStatus(root);
110
109
  return;
111
110
  }
112
- if (!embedProviderReady(root))
111
+ const provider = readyEmbedProvider(root, given);
112
+ if (!provider)
113
113
  return;
114
114
  console.log('Embedding all memories (this may take a moment on first run to download model)...');
115
115
  let count;
116
116
  try {
117
- count = await embedAll(root, resolveEmbeddingModel(root));
117
+ count = await embedAll(root, undefined, provider);
118
118
  }
119
119
  catch (err) {
120
120
  printError(`Embedding failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -126,6 +126,11 @@ export async function cmdEmbed(hippoRoot, flags) {
126
126
  const entriesAfter = loadAllEntries(root);
127
127
  const embIndexAfter = loadEmbeddingIndex(root);
128
128
  console.log(`Done. ${count} new embeddings created. ${Object.keys(embIndexAfter).length}/${entriesAfter.length} total.`);
129
+ const unembedded = entriesAfter.filter((e) => !embIndexAfter[e.id]).length;
130
+ if (unembedded > 0) {
131
+ printError(`${unembedded} memories are still not embedded (the warnings above name them). Re-run \`hippo embed\` to retry.`);
132
+ process.exitCode = 1;
133
+ }
129
134
  }
130
135
  function resetPhysics(root) {
131
136
  const entries = loadAllEntries(root);
@@ -154,10 +159,10 @@ function printEmbedStatus(root) {
154
159
  console.log(` ${missing.length} memories need embedding (run \`hippo embed\` to embed them)`);
155
160
  }
156
161
  }
157
- /** False, after saying why, when no usable provider exists. */
158
- function embedProviderReady(root) {
162
+ /** The provider to embed with, or null after saying why none is usable. */
163
+ function readyEmbedProvider(root, given) {
159
164
  // Embedding (unlike status/reset) needs an available provider.
160
- const embedProvider = (() => {
165
+ const embedProvider = given ?? (() => {
161
166
  try {
162
167
  return resolveEmbeddingProvider(root);
163
168
  }
@@ -168,23 +173,24 @@ function embedProviderReady(root) {
168
173
  })();
169
174
  if (!embedProvider) {
170
175
  process.exitCode = 1;
171
- return false;
176
+ return null;
172
177
  }
173
178
  if (embedProvider.isAvailable())
174
- return true;
179
+ return embedProvider;
175
180
  if (loadConfig(root).embeddings.enabled === false) {
176
181
  console.log('Embeddings are disabled in config (embeddings.enabled = false). Set it to true or "auto" to enable.');
177
- return false;
182
+ return null;
178
183
  }
179
184
  if (embedProvider.kind === 'local') {
180
185
  console.log('Embeddings not available. Install @huggingface/transformers to enable:');
181
186
  console.log(' npm install @huggingface/transformers');
187
+ process.exitCode = 1;
182
188
  }
183
189
  else {
184
190
  printError(`Embedding provider '${embedProvider.kind}' is configured but ${embedProvider.keyEnv} is not set.`);
185
191
  printError(`Export ${embedProvider.keyEnv}, or set config.embeddings.provider back to 'local'.`);
186
192
  process.exitCode = 1;
187
193
  }
188
- return false;
194
+ return null;
189
195
  }
190
196
  //# sourceMappingURL=maintenance.js.map
@@ -27,7 +27,7 @@
27
27
  * egress-blocked in the build sandbox).
28
28
  */
29
29
  import { envByName } from './env.js';
30
- import { getEmbedding, isEmbeddingAvailable, resolveEmbeddingModel, DEFAULT_EMBEDDING_MODEL, } from './local-embedding.js';
30
+ import { getEmbedding, isEmbeddingAvailable, requireLocalPipeline, resolveEmbeddingModel, DEFAULT_EMBEDDING_MODEL, } from './local-embedding.js';
31
31
  import { loadConfig } from './config.js';
32
32
  import { redactSecretsStrict } from './secret-detect.js';
33
33
  import { fetchWithRetry } from './http-retry.js';
@@ -57,8 +57,9 @@ class LocalEmbeddingProvider {
57
57
  return this.enabled && isEmbeddingAvailable();
58
58
  }
59
59
  async embed(texts, role) {
60
- // Sequential to preserve the historical single-pipeline behaviour and avoid
61
- // contending the one cached pipeline instance with N concurrent calls.
60
+ // A model that cannot load fails the call, as an API outage does; items then run one at a time on the shared pipeline.
61
+ if (texts.length > 0)
62
+ await requireLocalPipeline(this.model);
62
63
  const out = [];
63
64
  for (const text of texts) {
64
65
  out.push(await getEmbedding(text, this.model, role));
@@ -7,6 +7,8 @@ export type EmbeddingRole = 'query' | 'passage';
7
7
  export declare function prefixFor(model: string, role?: EmbeddingRole): string;
8
8
  /** Check (synchronously) if @xenova/transformers or @huggingface/transformers is installed. */
9
9
  export declare function isEmbeddingAvailable(): boolean;
10
+ /** Throws, with the reason, when the model's pipeline cannot load: a provider-level failure, not a per-item skip. */
11
+ export declare function requireLocalPipeline(model: string): Promise<void>;
10
12
  export declare function resolveEmbeddingModel(hippoRoot: string, explicitModel?: string): string;
11
13
  /** Embeds `text` with the local model, or returns `[]` when Transformers.js is missing or fails; `role` adds e5-style prefixes. */
12
14
  export declare function getEmbedding(text: string, model?: string, role?: EmbeddingRole): Promise<number[]>;
@@ -10,6 +10,7 @@ let _embeddingAvailable = null;
10
10
  // A pipeline is expensive to load, so one instance per model is kept for the process.
11
11
  const _pipelineInstances = new Map();
12
12
  const _pipelineLoading = new Map();
13
+ const _pipelineErrors = new Map();
13
14
  export const DEFAULT_EMBEDDING_MODEL = 'Xenova/all-MiniLM-L6-v2';
14
15
  /** BGE was trained with CLS pooling; everything else gets mean, the sentence-transformers default, since CLS degrades mean-trained models. */
15
16
  export function poolingFor(model) {
@@ -79,8 +80,10 @@ async function loadPipeline(model) {
79
80
  const loading = (async () => {
80
81
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
82
  const pkg = resolveTransformersPackage();
82
- if (!pkg)
83
+ if (!pkg) {
84
+ _pipelineErrors.set(model, 'no transformers package is installed');
83
85
  return null;
86
+ }
84
87
  let pipelineFn = null;
85
88
  try {
86
89
  // SAFETY: the resolved module's shape is untyped by design (optional peer
@@ -98,11 +101,16 @@ async function loadPipeline(model) {
98
101
  pipelineFn = mod.pipeline ?? mod.default?.pipeline;
99
102
  }
100
103
  catch (err) {
101
- log.debug(`transformers import failed (${pkg}): ${err instanceof Error ? err.message : String(err)}`);
104
+ // String(err) keeps a Node error's [ERR_...] code, which callers match on.
105
+ const reason = `transformers import failed (${pkg}): ${String(err)}`;
106
+ log.debug(reason);
107
+ _pipelineErrors.set(model, reason);
102
108
  return null;
103
109
  }
104
- if (!pipelineFn)
110
+ if (!pipelineFn) {
111
+ _pipelineErrors.set(model, `${pkg} exports no pipeline function`);
105
112
  return null;
113
+ }
106
114
  // The offline bundle used in egress-blocked sandboxes ships only the FP32 model, so use whichever file is on disk.
107
115
  const cacheRoot = envModelCache();
108
116
  const quantized = !cacheRoot
@@ -113,7 +121,9 @@ async function loadPipeline(model) {
113
121
  return instance;
114
122
  }
115
123
  catch (err) {
116
- log.debug(`embedding pipeline load failed (${model}): ${err instanceof Error ? err.message : String(err)}`);
124
+ const reason = `embedding pipeline load failed (${model}): ${String(err)}`;
125
+ log.debug(reason);
126
+ _pipelineErrors.set(model, reason);
117
127
  return null;
118
128
  }
119
129
  finally {
@@ -123,6 +133,12 @@ async function loadPipeline(model) {
123
133
  _pipelineLoading.set(model, loading);
124
134
  return loading;
125
135
  }
136
+ /** Throws, with the reason, when the model's pipeline cannot load: a provider-level failure, not a per-item skip. */
137
+ export async function requireLocalPipeline(model) {
138
+ if (await loadPipeline(model))
139
+ return;
140
+ throw new Error(`local embedding model did not load: ${_pipelineErrors.get(model) ?? 'unknown reason'}`);
141
+ }
126
142
  export function resolveEmbeddingModel(hippoRoot, explicitModel) {
127
143
  const direct = explicitModel?.trim();
128
144
  if (direct)
package/dist/version.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export declare const PACKAGE_VERSION = "1.64.0";
19
+ export declare const PACKAGE_VERSION = "1.64.1";
20
20
  /** The floor a store takes on its first expiring key: the first release with schema v53, set by hand in that release; scripts/check-expiring-keys-floor.mjs gates it. */
21
21
  export declare const EXPIRING_KEYS_MIN_BINARY = "1.64.0";
22
22
  /** The floor a store takes on its first session bind or owner snapshot: first release with v54; set by hand in that release. */
package/dist/version.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export const PACKAGE_VERSION = '1.64.0';
19
+ export const PACKAGE_VERSION = '1.64.1';
20
20
  /** The floor a store takes on its first expiring key: the first release with schema v53, set by hand in that release; scripts/check-expiring-keys-floor.mjs gates it. */
21
21
  export const EXPIRING_KEYS_MIN_BINARY = '1.64.0';
22
22
  /** The floor a store takes on its first session bind or owner snapshot: first release with v54; set by hand in that release. */
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
- "version": "1.64.0",
5
+ "version": "1.64.1",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.64.0",
3
+ "version": "1.64.1",
4
4
  "type": "module",
5
5
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
6
6
  "main": "index.ts",
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
- "version": "1.64.0",
5
+ "version": "1.64.1",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.64.0",
3
+ "version": "1.64.1",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
5
  "mcpName": "io.github.kitfunso/hippo-memory",
6
6
  "type": "module",