hippo-memory 1.58.0 → 1.59.0

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.
@@ -1,5 +1,16 @@
1
1
  import type { RerankerFn } from './types.js';
2
+ import type { SearchResult } from '../search.js';
2
3
  export declare const JEV_DEFAULT_TOP_K = 40;
4
+ /** The System One `state` and one `noul` question per candidate (`c1`..`cN`). */
5
+ export interface RelevanceRequest {
6
+ state: string;
7
+ questions: Record<string, {
8
+ type: 'noul';
9
+ instructions: string;
10
+ }>;
11
+ }
12
+ /** Redacted query plus numbered, redacted, truncated candidates. Shared with CLEF so both arms see matched input. */
13
+ export declare function buildRelevanceRequest(query: string, head: readonly SearchResult[]): RelevanceRequest;
3
14
  /** Builds a Jev reranker around the reranker it degrades to. Exported so a test can pass a stand-in. */
4
15
  export declare function createJevReranker(localFallback: RerankerFn): RerankerFn;
5
16
  /** Track 4 reranker: hosted TypeSafe Jev, opt-in and paid (TYPESAFE_API_KEY), one batched call per recall.
@@ -28,11 +28,8 @@ function parseScores(answers, n) {
28
28
  }
29
29
  return out;
30
30
  }
31
- /** One batched request for the whole candidate list. Rejects with the reason when there are no usable scores. */
32
- async function requestScores(query, head) {
33
- const key = process.env.TYPESAFE_API_KEY;
34
- if (!key)
35
- throw new Error('TYPESAFE_API_KEY not set');
31
+ /** Redacted query plus numbered, redacted, truncated candidates. Shared with CLEF so both arms see matched input. */
32
+ export function buildRelevanceRequest(query, head) {
36
33
  const lines = head.map((r, i) => `[${i + 1}] ${truncate(redactSecrets(r.entry.content), TRUNCATE_CHARS)}`);
37
34
  const state = `Query: ${redactSecrets(query)}\n\nNumbered candidate memories from an AI coding agent's project store:\n\n${lines.join('\n\n')}`;
38
35
  const questions = {};
@@ -42,6 +39,14 @@ async function requestScores(query, head) {
42
39
  instructions: `Probability that candidate ${i} (numbered in the state above) helps answer the query.`,
43
40
  };
44
41
  }
42
+ return { state, questions };
43
+ }
44
+ /** One batched request for the whole candidate list. Rejects with the reason when there are no usable scores. */
45
+ async function requestScores(query, head) {
46
+ const key = process.env.TYPESAFE_API_KEY;
47
+ if (!key)
48
+ throw new Error('TYPESAFE_API_KEY not set');
49
+ const { state, questions } = buildRelevanceRequest(query, head);
45
50
  const parsed = Number.parseInt(process.env.HIPPO_JEV_TIMEOUT_MS ?? '', 10);
46
51
  const timeoutMs = parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
47
52
  const controller = new AbortController();
@@ -26,6 +26,20 @@ export interface RerankerOptions {
26
26
  /** Per-track config blob; opaque to the seam. */
27
27
  config?: Record<string, RerankerConfigValue>;
28
28
  }
29
+ /** Which backend and model produced a rerank, or why it fell back. */
30
+ export interface RerankProvenance {
31
+ /** `cloudflare`, `private-endpoint`, or `native` when the input order was kept. */
32
+ backend: 'cloudflare' | 'private-endpoint' | 'native';
33
+ /** Model the caller asked for. */
34
+ requestedModel: string;
35
+ /** Model the provider says scored the request, when it reports one. */
36
+ actualModel?: string;
37
+ /** Set when the input order was kept instead of a model ranking. */
38
+ fallbackReason?: string;
39
+ /** Provider-reported token usage, when sent. */
40
+ inputTokens?: number;
41
+ outputTokens?: number;
42
+ }
29
43
  export interface RerankResult extends SearchResult {
30
44
  /** Score assigned by the reranker. Replaces `score` for downstream
31
45
  * ordering; original `score` preserved on the SearchResult. */
@@ -34,5 +48,7 @@ export interface RerankResult extends SearchResult {
34
48
  preRerankRank: number;
35
49
  /** 1-indexed rank in the reranker output. */
36
50
  postRerankRank: number;
51
+ /** Recorded by rerankers that track model identity (the CLEF rerankers). */
52
+ rerankProvenance?: RerankProvenance;
37
53
  }
38
54
  //# sourceMappingURL=types.d.ts.map
package/dist/server.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type IncomingMessage } from 'node:http';
2
2
  /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
3
3
  export declare function __resetSessionRecallHistoryHttp(): void;
4
+ import { authRevoke, type Actor, type Context } from './api.js';
5
+ import { ForbiddenError } from './api-errors.js';
6
+ export { authRevoke, ForbiddenError, type Context, type Actor };
4
7
  export interface ServerHandle {
5
8
  port: number;
6
9
  url: string;
@@ -15,7 +18,7 @@ export interface ServerHandle {
15
18
  export interface ResolvedBearer {
16
19
  tenantId: string;
17
20
  subject: string;
18
- /** Not 'admin' means 'member'. Admin is tenant-only, yet can mint member API keys (POST /v1/auth/keys) that outlive IdP deprovisioning. */
21
+ /** Not 'admin' means 'member'. Admin is tenant-only, yet can mint member API keys (POST /v1/auth/keys); an add-on revokes them through the exported authRevoke when the IdP deprovisions the minter. */
19
22
  role: 'admin' | 'member';
20
23
  scopes?: readonly string[];
21
24
  }
@@ -56,6 +59,8 @@ export declare function isCrossSite(req: IncomingMessage): boolean;
56
59
  * bucket per request and bypass the limiter entirely.
57
60
  */
58
61
  export declare function clientIpForRateLimit(req: IncomingMessage): string;
62
+ /** Add-ons call this to refuse a subject that would collide with a built-in actor. */
63
+ export declare function isReservedActor(subject: string): boolean;
59
64
  /**
60
65
  * Boot the HTTP daemon on host:port and write the pidfile under hippoRoot.
61
66
  *
package/dist/server.js CHANGED
@@ -39,7 +39,9 @@ import { handleMcpRequest } from './mcp/server.js';
39
39
  import { handleSlackEventsWebhook } from './connectors/slack/webhook.js';
40
40
  import { handleGitHubEventsWebhook } from './connectors/github/webhook.js';
41
41
  import { HttpError, JSON_HEADERS, BodyTooLargeError, isHeaderString, isJsonObjectRecord, mapApiError, readBody, sendJson, } from './http-util.js';
42
- import { NotFoundError } from './api-errors.js';
42
+ import { ForbiddenError, NotFoundError } from './api-errors.js';
43
+ // Add-on packages revoke keys through these without importing the whole api surface.
44
+ export { authRevoke, ForbiddenError };
43
45
  // Review patch #2: explicit allow-list for unauthenticated /v1/* routes.
44
46
  // New unauth routes MUST be added here AND get a corresponding entry in
45
47
  // tests/server-bearer-lockdown.test.ts. Do not gate auth elsewhere by
@@ -364,6 +366,11 @@ export function clientIpForRateLimit(req) {
364
366
  const RESERVED_ACTOR_NAMES = [
365
367
  'api_key', 'localhost', 'cli', 'system', 'mcp', 'connector', 'sleep', 'post-compact', 'recall', 'agent-memories',
366
368
  ];
369
+ /** Add-ons call this to refuse a subject that would collide with a built-in actor. */
370
+ export function isReservedActor(subject) {
371
+ const lower = subject.toLowerCase();
372
+ return RESERVED_ACTOR_NAMES.some((n) => lower === n || lower.startsWith(`${n}:`));
373
+ }
367
374
  function hasControlChar(s) {
368
375
  for (let i = 0; i < s.length; i++) {
369
376
  const c = s.charCodeAt(i);
@@ -388,8 +395,7 @@ function sanitiseResolved(r) {
388
395
  // Padding would let "system " pass the reserved-name check yet read as `system` in an audit log.
389
396
  if (hasControlChar(subject) || subject !== subject.trim())
390
397
  return null;
391
- const lower = subject.toLowerCase();
392
- if (RESERVED_ACTOR_NAMES.some((n) => lower === n || lower.startsWith(`${n}:`)))
398
+ if (isReservedActor(subject))
393
399
  return null;
394
400
  const clean = { tenantId: tenant, subject, role: role === 'admin' ? 'admin' : 'member' };
395
401
  if (Array.isArray(scopes))
package/dist/store.js CHANGED
@@ -180,9 +180,9 @@ export function serializeEntry(entry) {
180
180
  if (tenantId !== 'default') {
181
181
  frontmatter['tenant_id'] = tenantId;
182
182
  }
183
- // v39: origin_project '' (user-global) is meaningful and must round-trip;
184
- // only undefined/null (legacy/unstamped) is omitted.
185
- if (entry.origin_project !== undefined && entry.origin_project !== null) {
183
+ // v39: '' (user-global) and null (unknown, hidden by default) must both round-trip;
184
+ // only undefined (unstamped) is omitted, so a rebuild stamps nothing it can read.
185
+ if (entry.origin_project !== undefined) {
186
186
  frontmatter['origin_project'] = entry.origin_project;
187
187
  }
188
188
  // Spread into a fresh object literal: dumpFrontmatter's Record<string,
@@ -241,7 +241,7 @@ export function deserializeEntry(raw) {
241
241
  owner: data['owner'] === null || data['owner'] === undefined ? null : String(data['owner']),
242
242
  artifact_ref: data['artifact_ref'] === null || data['artifact_ref'] === undefined ? null : String(data['artifact_ref']),
243
243
  tenantId: data['tenant_id'] === null || data['tenant_id'] === undefined ? 'default' : String(data['tenant_id']),
244
- origin_project: data['origin_project'] === null || data['origin_project'] === undefined ? null : String(data['origin_project']),
244
+ origin_project: !('origin_project' in data) ? undefined : data['origin_project'] === null ? null : String(data['origin_project']),
245
245
  };
246
246
  }
247
247
  function normalizeStringArray(value) {
@@ -1174,7 +1174,7 @@ export function stampOriginProject(hippoRoot, entry) {
1174
1174
  return { ...entry, origin_project: deriveOriginProject(path.dirname(hippoRoot)) };
1175
1175
  }
1176
1176
  /**
1177
- * Import-time variant that ALSO stamps null: used only where evidence exists
1177
+ * Import-time variant for a mirror with no origin field (an explicit null stays null): used only where evidence exists
1178
1178
  * for rows that predate the origin column - the legacy-markdown bootstrap and
1179
1179
  * rebuildIndex import, which are the markdown-store equivalent of the v39 SQL
1180
1180
  * backfill. Same evidence order as the migration: the provenance source
@@ -1183,7 +1183,7 @@ export function stampOriginProject(hippoRoot, entry) {
1183
1183
  * owning project instead of becoming user-global (codex gating round 3 P1).
1184
1184
  */
1185
1185
  function stampOriginProjectForImport(hippoRoot, entry) {
1186
- if (entry.origin_project !== undefined && entry.origin_project !== null)
1186
+ if (entry.origin_project !== undefined)
1187
1187
  return entry;
1188
1188
  const fromSource = originFromSource(entry.source);
1189
1189
  return {
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.58.0";
19
+ export declare const PACKAGE_VERSION = "1.59.0";
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export declare function compareSemver(a: string, b: string): number;
22
22
  //# sourceMappingURL=version.d.ts.map
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.58.0';
19
+ export const PACKAGE_VERSION = '1.59.0';
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export function compareSemver(a, b) {
22
22
  const parse = (v) => {
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
5
- "version": "1.58.0",
5
+ "version": "1.59.0",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.58.0",
3
+ "version": "1.59.0",
4
4
  "type": "module",
5
5
  "description": "Hippo Memory plugin for OpenClaw - biologically-inspired agent memory",
6
6
  "main": "index.ts",
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
5
- "version": "1.58.0",
5
+ "version": "1.59.0",
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.58.0",
3
+ "version": "1.59.0",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. MCP server, hooks for Claude Code, OpenCode and Codex, AGENTS.md instructions for Codex, Cursor, OpenClaw and Pi. SQLite, zero runtime deps.",
5
5
  "mcpName": "io.github.kitfunso/hippo-memory",
6
6
  "type": "module",
@@ -34,6 +34,7 @@
34
34
  "pretest": "npm run build",
35
35
  "test": "vitest run",
36
36
  "test:watch": "vitest",
37
+ "test:coverage": "vitest run --coverage",
37
38
  "test:delivery-ledger": "vitest run tests/delivery-ledger",
38
39
  "lint": "oxlint",
39
40
  "sbom": "node scripts/sbom.mjs",
@@ -90,9 +91,11 @@
90
91
  "devDependencies": {
91
92
  "@oxlint/plugins": "^1.78.0",
92
93
  "@types/node": "^22.16.0",
94
+ "@vitest/coverage-v8": "^5.0.3",
93
95
  "oxlint": "^1.78.0",
94
96
  "typescript": "^5.4.0",
95
- "vitest": "^3.2.6"
97
+ "vite": "^8.3.2",
98
+ "vitest": "^5.0.3"
96
99
  },
97
100
  "peerDependenciesMeta": {
98
101
  "@xenova/transformers": {