hippo-memory 1.72.0 → 2.0.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.
Files changed (259) hide show
  1. package/README.md +6 -4
  2. package/dist/agent-memories/apply.js +5 -5
  3. package/dist/api/assemble.js +2 -1
  4. package/dist/api/audit.d.ts +7 -7
  5. package/dist/api/audit.js +19 -17
  6. package/dist/api/auth.d.ts +5 -6
  7. package/dist/api/auth.js +28 -33
  8. package/dist/api/conflicts.js +1 -1
  9. package/dist/api/context-auto.js +4 -2
  10. package/dist/api/context-select.js +8 -20
  11. package/dist/api/context.js +12 -10
  12. package/dist/api/drill-down.js +2 -1
  13. package/dist/api/forget.d.ts +2 -2
  14. package/dist/api/forget.js +6 -7
  15. package/dist/api/graph.js +2 -1
  16. package/dist/api/learn.d.ts +1 -1
  17. package/dist/api/learn.js +45 -44
  18. package/dist/api/memories.d.ts +4 -2
  19. package/dist/api/memories.js +16 -5
  20. package/dist/api/objects.js +2 -1
  21. package/dist/api/outcome.d.ts +5 -5
  22. package/dist/api/outcome.js +16 -15
  23. package/dist/api/predictions.js +2 -1
  24. package/dist/api/promote.d.ts +3 -3
  25. package/dist/api/promote.js +25 -27
  26. package/dist/api/prompt-hook.d.ts +3 -1
  27. package/dist/api/prompt-hook.js +1 -0
  28. package/dist/api/quarantine.js +2 -1
  29. package/dist/api/recall-core.js +8 -3
  30. package/dist/api/recall-pipeline.js +17 -17
  31. package/dist/api/recall-types.d.ts +3 -1
  32. package/dist/api/recall.js +32 -29
  33. package/dist/api/remember-local.d.ts +5 -2
  34. package/dist/api/remember-local.js +14 -6
  35. package/dist/api/remember.d.ts +5 -2
  36. package/dist/api/remember.js +16 -14
  37. package/dist/api/session-end.d.ts +24 -0
  38. package/dist/api/session-end.js +31 -1
  39. package/dist/api/sleep-run.js +5 -4
  40. package/dist/api/sleep.js +4 -3
  41. package/dist/api/tokens.js +1 -1
  42. package/dist/api/types.d.ts +1 -7
  43. package/dist/capture/command.js +1 -1
  44. package/dist/capture/compaction-spool.d.ts +3 -5
  45. package/dist/capture/compaction-spool.js +41 -46
  46. package/dist/capture/session-end-caller.js +1 -1
  47. package/dist/capture/session-worker.js +19 -6
  48. package/dist/cli/audit.js +10 -10
  49. package/dist/cli/auth.js +4 -4
  50. package/dist/cli/client.d.ts +1 -2
  51. package/dist/cli/client.js +13 -13
  52. package/dist/cli/context.js +1 -1
  53. package/dist/cli/curate.d.ts +1 -1
  54. package/dist/cli/curate.js +9 -9
  55. package/dist/cli/decisions.js +4 -5
  56. package/dist/cli/hook-runtime.d.ts +3 -1
  57. package/dist/cli/hook-runtime.js +2 -2
  58. package/dist/cli/init.d.ts +1 -1
  59. package/dist/cli/init.js +9 -9
  60. package/dist/cli/object-verbs.d.ts +2 -2
  61. package/dist/cli/object-verbs.js +4 -4
  62. package/dist/cli/predictions.js +2 -3
  63. package/dist/cli/quality-repair-once.d.ts +1 -1
  64. package/dist/cli/quality-repair-once.js +2 -2
  65. package/dist/cli/refine-llm.js +2 -1
  66. package/dist/cli/remember.js +5 -5
  67. package/dist/cli/serve.js +9 -4
  68. package/dist/cli/session-hooks.d.ts +0 -4
  69. package/dist/cli/session-hooks.js +57 -66
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -2
  72. package/dist/cli/shared.d.ts +1 -1
  73. package/dist/cli/shared.js +2 -2
  74. package/dist/cli/sleep.js +2 -2
  75. package/dist/cli/transfer.d.ts +3 -3
  76. package/dist/cli/transfer.js +34 -15
  77. package/dist/cli/usage.d.ts +1 -1
  78. package/dist/cli/usage.js +1 -1
  79. package/dist/cli/verb-row.d.ts +2 -0
  80. package/dist/cli/verbs/health.js +2 -0
  81. package/dist/cli/verbs/memory.js +43 -51
  82. package/dist/cli/verbs/objects.js +6 -3
  83. package/dist/cli/verbs/sessions.js +4 -2
  84. package/dist/cli.js +1 -2
  85. package/dist/connectors/dlq.js +2 -1
  86. package/dist/connectors/github/deletion.js +2 -1
  87. package/dist/connectors/github/ingest.js +2 -1
  88. package/dist/connectors/github/tenant-routing.js +2 -1
  89. package/dist/connectors/github/webhook.js +4 -2
  90. package/dist/connectors/ingest.js +2 -1
  91. package/dist/connectors/slack/deletion.js +2 -1
  92. package/dist/connectors/slack/ingest.js +2 -1
  93. package/dist/connectors/slack/tenant-routing.js +2 -1
  94. package/dist/consolidate/conflicts.js +1 -1
  95. package/dist/consolidate/dag.js +14 -9
  96. package/dist/consolidate/dedupe.js +1 -1
  97. package/dist/consolidate/memory-value.js +1 -1
  98. package/dist/consolidate/merge.js +6 -6
  99. package/dist/consolidate/sleep.js +1 -1
  100. package/dist/consolidate/traces.js +2 -2
  101. package/dist/core/config.js +15 -16
  102. package/dist/core/markdown.js +2 -2
  103. package/dist/core/memory.d.ts +4 -4
  104. package/dist/core/memory.js +10 -5
  105. package/dist/core/physics.d.ts +3 -1
  106. package/dist/core/physics.js +17 -2
  107. package/dist/core/project-identity.js +2 -1
  108. package/dist/core/search-types.d.ts +1 -1
  109. package/dist/dashboard/dashboard-actions.d.ts +2 -2
  110. package/dist/dashboard/dashboard-actions.js +5 -5
  111. package/dist/dashboard/dashboard-queries.js +2 -7
  112. package/dist/dashboard/dashboard-snapshot.js +1 -3
  113. package/dist/dashboard/dashboard.js +16 -5
  114. package/dist/db/connect.d.ts +2 -1
  115. package/dist/db/connect.js +2 -1
  116. package/dist/db/migrate.js +14 -7
  117. package/dist/db/migrations/v11.js +1 -0
  118. package/dist/db/migrations/v14.js +1 -0
  119. package/dist/db/physics-state.d.ts +0 -4
  120. package/dist/db/physics-state.js +5 -31
  121. package/dist/db/request-stores.d.ts +1 -0
  122. package/dist/db/request-stores.js +2 -1
  123. package/dist/db/vector-store.js +2 -1
  124. package/dist/doctor.d.ts +3 -0
  125. package/dist/doctor.js +7 -7
  126. package/dist/embeddings/local.js +2 -2
  127. package/dist/embeddings/provider.js +1 -10
  128. package/dist/entry/store-kit.d.ts +37 -0
  129. package/dist/entry/store-kit.js +25 -0
  130. package/dist/entry/testing.d.ts +6 -0
  131. package/dist/entry/testing.js +7 -0
  132. package/dist/eval/judgment.js +1 -1
  133. package/dist/hooks/json-hooks.js +6 -2
  134. package/dist/importers/core.js +3 -3
  135. package/dist/importers/vault.d.ts +1 -1
  136. package/dist/importers/vault.js +18 -18
  137. package/dist/index.d.ts +2 -2
  138. package/dist/index.js +1 -1
  139. package/dist/learn/autolearn.d.ts +6 -2
  140. package/dist/learn/autolearn.js +8 -6
  141. package/dist/learn/extract.js +1 -1
  142. package/dist/learn/invalidation.d.ts +2 -1
  143. package/dist/learn/invalidation.js +10 -5
  144. package/dist/mcp/memory-tools.d.ts +1 -1
  145. package/dist/mcp/memory-tools.js +6 -9
  146. package/dist/mcp/request.js +20 -19
  147. package/dist/mcp/stdio.js +13 -8
  148. package/dist/rerankers/cross-encoder.js +10 -3
  149. package/dist/search/embed-memories.d.ts +5 -2
  150. package/dist/search/embed-memories.js +40 -28
  151. package/dist/search/engine.d.ts +8 -0
  152. package/dist/search/engine.js +10 -0
  153. package/dist/search/vector.js +5 -5
  154. package/dist/server/auth.d.ts +1 -1
  155. package/dist/server/auth.js +7 -8
  156. package/dist/server/boot.d.ts +2 -1
  157. package/dist/server/boot.js +28 -19
  158. package/dist/server/client-ip.d.ts +9 -4
  159. package/dist/server/client-ip.js +35 -17
  160. package/dist/server/route-table.js +5 -6
  161. package/dist/server/routes/memories.js +4 -2
  162. package/dist/server/sleep-offload.js +3 -3
  163. package/dist/server/tls.d.ts +7 -2
  164. package/dist/server/tls.js +17 -5
  165. package/dist/server/types.d.ts +7 -2
  166. package/dist/server.d.ts +4 -41
  167. package/dist/server.js +3 -30
  168. package/dist/sharing/global-store.d.ts +4 -5
  169. package/dist/sharing/global-store.js +6 -11
  170. package/dist/sharing/global-sync.js +1 -1
  171. package/dist/sharing/search-both.js +9 -2
  172. package/dist/sharing/share.js +2 -4
  173. package/dist/store/ambient.js +2 -1
  174. package/dist/store/candidates.js +18 -8
  175. package/dist/store/cards.js +5 -4
  176. package/dist/store/compactions-record.js +8 -7
  177. package/dist/store/conflicts.d.ts +7 -1
  178. package/dist/store/conflicts.js +6 -5
  179. package/dist/store/connectors/dlq.js +2 -1
  180. package/dist/store/connectors/event-log.d.ts +13 -0
  181. package/dist/store/connectors/event-log.js +24 -0
  182. package/dist/store/connectors/github.js +6 -8
  183. package/dist/store/connectors/slack.js +6 -10
  184. package/dist/store/dashboard-reads.js +1 -0
  185. package/dist/store/delete-and-batch.js +5 -4
  186. package/dist/store/delivery-recorder.d.ts +2 -2
  187. package/dist/store/delivery-recorder.js +1 -6
  188. package/dist/store/descendant-page.js +2 -1
  189. package/dist/store/dormant.js +7 -3
  190. package/dist/store/entry-reads.d.ts +25 -3
  191. package/dist/store/entry-reads.js +43 -12
  192. package/dist/store/entry-row.js +9 -2
  193. package/dist/store/entry-writes.d.ts +10 -7
  194. package/dist/store/entry-writes.js +23 -11
  195. package/dist/store/goals.js +3 -2
  196. package/dist/store/graph-reads.js +5 -4
  197. package/dist/store/graph-view-rows.js +2 -1
  198. package/dist/store/half-life.js +2 -2
  199. package/dist/store/handoffs.d.ts +9 -3
  200. package/dist/store/handoffs.js +4 -3
  201. package/dist/store/index.d.ts +2 -2
  202. package/dist/store/index.js +1 -1
  203. package/dist/store/mirrors.js +9 -4
  204. package/dist/store/open.d.ts +2 -1
  205. package/dist/store/open.js +2 -1
  206. package/dist/store/port.d.ts +59 -3
  207. package/dist/store/quality-repair.js +1 -1
  208. package/dist/store/recall-trace.js +2 -1
  209. package/dist/store/rejected-values.js +14 -9
  210. package/dist/store/rows.js +2 -2
  211. package/dist/store/rule-sql.d.ts +2 -0
  212. package/dist/store/rule-sql.js +12 -6
  213. package/dist/store/search-rows.d.ts +5 -5
  214. package/dist/store/search-rows.js +9 -12
  215. package/dist/store/set-aside.js +1 -1
  216. package/dist/store/sqlite/connector-writes-group.js +3 -1
  217. package/dist/store/sqlite/entry-writes-group.d.ts +3 -3
  218. package/dist/store/sqlite/entry-writes-group.js +1 -1
  219. package/dist/store/sqlite/held-connection.d.ts +2 -0
  220. package/dist/store/sqlite/held-connection.js +1 -1
  221. package/dist/store/sqlite/last-recall-group.d.ts +13 -0
  222. package/dist/store/sqlite/last-recall-group.js +57 -0
  223. package/dist/store/sqlite/local.d.ts +0 -17
  224. package/dist/store/sqlite/local.js +0 -31
  225. package/dist/store/sqlite/maintenance-groups.d.ts +6 -0
  226. package/dist/store/sqlite/maintenance-groups.js +60 -0
  227. package/dist/store/sqlite/objects-group.js +14 -6
  228. package/dist/store/sqlite/store.d.ts +1 -8
  229. package/dist/store/sqlite/store.js +13 -24
  230. package/dist/store/summaries.d.ts +7 -0
  231. package/dist/store/summaries.js +49 -7
  232. package/dist/store/token-ledger-rows.js +3 -2
  233. package/dist/store/vector-index.js +5 -2
  234. package/dist/util/anthropic-messages.d.ts +3 -1
  235. package/dist/util/anthropic-messages.js +4 -2
  236. package/dist/util/atomic-write.d.ts +5 -2
  237. package/dist/util/atomic-write.js +12 -8
  238. package/dist/util/env.d.ts +10 -1
  239. package/dist/util/env.js +10 -1
  240. package/dist/util/format.d.ts +2 -0
  241. package/dist/util/format.js +4 -0
  242. package/dist/util/request-scope.d.ts +6 -2
  243. package/dist/util/request-scope.js +7 -3
  244. package/dist/util/spawn-argv.d.ts +9 -0
  245. package/dist/util/spawn-argv.js +57 -0
  246. package/dist/util/sql.d.ts +3 -0
  247. package/dist/util/sql.js +6 -0
  248. package/dist/util/vector-codec.d.ts +4 -0
  249. package/dist/util/vector-codec.js +15 -0
  250. package/dist/util/version.d.ts +1 -1
  251. package/dist/util/version.js +1 -1
  252. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  253. package/extensions/openclaw-plugin/package.json +1 -1
  254. package/openclaw.plugin.json +1 -1
  255. package/package.json +3 -1
  256. package/dist/api/on-store.d.ts +0 -16
  257. package/dist/api/on-store.js +0 -38
  258. package/dist/cli/lenient-id.d.ts +0 -2
  259. package/dist/cli/lenient-id.js +0 -13
@@ -1,12 +1,14 @@
1
1
  // Transport-agnostic request handling: tool dispatch table, tool execution and the JSON-RPC method switch.
2
2
  import { errorMessage, log } from '../util/log.js';
3
+ import { currentSignal } from '../util/request-scope.js';
3
4
  import { STORE_NOT_PORTED_MESSAGE } from '../util/http-util.js';
4
5
  import { getGlobalRoot, initGlobal } from '../sharing/global-store.js';
5
6
  import { loadConfig } from '../core/config.js';
6
7
  import { resolveTenantId } from '../store/tenant.js';
7
8
  import { runWithRequestStores } from '../db/index.js';
8
9
  import { rethrowIfSqliteBlocked } from '../util/sqlite-blocked.js';
9
- import { hasGroup, storeFor } from '../store/index.js';
10
+ import { hasGroup } from '../store/index.js';
11
+ import { storeFor } from '../store/sqlite/store.js';
10
12
  import { estimateTokens } from '../util/token-text.js';
11
13
  import { PACKAGE_VERSION } from '../util/version.js';
12
14
  import { validateToolArgs } from './tool-args.js';
@@ -62,20 +64,15 @@ const TOOL_HANDLERS = new Map([
62
64
  ['hippo_outcome', { handler: runOutcomeTool, storeReady: 'entryWrites' }],
63
65
  ['hippo_context', { handler: runContextTool, storeReady: 'contextReads' }],
64
66
  ['hippo_status', { handler: runStatusTool }],
65
- ['hippo_learn', { handler: runLearnTool }],
67
+ ['hippo_learn', { handler: runLearnTool, storeReady: 'lessonWrites' }],
66
68
  ['hippo_conflicts', { handler: runConflictsTool }],
67
69
  ['hippo_resolve', { handler: runResolveTool }],
68
70
  ['hippo_share', { handler: runShareTool }],
69
71
  ['hippo_peers', { handler: runPeersTool }],
70
72
  ]);
71
- /** The served store when it is not hippo.db, else null. */
72
- function otherStore(ctx) {
73
- const store = ctx?.store;
74
- return store !== undefined && store.kind !== 'sqlite' ? store : null;
75
- }
73
+ /** Stdio serves no store and runs every tool on the folder's hippo.db. */
76
74
  function runsOn(store, toolName) {
77
- const group = TOOL_HANDLERS.get(toolName)?.storeReady;
78
- return group !== undefined && hasGroup(store, group);
75
+ return store === undefined || hasGroup(store, TOOL_HANDLERS.get(toolName)?.storeReady ?? 'localFiles');
79
76
  }
80
77
  async function executeTool(name, args, ctx) {
81
78
  // With a transport context (HTTP), trust it: hippoRoot comes from the server's bound opts and tenantId from the Bearer (or loopback fallback).
@@ -100,6 +97,17 @@ function invalidArgs(id, toolName, problems) {
100
97
  result: { content: [{ type: 'text', text: `Invalid arguments for ${toolName}: ${problems.join('; ')}` }], isError: true },
101
98
  };
102
99
  }
100
+ /** One handle per store for the tool and its ledger row; stdio interleaves calls, so each gets its own scope.
101
+ * SHORTCUT: the deadline stops a call only between these steps, not a store read or fetch already running; thread the signal into them. */
102
+ function runToolScoped(toolName, args, ctx) {
103
+ return runWithRequestStores(async () => {
104
+ currentSignal()?.throwIfAborted();
105
+ const text = await executeTool(toolName, args, ctx);
106
+ currentSignal()?.throwIfAborted();
107
+ await recordMcpTokens(toolName, text, ctx);
108
+ return text;
109
+ });
110
+ }
103
111
  async function callTool(id, params, ctx) {
104
112
  const nameValue = params?.name;
105
113
  const toolName = isJsonString(nameValue) ? nameValue : '';
@@ -108,8 +116,7 @@ async function callTool(id, params, ctx) {
108
116
  return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${toolName.slice(0, 128)}` } };
109
117
  }
110
118
  // The same refusal a ported tool gives when it reaches hippo.db, so a client handles one shape.
111
- const other = otherStore(ctx);
112
- if (other && !runsOn(other, toolName)) {
119
+ if (!runsOn(ctx?.store, toolName)) {
113
120
  return { jsonrpc: '2.0', id, error: { code: -32603, message: STORE_NOT_PORTED_MESSAGE } };
114
121
  }
115
122
  const refusal = sharedStoreRefusal(toolName, ctx);
@@ -125,12 +132,7 @@ async function callTool(id, params, ctx) {
125
132
  return invalidArgs(id, toolName, problems);
126
133
  let output;
127
134
  try {
128
- // One handle per store for the tool and its ledger row; stdio interleaves calls, so each gets its own scope.
129
- output = await runWithRequestStores(async () => {
130
- const text = await executeTool(toolName, toolArgs, ctx);
131
- await recordMcpTokens(toolName, text, ctx);
132
- return text;
133
- });
135
+ output = await runToolScoped(toolName, toolArgs, ctx);
134
136
  }
135
137
  catch (err) {
136
138
  if (!(err instanceof RecallRequestError))
@@ -163,8 +165,7 @@ export async function handleMcpRequest(req, ctx) {
163
165
  case 'notifications/initialized':
164
166
  return null;
165
167
  case 'tools/list': {
166
- const other = otherStore(ctx);
167
- return { jsonrpc: '2.0', id, result: { tools: other ? TOOLS.filter((t) => runsOn(other, t.name)) : TOOLS } };
168
+ return { jsonrpc: '2.0', id, result: { tools: TOOLS.filter((t) => runsOn(ctx?.store, t.name)) } };
168
169
  }
169
170
  case 'tools/call':
170
171
  return callTool(id, params, ctx);
package/dist/mcp/stdio.js CHANGED
@@ -75,7 +75,7 @@ function callName(req) {
75
75
  }
76
76
  /** Answers `req` with one timeout error at the deadline a served request has. The returned function stops the timer and says whether that
77
77
  * answer went out, so a later reply is dropped. */
78
- function watchDeadline(req, owedReply) {
78
+ function watchDeadline(req, owedReply, abort) {
79
79
  const ms = requestDeadlineMs();
80
80
  if (ms === 0)
81
81
  return () => false;
@@ -83,7 +83,8 @@ function watchDeadline(req, owedReply) {
83
83
  const timer = setTimeout(() => {
84
84
  expired = true;
85
85
  const name = callName(req);
86
- log.warn(`mcp: ${name} did not answer within ${ms} ms; it keeps running and its reply will be dropped`, { method: req.method });
86
+ log.warn(`mcp: ${name} did not answer within ${ms} ms; its next step is cancelled and its reply will be dropped`, { method: req.method });
87
+ abort.abort(new DOMException(`${name} did not finish within ${ms} ms`, 'TimeoutError'));
87
88
  if (!owedReply)
88
89
  return;
89
90
  const requestId = currentRequestId();
@@ -100,7 +101,7 @@ function watchDeadline(req, owedReply) {
100
101
  return expired;
101
102
  };
102
103
  }
103
- function dispatch(body) {
104
+ function dispatch(body, abort) {
104
105
  let parsed;
105
106
  try {
106
107
  parsed = JSON.parse(body);
@@ -116,17 +117,19 @@ function dispatch(body) {
116
117
  }
117
118
  const req = parsed;
118
119
  if (req.method.startsWith('notifications/')) {
119
- const stopWatching = watchDeadline(req, false);
120
+ const stopWatching = watchDeadline(req, false, abort);
120
121
  track(req.method, handleMcpRequest(req).then(() => { }).catch((err) => {
121
122
  log.error(`mcp notification ${req.method} failed: ${errorMessage(err)}`, errorFields(err));
122
123
  }).finally(stopWatching));
123
124
  return;
124
125
  }
125
126
  // A frame with no id is a notification under any method name, and JSON-RPC forbids answering one.
126
- const timedOut = watchDeadline(req, isJsonString(req.id) || isJsonNumber(req.id));
127
+ const timedOut = watchDeadline(req, isJsonString(req.id) || isJsonNumber(req.id), abort);
127
128
  track(req.method, handleMcpRequest(req).then((resp) => { if (!timedOut() && resp)
128
129
  send(resp); }).catch((err) => {
129
- if (timedOut())
130
+ if (timedOut() && err instanceof DOMException && err.name === 'TimeoutError')
131
+ log.debug(`mcp: ${callName(req)} stopped at its deadline`);
132
+ else if (timedOut())
130
133
  log.error(`mcp: ${callName(req)} failed after its timeout reply: ${errorMessage(err)}`, errorFields(err));
131
134
  else
132
135
  send(mcpErrorResponse(req.id, err, undefined, callName(req)));
@@ -158,8 +161,10 @@ export function startStdioLoop() {
158
161
  continue;
159
162
  }
160
163
  // One id per frame, so the error reply and every line logged for the call name the same request.
161
- if (result.kind === 'message')
162
- runWithRequestId(randomUUID(), () => dispatch(result.body));
164
+ if (result.kind === 'message') {
165
+ const abort = new AbortController();
166
+ runWithRequestId(randomUUID(), () => dispatch(result.body, abort), undefined, abort.signal);
167
+ }
163
168
  }
164
169
  pending = buffer.length > 0 ? [buffer] : [];
165
170
  pendingBytes = buffer.length;
@@ -48,10 +48,12 @@ function identityOrder(head) {
48
48
  }));
49
49
  }
50
50
  async function scoreHead(pipe, query, head) {
51
+ let succeeded = 0;
51
52
  const scored = await Promise.all(head.map(async (r, i) => {
52
53
  let ceScore;
53
54
  try {
54
55
  ceScore = await pipe(query, r.entry.content);
56
+ succeeded++;
55
57
  }
56
58
  catch (err) {
57
59
  // One bad inference must not sink the whole pass.
@@ -69,7 +71,7 @@ async function scoreHead(pipe, query, head) {
69
71
  // relevance order, never an arbitrary content order.
70
72
  scored.sort((a, b) => compareScoresDesc(a.rerankScore, b.rerankScore));
71
73
  scored.forEach((r, i) => (r.postRerankRank = i + 1));
72
- return scored;
74
+ return { results: scored, succeeded };
73
75
  }
74
76
  /** Track 2 reranker: MS-MARCO MiniLM cross-encoder, identity fallback if the model will not load. Each instance owns its model and outage warning. */
75
77
  export function createCrossEncoderReranker(importModule = importTransformers) {
@@ -88,8 +90,13 @@ export function createCrossEncoderReranker(importModule = importTransformers) {
88
90
  outage.failed(errorMessage(err), errorFields(err));
89
91
  return identityOrder(head);
90
92
  }
91
- outage.answered();
92
- return scoreHead(pipe, query, head);
93
+ const { results: scored, succeeded } = await scoreHead(pipe, query, head);
94
+ // A loaded model whose every call throws is as dead as one that never loaded.
95
+ if (head.length > 0 && succeeded === 0)
96
+ outage.failed('every inference failed');
97
+ else
98
+ outage.answered();
99
+ return scored;
93
100
  };
94
101
  }
95
102
  //# sourceMappingURL=cross-encoder.js.map
@@ -1,8 +1,11 @@
1
1
  import type { MemoryEntry } from '../core/memory.js';
2
2
  import { type EmbeddingProvider } from '../embeddings/provider.js';
3
3
  import { type HippoStore } from '../store/port.js';
4
- /** Embed a single memory entry and cache the result in the embedding index; with `store`, through its vectorWrites, never opening hippo.db. */
5
- export declare function embedMemory(hippoRoot: string, entry: MemoryEntry, model?: string, store?: HippoStore): Promise<void>;
4
+ /** `off` is a provider that is disabled or not installed, which is a choice; `failed` is a memory that stays out of the vector index. */
5
+ export type EmbedOutcome = 'indexed' | 'off' | 'failed';
6
+ /** Embed a single memory entry and cache the result in the embedding index; with `store`, through its vectorWrites, never opening hippo.db.
7
+ * Resolves whatever fails, so a caller that fires and forgets stays safe; the outcome says whether the memory reached the index. */
8
+ export declare function embedMemory(hippoRoot: string, entry: MemoryEntry, model?: string, store?: HippoStore): Promise<EmbedOutcome>;
6
9
  /** Embeds every entry with no cached vector, prunes vectors of deleted memories, and returns how many it embedded; `provider` defaults to the
7
10
  * configured one. With `store`, every tenant's memories with no vector for the provider's model go through its `vectorWrites`, never hippo.db. */
8
11
  export declare function embedAll(hippoRoot: string, model?: string, provider?: EmbeddingProvider, store?: HippoStore): Promise<number>;
@@ -4,7 +4,7 @@ import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { randomUUID } from 'crypto';
6
6
  import { loadConfig } from '../core/config.js';
7
- import { initializeParticle } from '../db/physics-state.js';
7
+ import { initializeParticle } from '../core/physics.js';
8
8
  import { resolveEmbeddingProvider } from '../embeddings/provider.js';
9
9
  import { embeddingIndexIdentity, embeddingInputText, embeddingModelRequiresReindex, indexNeedsRebuild, saveStoredEmbeddingModel, } from '../store/embeddings/index.js';
10
10
  import { loadAllEntryIds, loadEntriesByIds } from '../store/entry-reads.js';
@@ -191,7 +191,7 @@ async function writeVectorPage(writes, write, page, vectors, withParticles) {
191
191
  if (!vector || vector.length === 0)
192
192
  return noteSkippedEmbedding(entry.id);
193
193
  const row = withParticles ? { memoryId: entry.id, vector, particle: initializeParticle(entry, vector) } : { memoryId: entry.id, vector };
194
- byTenant.set(entry.tenantId, [...(byTenant.get(entry.tenantId) ?? []), row]);
194
+ byTenant.set(entry.tenant_id, [...(byTenant.get(entry.tenant_id) ?? []), row]);
195
195
  });
196
196
  let written = 0;
197
197
  for (const [tenantId, rows] of byTenant) {
@@ -237,15 +237,20 @@ function embedMemoryInStore(store, provider, entry) {
237
237
  || (await writeVectorPage(writes, { model, replaceIndex: false }, [entry], await provider.embed([embeddingInputText(entry)], 'passage'), true)).modelMismatch;
238
238
  if (refused)
239
239
  warnEmbedFailureOnce('index', OTHER_MODEL_INDEX);
240
+ return refused ? 'failed' : 'indexed';
240
241
  }
241
242
  catch (err) {
242
243
  warnEmbedFailureOnce(provider.kind, errorMessage(err), err);
244
+ return 'failed';
243
245
  }
244
246
  }).catch((err) => {
245
247
  log.warn(`skipped embedding ${entry.id} (${errorMessage(err)})`);
248
+ return 'failed';
246
249
  });
247
250
  }
248
- /** Embed a single memory entry and cache the result in the embedding index; with `store`, through its vectorWrites, never opening hippo.db. */
251
+ const INLINE_REINDEX_MAX_ROWS = 256;
252
+ /** Embed a single memory entry and cache the result in the embedding index; with `store`, through its vectorWrites, never opening hippo.db.
253
+ * Resolves whatever fails, so a caller that fires and forgets stays safe; the outcome says whether the memory reached the index. */
249
254
  export async function embedMemory(hippoRoot, entry, model, store) {
250
255
  let provider;
251
256
  try {
@@ -254,38 +259,45 @@ export async function embedMemory(hippoRoot, entry, model, store) {
254
259
  catch (err) {
255
260
  // Callers fire and forget, so this must resolve: a bad config warns once instead of rejecting.
256
261
  warnEmbedFailureOnce('config', errorMessage(err), err);
257
- return;
262
+ return 'failed';
258
263
  }
259
264
  if (!provider.isAvailable())
260
- return;
265
+ return 'off';
261
266
  if (store)
262
267
  return embedMemoryInStore(store, provider, entry);
263
- return withEmbedLock(hippoRoot, async () => {
264
- // Best-effort: an embedding failure (API down, bad key, 5xx) must not reject the caller's write; the existing index is left as-is.
265
- // Failures surface on the explicit `hippo embed` / `embedAll` path.
266
- try {
267
- const identity = provider.id;
268
- if (embeddingModelRequiresReindex(hippoRoot, identity)) {
269
- await rebuildIndexForProvider(hippoRoot, provider);
270
- return;
271
- }
272
- const text = embeddingInputText(entry);
273
- const [vector] = await provider.embed([text], 'passage');
274
- if (!vector || vector.length === 0)
275
- return;
276
- saveStoredVectors(hippoRoot, [[entry.id, vector]], embeddingIndexIdentity(identity));
277
- saveStoredEmbeddingModel(hippoRoot, identity);
278
- // Initialize physics state for this memory
279
- initializePhysicsIfMissing(hippoRoot, entry, vector);
280
- }
281
- catch (err) {
282
- // Provider failure (API down / bad key). Best-effort: leave the index as-is, but say so once.
283
- warnEmbedFailureOnce(provider.kind, errorMessage(err), err);
284
- }
285
- }).catch((err) => {
268
+ return withEmbedLock(hippoRoot, () => embedMemoryLocked(hippoRoot, provider, entry)).catch((err) => {
286
269
  log.warn(`skipped embedding ${entry.id} (${errorMessage(err)}); run 'hippo embed' to backfill`);
270
+ return 'failed';
287
271
  });
288
272
  }
273
+ /** Best-effort: an embedding failure (API down, bad key, 5xx) must not reject the caller's write; the existing index is left as-is.
274
+ * Failures surface on the explicit `hippo embed` / `embedAll` path. */
275
+ async function embedMemoryLocked(hippoRoot, provider, entry) {
276
+ try {
277
+ const identity = provider.id;
278
+ if (embeddingModelRequiresReindex(hippoRoot, identity)) {
279
+ // A fire-and-forget write heals a small index inline; a large one is left to `hippo embed`, so no write holds the lock for a whole-store embed.
280
+ if (loadAllEntryIds(hippoRoot).length > INLINE_REINDEX_MAX_ROWS) {
281
+ warnEmbedFailureOnce('index', OTHER_MODEL_INDEX);
282
+ return 'failed';
283
+ }
284
+ await rebuildIndexForProvider(hippoRoot, provider);
285
+ return 'indexed';
286
+ }
287
+ const [vector] = await provider.embed([embeddingInputText(entry)], 'passage');
288
+ if (!vector || vector.length === 0)
289
+ return 'failed';
290
+ saveStoredVectors(hippoRoot, [[entry.id, vector]], embeddingIndexIdentity(identity));
291
+ saveStoredEmbeddingModel(hippoRoot, identity);
292
+ initializePhysicsIfMissing(hippoRoot, entry, vector);
293
+ return 'indexed';
294
+ }
295
+ catch (err) {
296
+ // Provider failure (API down / bad key). Best-effort: leave the index as-is, but say so once.
297
+ warnEmbedFailureOnce(provider.kind, errorMessage(err), err);
298
+ return 'failed';
299
+ }
300
+ }
289
301
  // Every page is embedded before one transaction replaces the index, and the identity is saved only after that: a provider failure or a crash
290
302
  // at any earlier point leaves the old identity, so embeddingModelRequiresReindex has the next run rebuild from the start.
291
303
  async function rebuildIndexForProvider(hippoRoot, provider) {
@@ -0,0 +1,8 @@
1
+ import type { MemoryEntry } from '../core/memory.js';
2
+ import type { PhysicsConfig } from '../core/physics-config.js';
3
+ import type { SearchResult } from '../core/search-types.js';
4
+ import { type HybridSearchOptions } from './hybrid.js';
5
+ import type { VectorSource } from './vector.js';
6
+ /** Physics scoring when `physics` holds its config, hybrid otherwise, both on the store `source` names; each caller keeps its own options. */
7
+ export declare function searchByEngine(source: VectorSource, query: string, pool: MemoryEntry[], physics: PhysicsConfig | null, options: Omit<HybridSearchOptions, 'hippoRoot' | 'store'>): Promise<SearchResult[]>;
8
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1,10 @@
1
+ // The one search-engine choice every ranker over a candidate pool makes: recall's band, MCP's wide list, the CLI core and context.
2
+ import { hybridSearch } from './hybrid.js';
3
+ import { physicsSearchIn } from './physics-search.js';
4
+ /** Physics scoring when `physics` holds its config, hybrid otherwise, both on the store `source` names; each caller keeps its own options. */
5
+ export function searchByEngine(source, query, pool, physics, options) {
6
+ return physics
7
+ ? physicsSearchIn(source, query, pool, { ...options, physicsConfig: physics })
8
+ : hybridSearch(query, pool, { ...options, hippoRoot: source.root, store: source.store });
9
+ }
10
+ //# sourceMappingURL=engine.js.map
@@ -5,7 +5,7 @@ import { resolveEmbeddingProvider } from '../embeddings/provider.js';
5
5
  import { rethrowIfSqliteBlocked } from '../util/sqlite-blocked.js';
6
6
  import { errorMessage, log } from '../util/log.js';
7
7
  import { envQueryEmbedTimeoutMs } from '../util/env.js';
8
- import { requireGroup } from '../store/index.js';
8
+ import { hasGroup, requireGroup } from '../store/index.js';
9
9
  import { redactSecretsStrict } from '../util/secret-detect.js';
10
10
  import { currentEntries } from './as-of.js';
11
11
  // Jev's default budget (rerankers/jev.ts): each is one off-box call that a recall waits on.
@@ -33,12 +33,12 @@ export async function embedQueryBy(deadline, provider, query) {
33
33
  function warnBm25Fallback(reason, detail) {
34
34
  log.once(`search.bm25-fallback.${reason}`, 'warn', `hybrid search fell back to BM25 only: ${redactSecretsStrict(detail)}`);
35
35
  }
36
- function reindexHint(storeKind) {
36
+ function reindexHint(store) {
37
37
  const why = 'the embedding index was built by another model or is being rebuilt';
38
38
  // `hippo embed` writes only hippo.db, so another store gets its vectors by a rebuild from it.
39
- return storeKind === 'sqlite'
39
+ return hasGroup(store, 'localFiles')
40
40
  ? `${why}; run 'hippo embed'`
41
- : `${why}; run 'hippo embed' on the SQLite store, then rebuild the '${storeKind}' database from it`;
41
+ : `${why}; run 'hippo embed' on the SQLite store, then rebuild the '${store.kind}' database from it`;
42
42
  }
43
43
  /** The store's vector reads; a store without them answers 501, as any unported path does. */
44
44
  export function requireVectorReads(store) {
@@ -76,7 +76,7 @@ async function fillVectorArm(arm, query, { root, store }, options) {
76
76
  const reads = requireVectorReads(store);
77
77
  const index = await reads.embeddingIndexState();
78
78
  if (indexNeedsRebuild(indexedModel(index), provider.id)) {
79
- warnBm25Fallback('reindex', reindexHint(store.kind));
79
+ warnBm25Fallback('reindex', reindexHint(store));
80
80
  return;
81
81
  }
82
82
  const spec = options.vectorCandidates;
@@ -16,7 +16,7 @@ type AuthHeader = {
16
16
  token: string;
17
17
  };
18
18
  export declare function readAuthHeader(req: IncomingMessage): AuthHeader;
19
- type AuthOpts = Pick<ResolvedServeOpts, 'hippoRoot' | 'authResolver' | 'authResolverTimeoutMs' | 'store' | 'callerLimiter'> & Partial<Pick<ResolvedServeOpts, 'failedAuthLimiter'>>;
19
+ type AuthOpts = Pick<ResolvedServeOpts, 'hippoRoot' | 'authResolver' | 'authResolverTimeoutMs' | 'store' | 'callerLimiter'> & Partial<Pick<ResolvedServeOpts, 'failedAuthLimiter' | 'keylessLocalRole'>>;
20
20
  /** Add-ons call this to refuse a subject that would collide with a built-in actor. */
21
21
  export declare function isReservedActor(subject: string): boolean;
22
22
  /** Key cap for every serve() bucket, shared by the warn map so it never tracks more callers than the buckets do. */
@@ -1,5 +1,5 @@
1
1
  // Bearer and loopback auth for the HTTP server.
2
- import { envAllowKeylessLocal, envRequireAuth } from '../util/env.js';
2
+ import { envAllowKeylessLocal, envKeylessLocalRole, envRequireAuth } from '../util/env.js';
3
3
  import { resolveTenantId } from '../store/tenant.js';
4
4
  import { errorFields, errorMessage, log } from '../util/log.js';
5
5
  import { API_KEY_PREFIX, verifyApiKeyCached } from '../store/auth.js';
@@ -253,16 +253,15 @@ export async function buildContextWithAuth(req, opts) {
253
253
  export async function recheckContextWithAuth(req, opts) {
254
254
  return contextFor(await checkAuth(req, opts), opts);
255
255
  }
256
+ /** The keyless local caller: any process on this machine can reach loopback, so it is a member unless the operator names admin. */
257
+ function keylessLocalActor(opts) {
258
+ const role = opts.keylessLocalRole ?? envKeylessLocalRole();
259
+ return role === 'admin' ? { subject: 'localhost:cli', role: 'admin', hostAdmin: true } : { subject: 'localhost:cli', role: 'member' };
260
+ }
256
261
  function contextFor(id, opts) {
257
262
  if (id !== null)
258
263
  return { hippoRoot: opts.hippoRoot, tenantId: id.tenantId, actor: bearerActor(id), store: opts.store };
259
- // The keyless local fallback (HIPPO_ALLOW_KEYLESS_LOCAL=1) is this machine's own user, so it is host admin.
260
- return {
261
- hippoRoot: opts.hippoRoot,
262
- tenantId: resolveTenantId({}),
263
- actor: { subject: 'localhost:cli', role: 'admin', hostAdmin: true },
264
- store: opts.store,
265
- };
264
+ return { hippoRoot: opts.hippoRoot, tenantId: resolveTenantId({}), actor: keylessLocalActor(opts), store: opts.store };
266
265
  }
267
266
  /** Auth check for routes that need no tenant Context (e.g. the MCP transport, which resolves its own root): throws HttpError 401 like buildContextWithAuth.
268
267
  * A keyless local request passes only under HIPPO_ALLOW_KEYLESS_LOCAL=1. */
@@ -1,5 +1,6 @@
1
1
  import type { ServeOpts, ServerHandle } from './types.js';
2
- /** Boots the HTTP daemon on host:port and writes the pidfile under hippoRoot; refuses non-loopback hosts unless HIPPO_REQUIRE_AUTH=1.
2
+ /** Boots the HTTP daemon on host:port and writes the pidfile under hippoRoot; refuses a non-loopback host unless HIPPO_REQUIRE_AUTH=1 and
3
+ * either `tls` or allowCleartext is given, and refuses HIPPO_CLIENT_IP_HEADER without HIPPO_TRUSTED_PROXIES.
3
4
  * Use port: 0 in tests for an ephemeral port. See docs/ARCHITECTURE.md#srcserverbootts. */
4
5
  export declare function serve(opts: ServeOpts): Promise<ServerHandle>;
5
6
  //# sourceMappingURL=boot.d.ts.map
@@ -1,6 +1,7 @@
1
- import { envPort, envRequireAuth, envV1Rps } from '../util/env.js';
1
+ import { envAllowCleartext, envCallerRps, envPort, envRequireAuth, envV1Rps } from '../util/env.js';
2
2
  import { detectServer, removePidfileIfOwned, writePidfile } from './server-detect.js';
3
3
  import { outsideSqliteOffLoop, runWithRequestStores, SERVER_DB_WAIT_MS, withSqliteBlocked } from '../db/index.js';
4
+ import { hasGroup } from '../store/index.js';
4
5
  import { isStoreBusy } from '../store/sqlite/busy.js';
5
6
  import { holdSqliteConnection } from '../store/sqlite/held-connection.js';
6
7
  import { workerSqliteStore } from '../store/sqlite/worker-store.js';
@@ -14,7 +15,7 @@ import { RecallContractError } from '../api/index.js';
14
15
  import { BodyTimeoutError, BodyTooLargeError, closeAfterReply, DeadlineExceededError, HttpError, JSON_HEADERS, sendJson } from '../util/http-util.js';
15
16
  import { workerCounts } from '../store/sqlite/executor-counts.js';
16
17
  import { isLocalCaller, LIMITER_MAX_KEYS } from './auth.js';
17
- import { enforceRateLimit, warnIfClientIpHeaderUnpinned } from './client-ip.js';
18
+ import { assertClientIpHeaderPinned, enforceRateLimit } from './client-ip.js';
18
19
  import { answerAtDeadline, handlerDeadlineCount, isAbandoned, requestDeadlineFor } from './deadline.js';
19
20
  import { DEFAULT_SHUTDOWN_DRAIN_MS, drainAndClose, setKeepAliveTimeouts, shutdownBoundMs } from './lifecycle.js';
20
21
  import { readyProbeFor } from './ready.js';
@@ -23,7 +24,7 @@ import { installCrashHandlers, installSignalHandlers } from '../util/crash-handl
23
24
  import { handleMcpPost, handleMcpStream } from './mcp-http.js';
24
25
  import { MCP_PROJECT_SCOPED_HEADER } from '../core/project-identity.js';
25
26
  import { accessRouteOf, logRequestFailure, noteAccess, openRequest, parseRequest, rejectEncodedSlash, replyFor, sendError } from './request.js';
26
- import { createListener, warnIfCleartext } from './tls.js';
27
+ import { assertTlsOrCleartextAllowed, createListener } from './tls.js';
27
28
  import { assertAddonRoutes, assertPublicJson } from './route-checks.js';
28
29
  import { dispatchAddonRoute, dispatchPublicJson, dispatchV1Route } from './route-table.js';
29
30
  // server.address() returns AddressInfo once a TCP socket is bound; null before
@@ -156,27 +157,37 @@ function assertRateLimitSpec(name, { ratePerSec, burst }) {
156
157
  if (!Number.isFinite(burst) || burst < 1)
157
158
  throw new Error(`rateLimits.${name}.burst must be a finite number of at least 1`);
158
159
  }
160
+ const DEFAULT_CALLER_RPS = 20;
161
+ /** The per-caller spec when serve() names none: HIPPO_CALLER_RPS (0 is off), else 20 per second with a burst of twice that. */
162
+ function defaultCallerSpec() {
163
+ const raw = envCallerRps();
164
+ const rps = raw === undefined ? DEFAULT_CALLER_RPS : Number(raw);
165
+ // A typo must not switch the limit off, so only an explicit 0 does.
166
+ if (!Number.isFinite(rps) || rps < 0)
167
+ throw new Error(`HIPPO_CALLER_RPS must be a number of at least 0 (0 turns the per-caller limit off), not '${raw}'`);
168
+ return rps === 0 ? 'off' : { ratePerSec: rps, burst: Math.max(1, rps * 2) };
169
+ }
159
170
  /** Refuses a bad spec at boot, since a NaN or zero rate would otherwise turn a bucket off or refuse every request. */
160
171
  function bootLimiters(rateLimits) {
161
- const { perCaller, perAddress, failedAuthPerAddress = DEFAULT_FAILED_AUTH } = rateLimits ?? {};
162
- if (perCaller !== undefined)
172
+ const { perCaller = defaultCallerSpec(), perAddress, failedAuthPerAddress = DEFAULT_FAILED_AUTH } = rateLimits ?? {};
173
+ if (perCaller !== 'off')
163
174
  assertRateLimitSpec('perCaller', perCaller);
164
175
  if (perAddress !== undefined && perAddress !== 'off')
165
176
  assertRateLimitSpec('perAddress', perAddress);
166
177
  assertRateLimitSpec('failedAuthPerAddress', failedAuthPerAddress);
167
- warnIfClientIpHeaderUnpinned();
178
+ assertClientIpHeaderPinned();
168
179
  return {
169
180
  perAddress: bootRateLimiter(perAddress),
170
- callerLimiter: perCaller === undefined ? undefined : limiterFor(perCaller),
181
+ callerLimiter: perCaller === 'off' ? undefined : limiterFor(perCaller),
171
182
  failedAuthLimiter: limiterFor(failedAuthPerAddress),
172
183
  };
173
184
  }
174
185
  function createStoreHolder(hippoRoot, store) {
175
- if (store.kind !== 'sqlite') {
176
- log.info(`serve: the '${store.kind}' store serves ${hippoRoot}, so no hippo.db connection is held`);
177
- return { hold: () => { }, afterResponse: () => { }, release: async () => { } };
178
- }
179
- return holdSqliteConnection(hippoRoot);
186
+ if (hasGroup(store, 'localFiles'))
187
+ return holdSqliteConnection(hippoRoot);
188
+ log.info(`serve: the '${store.kind}' store serves ${hippoRoot}, so no hippo.db connection is held`);
189
+ // A missed port under another store would otherwise create and write a hippo.db that store never reads.
190
+ return { hold: () => { }, afterResponse: () => { }, release: async () => { }, runRequest: (run) => withSqliteBlocked(store.kind, run) };
180
191
  }
181
192
  // A plugin's resolver may read hippo.db through this package's synchronous functions, so it runs outside the block of a `loop: 'off'` route.
182
193
  function outsideRouteBlock(resolver) {
@@ -287,7 +298,8 @@ function exitOnSignalOrCrash(stop, drainMs) {
287
298
  installSignalHandlers('serve', shutdown);
288
299
  installCrashHandlers('serve', shutdown);
289
300
  }
290
- /** Boots the HTTP daemon on host:port and writes the pidfile under hippoRoot; refuses non-loopback hosts unless HIPPO_REQUIRE_AUTH=1.
301
+ /** Boots the HTTP daemon on host:port and writes the pidfile under hippoRoot; refuses a non-loopback host unless HIPPO_REQUIRE_AUTH=1 and
302
+ * either `tls` or allowCleartext is given, and refuses HIPPO_CLIENT_IP_HEADER without HIPPO_TRUSTED_PROXIES.
291
303
  * Use port: 0 in tests for an ephemeral port. See docs/ARCHITECTURE.md#srcserverbootts. */
292
304
  export async function serve(opts) {
293
305
  const host = opts.host ?? DEFAULT_SERVER_HOST;
@@ -296,6 +308,7 @@ export async function serve(opts) {
296
308
  const publicJsonBodies = assertPublicJson(opts.publicJson ?? {});
297
309
  const limiters = bootLimiters(opts.rateLimits);
298
310
  assertBindable(host);
311
+ assertTlsOrCleartextAllowed(host, opts.tls, LOOPBACK_HOSTS, opts.allowCleartext ?? envAllowCleartext());
299
312
  await assertNoLiveServer(opts.hippoRoot);
300
313
  // Single source of truth for the start time: returned by GET /health and written into the pidfile, so detectServer can prove a pid-reusing impostor is not
301
314
  // this server.
@@ -304,7 +317,7 @@ export async function serve(opts) {
304
317
  const streamSlots = new Map();
305
318
  const served = servedOptsFor(opts, routes, publicJsonBodies, limiters);
306
319
  // A store other than hippo.db is a team's central server, so its folder's config.json must not decide shared-ness.
307
- if (served.store.kind !== 'sqlite')
320
+ if (!hasGroup(served.store, 'localFiles'))
308
321
  markSharedStore(opts.hippoRoot);
309
322
  const holder = createStoreHolder(opts.hippoRoot, served.store);
310
323
  const inflight = new Set();
@@ -343,14 +356,11 @@ function servedOptsFor(opts, routes, publicJsonBodies, { callerLimiter, failedAu
343
356
  };
344
357
  }
345
358
  function acceptingServer(served, holder, inflight, options) {
346
- const { kind } = served.store;
347
359
  return createListener(served.tls, (req, res) => {
348
360
  res.once('finish', holder.afterResponse);
349
361
  inflight.add(res);
350
362
  res.once('close', () => inflight.delete(res));
351
- const run = () => handleRequest(req, res, served, options);
352
- // A missed port under another store would otherwise create and write a hippo.db that store never reads.
353
- answerRequest(req, res, () => (kind === 'sqlite' ? run() : withSqliteBlocked(kind, run)), served.slowRequestWarnMs);
363
+ answerRequest(req, res, () => holder.runRequest(() => handleRequest(req, res, served, options)), served.slowRequestWarnMs);
354
364
  });
355
365
  }
356
366
  async function listenAndDescribe(server, port, host, tls) {
@@ -362,7 +372,6 @@ async function listenAndDescribe(server, port, host, tls) {
362
372
  const addressInfo = address;
363
373
  const actualPort = addressInfo.port;
364
374
  const url = `${tls ? 'https' : 'http'}://${host.includes(':') ? `[${host}]` : host}:${actualPort}`;
365
- warnIfCleartext(host, tls, LOOPBACK_HOSTS);
366
375
  return { port: actualPort, url };
367
376
  }
368
377
  async function stopListening(opts, server, inflight, startedAt) {
@@ -2,11 +2,16 @@ import type { IncomingMessage } from 'node:http';
2
2
  import type { RateLimiter } from './rate-limit.js';
3
3
  /**
4
4
  * Rate-limit key: the socket's remote address, which behind a TLS proxy collapses into one bucket unauthenticated traffic can drain.
5
- * Set HIPPO_CLIENT_IP_HEADER (fly-client-ip on Fly) only when a trusted proxy fronts EVERY request, or clients could mint a fresh bucket per request.
6
- * HIPPO_TRUSTED_PROXIES (IPs or CIDRs) pins which peers count as that proxy; in a comma-joined chain the key is the rightmost hop no trusted proxy added. */
5
+ * HIPPO_CLIENT_IP_HEADER (fly-client-ip on Fly) is read only from a peer in HIPPO_TRUSTED_PROXIES (IPs or CIDRs, or `*` for any peer),
6
+ * so a caller that reaches the port past the proxy cannot pick its own bucket. In a comma-joined chain the key is the rightmost hop no trusted proxy added. */
7
7
  export declare function clientIpForRateLimit(req: IncomingMessage): string;
8
- /** Said once at boot: with no proxy list, a caller that reaches the port without the proxy picks its own bucket by sending the header. */
9
- export declare function warnIfClientIpHeaderUnpinned(): void;
8
+ /** serve() refused HIPPO_CLIENT_IP_HEADER with no HIPPO_TRUSTED_PROXIES: the header would be read from no one, so the setting is a mistake. */
9
+ export declare class UntrustedClientIpHeaderError extends Error {
10
+ readonly header: string;
11
+ constructor(header: string);
12
+ }
13
+ /** Checked at boot: a client-IP header needs a trusted-proxy list, and `*` is said once, since it believes any peer. */
14
+ export declare function assertClientIpHeaderPinned(): void;
10
15
  /** The one key every per-address limit and slot count uses, so rotating inside a /64 never buys a fresh bucket. */
11
16
  export declare function clientLimitKey(req: IncomingMessage): string;
12
17
  export declare function enforceRateLimit(req: IncomingMessage, path: string, limiter?: RateLimiter): void;