hippo-memory 1.63.2 → 1.64.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 (275) hide show
  1. package/README.md +10 -4
  2. package/dist/agent-memories/claude-code.js +3 -3
  3. package/dist/ambient-store.d.ts +2 -0
  4. package/dist/ambient-store.js +4 -3
  5. package/dist/api/assemble.d.ts +2 -0
  6. package/dist/api/assemble.js +11 -7
  7. package/dist/api/auth.d.ts +15 -11
  8. package/dist/api/auth.js +116 -22
  9. package/dist/api/context-select.d.ts +5 -3
  10. package/dist/api/context-select.js +6 -11
  11. package/dist/api/context-types.d.ts +2 -1
  12. package/dist/api/context.d.ts +1 -1
  13. package/dist/api/context.js +79 -55
  14. package/dist/api/dormant.d.ts +5 -5
  15. package/dist/api/dormant.js +13 -9
  16. package/dist/api/drill-down.d.ts +2 -0
  17. package/dist/api/drill-down.js +12 -7
  18. package/dist/api/forget.d.ts +1 -1
  19. package/dist/api/forget.js +8 -4
  20. package/dist/api/learn.js +5 -3
  21. package/dist/api/outcome.d.ts +1 -1
  22. package/dist/api/outcome.js +2 -1
  23. package/dist/api/promote.js +8 -5
  24. package/dist/api/recall-types.d.ts +4 -2
  25. package/dist/api/recall.js +51 -36
  26. package/dist/api/remember.d.ts +7 -0
  27. package/dist/api/remember.js +26 -2
  28. package/dist/api/sleep-run.d.ts +28 -0
  29. package/dist/api/sleep-run.js +267 -0
  30. package/dist/api/sleep.d.ts +5 -45
  31. package/dist/api/sleep.js +5 -252
  32. package/dist/api/types.d.ts +5 -0
  33. package/dist/api/types.js +4 -0
  34. package/dist/audit-prune.js +1 -1
  35. package/dist/audit.d.ts +1 -3
  36. package/dist/audit.js +7 -103
  37. package/dist/auth.d.ts +48 -4
  38. package/dist/auth.js +111 -42
  39. package/dist/autolearn.js +2 -2
  40. package/dist/capture/caller-session.d.ts +16 -0
  41. package/dist/capture/caller-session.js +58 -0
  42. package/dist/capture/command.d.ts +19 -1
  43. package/dist/capture/command.js +28 -21
  44. package/dist/capture/compact-resume-caller.d.ts +11 -0
  45. package/dist/capture/compact-resume-caller.js +35 -0
  46. package/dist/capture/compact.d.ts +0 -14
  47. package/dist/capture/compact.js +7 -160
  48. package/dist/capture/compaction-items-caller.d.ts +16 -0
  49. package/dist/capture/compaction-items-caller.js +32 -0
  50. package/dist/capture/extract.d.ts +2 -14
  51. package/dist/capture/extract.js +135 -346
  52. package/dist/capture/failure-caller.d.ts +21 -0
  53. package/dist/capture/failure-caller.js +74 -0
  54. package/dist/capture/failure-reading.d.ts +44 -0
  55. package/dist/capture/failure-reading.js +78 -0
  56. package/dist/capture/pre-compact-caller.d.ts +16 -0
  57. package/dist/capture/pre-compact-caller.js +39 -0
  58. package/dist/capture/session-end-caller.d.ts +21 -0
  59. package/dist/capture/session-end-caller.js +30 -0
  60. package/dist/capture/session-texts.d.ts +14 -0
  61. package/dist/capture/session-texts.js +42 -0
  62. package/dist/capture/transcript.d.ts +6 -1
  63. package/dist/capture/transcript.js +20 -16
  64. package/dist/capture/working-state.d.ts +21 -0
  65. package/dist/capture/working-state.js +154 -0
  66. package/dist/capture-error.d.ts +8 -22
  67. package/dist/capture-error.js +9 -61
  68. package/dist/cli/audit.js +25 -0
  69. package/dist/cli/auth.js +4 -3
  70. package/dist/cli/context.js +13 -158
  71. package/dist/cli/continuity.js +2 -1
  72. package/dist/cli/init.js +5 -3
  73. package/dist/cli/last-sleep.d.ts +6 -0
  74. package/dist/cli/last-sleep.js +68 -0
  75. package/dist/cli/quality-repair-once.d.ts +3 -0
  76. package/dist/cli/quality-repair-once.js +36 -0
  77. package/dist/cli/recall.js +2 -1
  78. package/dist/cli/remember.d.ts +2 -0
  79. package/dist/cli/remember.js +4 -1
  80. package/dist/cli/session-hooks.d.ts +0 -3
  81. package/dist/cli/session-hooks.js +12 -49
  82. package/dist/cli/setup.js +10 -2
  83. package/dist/cli/shared.d.ts +7 -14
  84. package/dist/cli/shared.js +19 -78
  85. package/dist/cli/sleep.js +6 -3
  86. package/dist/cli/usage.js +18 -9
  87. package/dist/cli.d.ts +1 -1
  88. package/dist/cli.js +3 -2
  89. package/dist/client.js +1 -1
  90. package/dist/compaction-record.d.ts +19 -1
  91. package/dist/compaction-record.js +61 -132
  92. package/dist/compaction-spool.d.ts +45 -0
  93. package/dist/compaction-spool.js +454 -0
  94. package/dist/config.d.ts +8 -0
  95. package/dist/config.js +49 -0
  96. package/dist/connectors/github/deletion.d.ts +1 -1
  97. package/dist/connectors/github/deletion.js +5 -17
  98. package/dist/consolidate/conflicts.js +4 -0
  99. package/dist/consolidate/flush-units.d.ts +9 -0
  100. package/dist/consolidate/flush-units.js +63 -0
  101. package/dist/consolidate/llm-passes.js +5 -4
  102. package/dist/consolidate/merge.js +5 -1
  103. package/dist/consolidate/run.d.ts +2 -1
  104. package/dist/consolidate/sleep.d.ts +2 -0
  105. package/dist/consolidate/sleep.js +82 -15
  106. package/dist/context-render.d.ts +14 -0
  107. package/dist/context-render.js +28 -0
  108. package/dist/dag.js +6 -0
  109. package/dist/dashboard-actions.js +3 -2
  110. package/dist/dashboard.js +4 -3
  111. package/dist/db/backup.d.ts +6 -0
  112. package/dist/db/backup.js +29 -0
  113. package/dist/db/busy.d.ts +9 -0
  114. package/dist/db/busy.js +35 -0
  115. package/dist/db/continuity.d.ts +1 -0
  116. package/dist/db/continuity.js +31 -4
  117. package/dist/db/meta.d.ts +2 -0
  118. package/dist/db/meta.js +7 -0
  119. package/dist/db/migrate.js +2 -3
  120. package/dist/db/migrations/index.d.ts +1 -1
  121. package/dist/db/migrations/index.js +5 -1
  122. package/dist/db/migrations/v39.js +2 -7
  123. package/dist/db/migrations/v53.d.ts +3 -0
  124. package/dist/db/migrations/v53.js +16 -0
  125. package/dist/db/migrations/v54.d.ts +3 -0
  126. package/dist/db/migrations/v54.js +25 -0
  127. package/dist/db/open.d.ts +11 -0
  128. package/dist/db/open.js +27 -0
  129. package/dist/db/tables.d.ts +2 -0
  130. package/dist/db/tables.js +6 -2
  131. package/dist/db.d.ts +2 -2
  132. package/dist/db.js +2 -2
  133. package/dist/doctor.d.ts +1 -1
  134. package/dist/doctor.js +31 -15
  135. package/dist/dormant.d.ts +16 -20
  136. package/dist/dormant.js +24 -30
  137. package/dist/entry/json-hooks.d.ts +3 -0
  138. package/dist/entry/json-hooks.js +4 -0
  139. package/dist/entry/project-identity.d.ts +2 -0
  140. package/dist/entry/project-identity.js +3 -0
  141. package/dist/entry/session-text.d.ts +7 -0
  142. package/dist/entry/session-text.js +8 -0
  143. package/dist/extract.js +6 -0
  144. package/dist/failure-log.d.ts +10 -2
  145. package/dist/failure-log.js +19 -2
  146. package/dist/gated-write.js +1 -1
  147. package/dist/graph/delta.d.ts +44 -0
  148. package/dist/graph/delta.js +109 -0
  149. package/dist/graph/types.d.ts +5 -0
  150. package/dist/graph/write.d.ts +20 -20
  151. package/dist/graph/write.js +103 -36
  152. package/dist/graph-extract.d.ts +43 -9
  153. package/dist/graph-extract.js +140 -157
  154. package/dist/graph-recall.d.ts +1 -0
  155. package/dist/graph-recall.js +2 -2
  156. package/dist/graph-view.d.ts +1 -0
  157. package/dist/graph-view.js +25 -2
  158. package/dist/half-life-migration.js +5 -1
  159. package/dist/handoff-evidence.d.ts +5 -0
  160. package/dist/handoff-evidence.js +30 -0
  161. package/dist/home-path.d.ts +5 -0
  162. package/dist/home-path.js +24 -0
  163. package/dist/hooks/json-hooks.d.ts +13 -2
  164. package/dist/hooks/json-hooks.js +246 -117
  165. package/dist/hooks/opencode.js +1 -1
  166. package/dist/hooks/shared.d.ts +2 -0
  167. package/dist/hooks/shared.js +6 -1
  168. package/dist/http-util.d.ts +16 -7
  169. package/dist/http-util.js +65 -20
  170. package/dist/importers/vault.js +2 -0
  171. package/dist/index.d.ts +2 -1
  172. package/dist/index.js +1 -0
  173. package/dist/json.d.ts +2 -0
  174. package/dist/json.js +7 -0
  175. package/dist/ledger-db.d.ts +10 -0
  176. package/dist/ledger-db.js +42 -0
  177. package/dist/mcp/admin-tools.d.ts +2 -2
  178. package/dist/mcp/admin-tools.js +47 -5
  179. package/dist/mcp/memory-tools.js +7 -2
  180. package/dist/mcp/protocol.d.ts +6 -0
  181. package/dist/mcp/protocol.js +4 -2
  182. package/dist/mcp/recall-tools.js +19 -6
  183. package/dist/mcp/request.js +4 -0
  184. package/dist/mcp/session-state.d.ts +4 -0
  185. package/dist/mcp/session-state.js +15 -6
  186. package/dist/mcp/shared-gate.d.ts +4 -0
  187. package/dist/mcp/shared-gate.js +22 -0
  188. package/dist/mcp/tools.js +1 -0
  189. package/dist/memory-quality.d.ts +35 -0
  190. package/dist/memory-quality.js +191 -0
  191. package/dist/memory.d.ts +4 -4
  192. package/dist/physics-state.js +1 -1
  193. package/dist/pilot-arm.d.ts +14 -3
  194. package/dist/pilot-arm.js +32 -10
  195. package/dist/postinstall.js +2 -2
  196. package/dist/project-briefs.js +4 -4
  197. package/dist/project-identity.d.ts +14 -10
  198. package/dist/project-identity.js +29 -11
  199. package/dist/project-merge.d.ts +0 -2
  200. package/dist/project-merge.js +9 -13
  201. package/dist/prompt-hook.d.ts +65 -0
  202. package/dist/prompt-hook.js +213 -0
  203. package/dist/prompt-recall.js +1 -1
  204. package/dist/quality-repair.d.ts +27 -0
  205. package/dist/quality-repair.js +220 -0
  206. package/dist/rate-limit.d.ts +1 -0
  207. package/dist/rate-limit.js +5 -3
  208. package/dist/raw-archive.d.ts +1 -1
  209. package/dist/raw-archive.js +6 -16
  210. package/dist/recall-scope.d.ts +28 -5
  211. package/dist/recall-scope.js +52 -7
  212. package/dist/recall-trace.js +1 -1
  213. package/dist/reject-flow.d.ts +2 -0
  214. package/dist/reject-flow.js +20 -6
  215. package/dist/secret-detect.js +7 -8
  216. package/dist/server/auth.d.ts +8 -6
  217. package/dist/server/auth.js +85 -38
  218. package/dist/server/client-ip.d.ts +2 -0
  219. package/dist/server/client-ip.js +16 -1
  220. package/dist/server/mcp-http.d.ts +9 -3
  221. package/dist/server/mcp-http.js +56 -13
  222. package/dist/server/request.d.ts +1 -1
  223. package/dist/server/request.js +5 -5
  224. package/dist/server/routes/admin.js +12 -6
  225. package/dist/server/routes/memories.js +9 -1
  226. package/dist/server/routes/recall.js +14 -2
  227. package/dist/server/types.d.ts +42 -5
  228. package/dist/server/validation.d.ts +5 -2
  229. package/dist/server/validation.js +19 -2
  230. package/dist/server.d.ts +21 -4
  231. package/dist/server.js +207 -49
  232. package/dist/session-digest.d.ts +0 -1
  233. package/dist/session-digest.js +3 -12
  234. package/dist/session-owners.d.ts +7 -0
  235. package/dist/session-owners.js +54 -0
  236. package/dist/share-scrub.d.ts +3 -0
  237. package/dist/share-scrub.js +8 -0
  238. package/dist/shared.d.ts +1 -0
  239. package/dist/shared.js +26 -18
  240. package/dist/store/candidates.d.ts +14 -3
  241. package/dist/store/candidates.js +48 -26
  242. package/dist/store/conflicts.d.ts +15 -0
  243. package/dist/store/conflicts.js +68 -44
  244. package/dist/store/delete-and-batch.d.ts +25 -1
  245. package/dist/store/delete-and-batch.js +121 -69
  246. package/dist/store/entry-reads.d.ts +5 -6
  247. package/dist/store/entry-reads.js +24 -12
  248. package/dist/store/entry-row.d.ts +7 -10
  249. package/dist/store/entry-row.js +40 -18
  250. package/dist/store/entry-writes.d.ts +3 -3
  251. package/dist/store/entry-writes.js +9 -23
  252. package/dist/store/handoffs.d.ts +9 -6
  253. package/dist/store/handoffs.js +72 -59
  254. package/dist/store/index-and-stats.js +2 -2
  255. package/dist/store/open.d.ts +3 -1
  256. package/dist/store/open.js +3 -3
  257. package/dist/store/search-rows.d.ts +8 -5
  258. package/dist/store/search-rows.js +15 -18
  259. package/dist/store/sessions.d.ts +25 -6
  260. package/dist/store/sessions.js +61 -19
  261. package/dist/store/summaries.d.ts +1 -1
  262. package/dist/store/summaries.js +3 -18
  263. package/dist/store/tenant-lookup.d.ts +7 -3
  264. package/dist/store/tenant-lookup.js +9 -9
  265. package/dist/store-port.d.ts +13 -0
  266. package/dist/store-port.js +20 -0
  267. package/dist/version.d.ts +5 -1
  268. package/dist/version.js +5 -1
  269. package/dist/working-memory.js +1 -1
  270. package/dist/write-budget.d.ts +15 -0
  271. package/dist/write-budget.js +15 -0
  272. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  273. package/extensions/openclaw-plugin/package.json +1 -1
  274. package/openclaw.plugin.json +1 -1
  275. package/package.json +8 -4
package/dist/http-util.js CHANGED
@@ -11,50 +11,95 @@ export function isHeaderString(value) {
11
11
  // 1 MB body cap. The CLI never sends payloads near this; anything bigger is
12
12
  // almost certainly a misconfigured client or a deliberate memory-blowup attempt.
13
13
  const MAX_BODY_BYTES = 1024 * 1024;
14
+ function sizeLabel(bytes) {
15
+ return bytes % (1024 * 1024) === 0 ? `${bytes / (1024 * 1024)}MB` : `${Math.ceil(bytes / 1024)}KB`;
16
+ }
17
+ // How long a refused upload is still read and discarded after its reply, so a client that never stops cannot hold the socket.
18
+ const REFUSED_UPLOAD_LINGER_MS = 2000;
14
19
  // Cap for id-shaped request fields (ids, tenant, session, scope, class): far above real values, small enough to bound logs and indexes.
15
20
  export const MAX_ID_LEN = 256;
16
21
  export const JSON_HEADERS = { 'content-type': 'application/json' };
17
22
  export class HttpError extends Error {
18
23
  status;
19
- constructor(status, message) {
24
+ /** Sent as the Retry-After header, so a refused caller knows when to come back. */
25
+ retryAfterSec;
26
+ constructor(status, message, retryAfterSec) {
20
27
  super(message);
21
28
  this.status = status;
29
+ this.retryAfterSec = retryAfterSec;
22
30
  }
23
31
  }
24
32
  export class BodyTooLargeError extends Error {
25
33
  }
34
+ /** The body did not arrive in time. Its own class so mapApiError answers 408 and the server drops the socket. */
35
+ export class BodyTimeoutError extends Error {
36
+ }
26
37
  export const INTERNAL_ERROR_MESSAGE = 'internal server error';
27
38
  /** Maps by class so rewording a message never moves a status; an untyped error is a 500 whose text stays in the server log. */
28
39
  export function mapApiError(err) {
40
+ // An add-on can build an HttpError from any number, and writeHead throws on one outside 100-999.
41
+ if (err instanceof HttpError && !(Number.isInteger(err.status) && err.status >= 100 && err.status <= 999)) {
42
+ return { status: 500, message: INTERNAL_ERROR_MESSAGE };
43
+ }
29
44
  if (err instanceof HttpError || err instanceof ApiError)
30
45
  return { status: err.status, message: err.message };
31
46
  if (err instanceof BodyTooLargeError)
32
47
  return { status: 413, message: err.message };
48
+ if (err instanceof BodyTimeoutError)
49
+ return { status: 408, message: err.message };
33
50
  return { status: 500, message: INTERNAL_ERROR_MESSAGE };
34
51
  }
52
+ /** Serialises before the head goes out, so a body that is not JSON still reaches the caller's error reply. */
35
53
  export function sendJson(res, status, body) {
54
+ const text = JSON.stringify(body);
55
+ if (text === undefined)
56
+ throw new Error('response body is not JSON');
36
57
  res.writeHead(status, JSON_HEADERS);
37
- res.end(JSON.stringify(body));
58
+ res.end(text);
38
59
  }
39
- /**
40
- * Read the entire request body into a Buffer. Caps at MAX_BODY_BYTES to keep
41
- * a malicious or buggy client from exhausting memory. The cap is enforced
42
- * mid-stream so we don't wait for an attacker to finish before erroring out.
43
- */
44
- export async function readBody(req) {
45
- const chunks = [];
46
- let total = 0;
47
- for await (const chunk of req) {
48
- // SAFETY: IncomingMessage never runs setEncoding() here, so every
49
- // streamed chunk is a Buffer, not a decoded string.
50
- const buf = chunk;
51
- total += buf.length;
52
- if (total > MAX_BODY_BYTES) {
53
- throw new BodyTooLargeError('request body exceeds 1MB');
60
+ /** The body as text, refused mid-stream past maxBytes and past deadlineMs, so an oversized or slow sender cannot tie up the server. */
61
+ export function readBody(req, { maxBytes = MAX_BODY_BYTES, deadlineMs } = {}) {
62
+ return new Promise((resolve, reject) => {
63
+ const chunks = [];
64
+ let total = 0;
65
+ let timer;
66
+ const refuse = (err) => {
67
+ clearTimeout(timer);
68
+ req.off('data', onData);
69
+ chunks.length = 0;
70
+ reject(err);
71
+ };
72
+ // Not for-await: leaving that loop early destroys the request, so the socket stops reading and the 413 caller cannot close it cleanly.
73
+ const onData = (chunk) => {
74
+ total += chunk.length;
75
+ if (total <= maxBytes) {
76
+ chunks.push(chunk);
77
+ return;
78
+ }
79
+ refuse(new BodyTooLargeError(`request body exceeds ${sizeLabel(maxBytes)}`));
80
+ };
81
+ if (deadlineMs !== undefined) {
82
+ timer = setTimeout(() => refuse(new BodyTimeoutError(`request body not received within ${deadlineMs} ms`)), deadlineMs);
54
83
  }
55
- chunks.push(buf);
56
- }
57
- return Buffer.concat(chunks).toString('utf8');
84
+ req.on('data', onData);
85
+ req.once('end', () => {
86
+ clearTimeout(timer);
87
+ resolve(Buffer.concat(chunks).toString('utf8'));
88
+ });
89
+ req.on('error', (err) => {
90
+ clearTimeout(timer);
91
+ reject(err);
92
+ });
93
+ });
94
+ }
95
+ // A full close with request bytes unread sends a TCP reset, which discards the reply before the client reads it (RFC 9112 9.6).
96
+ export function closeAfterReply(req) {
97
+ const socket = req.socket;
98
+ req.resume();
99
+ socket.end();
100
+ const timer = setTimeout(() => socket.destroy(), REFUSED_UPLOAD_LINGER_MS);
101
+ timer.unref();
102
+ socket.once('close', () => clearTimeout(timer));
58
103
  }
59
104
  // Any other Host on a loopback socket is DNS rebinding: a hostile page resolved to 127.0.0.1.
60
105
  export const LOOPBACK_HOST_HEADER = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i;
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
4
4
  import { createMemory } from '../memory.js';
5
5
  import { initStore } from '../store/open.js';
6
6
  import { remember, archiveRaw, isPrivateScope } from '../api.js';
7
+ import { assertClientScope } from '../recall-scope.js';
7
8
  import { openHippoDb, closeHippoDb } from '../db.js';
8
9
  import { RejectedValueError, checkRejectionGuard } from '../rejection.js';
9
10
  import { loadConfig } from '../config.js';
@@ -118,6 +119,7 @@ function vaultIdentityOrThrow(options) {
118
119
  // isPrivateScope as the single source of truth: reject a scope that names a
119
120
  // `private` segment yet is NOT a valid `<source>:private:*` (catches `private`,
120
121
  // `private:x`, and `vault:private` with a missing trailing segment).
122
+ assertClientScope(scope);
121
123
  if (scope !== null && scope.split(':').includes('private') && !isPrivateScope(scope)) {
122
124
  throw new Error(`vault scope '${scope}' is not recognized as private by recall (only '<source>:private:*' scopes are default-denied). Use a source-prefixed scope such as 'vault:private:${vaultName}'.`);
123
125
  }
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export { loadAllEntries, readEntry } from './store/entry-reads.js';
22
22
  export { deleteEntry } from './store/delete-and-batch.js';
23
23
  export { loadSearchEntries, loadRecallSearchEntries } from './store/search-rows.js';
24
24
  export { loadIndex, rebuildIndex, loadSessionDecayContext, SessionDecayContext } from './store/index-and-stats.js';
25
- export { saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, SNAPSHOT_AMBIENT_MAX_AGE_MS, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, } from './store/sessions.js';
25
+ export { saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, SNAPSHOT_AMBIENT_MAX_AGE_MS, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, type ContinuityKey, } from './store/sessions.js';
26
26
  export { listMemoryConflicts, replaceDetectedConflicts, resolveConflict } from './store/conflicts.js';
27
27
  export { saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, } from './store/handoffs.js';
28
28
  export { createCard, loadCard, listCards, loadCardDeps, loadCardRuns, loadCardComments, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, loadLatestHandoffForCard, } from './store-cards.js';
@@ -30,6 +30,7 @@ export { SessionHandoff, HandoffOutcome, HandoffEvidence, isHandoffOutcome } fro
30
30
  export { Card, CardStatus, CardRun, CardComment, CardTransitions, isCardStatus, CARD_TRANSITIONS, CARD_LEASE_MS } from './card.js';
31
31
  export { consolidate } from './consolidate/sleep.js';
32
32
  export { ConsolidationResult } from './consolidate/run.js';
33
+ export { sleep, type SleepOpts, type SleepResult } from './api/sleep.js';
33
34
  export { strengthBucket } from './dedupe.js';
34
35
  export { isEmbeddingAvailable, getEmbedding } from './local-embedding.js';
35
36
  export { cosineSimilarity, loadEmbeddingIndex, saveEmbeddingIndex, embedMemory, embedAll, } from './embeddings.js';
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ export { isHandoffOutcome } from './handoff.js';
32
32
  // W2a: work-queue cards
33
33
  export { isCardStatus, CARD_TRANSITIONS, CARD_LEASE_MS } from './card.js';
34
34
  export { consolidate } from './consolidate/sleep.js';
35
+ export { sleep } from './api/sleep.js';
35
36
  // Announced public in CHANGELOG 1.26.3 but never re-exported; the rest of dedupe.js stays internal.
36
37
  export { strengthBucket } from './dedupe.js';
37
38
  // Feature 1: Embedding search
package/dist/json.d.ts CHANGED
@@ -2,4 +2,6 @@ export type JsonValue = string | number | boolean | null | JsonValue[] | {
2
2
  [key: string]: JsonValue;
3
3
  };
4
4
  export declare function isJsonString(value: JsonValue | undefined): value is string;
5
+ /** Windows PowerShell 5.1 saves JSON with a UTF-8 byte order mark that JSON.parse rejects, so one leading mark is dropped. */
6
+ export declare function readJsonFile(file: string): JsonValue;
5
7
  //# sourceMappingURL=json.d.ts.map
package/dist/json.js CHANGED
@@ -1,4 +1,11 @@
1
+ // Leaf module: the one value space JSON.parse can produce, so boundary guards narrow a shared type.
2
+ import * as fs from 'fs';
1
3
  export function isJsonString(value) {
2
4
  return typeof value === 'string';
3
5
  }
6
+ /** Windows PowerShell 5.1 saves JSON with a UTF-8 byte order mark that JSON.parse rejects, so one leading mark is dropped. */
7
+ export function readJsonFile(file) {
8
+ const text = fs.readFileSync(file, 'utf8');
9
+ return JSON.parse(text.codePointAt(0) === 0xfeff ? text.slice(1) : text);
10
+ }
4
11
  //# sourceMappingURL=json.js.map
@@ -0,0 +1,10 @@
1
+ import { openHippoDb } from './db.js';
2
+ export interface LedgerRootOpts {
3
+ /** The store serves many people, so the global store of the user running it belongs to none of them. */
4
+ readonly sharedStore?: boolean;
5
+ }
6
+ /** `hippoRoot` when it holds a store, else the global store (the prompt hook fires where no local one exists), else null. */
7
+ export declare function ledgerRoot(hippoRoot: string, opts?: LedgerRootOpts): string | null;
8
+ /** Runs `fn` on ledgerRoot's store. Best-effort: undefined on any failure, because a ledger failure must not break context or recall. */
9
+ export declare function withLedgerDb<T>(hippoRoot: string, fn: (db: ReturnType<typeof openHippoDb>) => T, opts?: LedgerRootOpts): T | undefined;
10
+ //# sourceMappingURL=ledger-db.d.ts.map
@@ -0,0 +1,42 @@
1
+ // The store the token ledger writes to for a caller's root; context, recall and the session hooks share it.
2
+ import { closeHippoDb, isSqliteBusy, noteStoreBusy, openHippoDb } from './db.js';
3
+ import { getGlobalRoot } from './shared.js';
4
+ import { isInitialized } from './store/open.js';
5
+ /** `hippoRoot` when it holds a store, else the global store (the prompt hook fires where no local one exists), else null. */
6
+ export function ledgerRoot(hippoRoot, opts) {
7
+ if (isInitialized(hippoRoot))
8
+ return hippoRoot;
9
+ if (opts?.sharedStore)
10
+ return null;
11
+ const globalRoot = getGlobalRoot();
12
+ return isInitialized(globalRoot) ? globalRoot : null;
13
+ }
14
+ /** Runs `fn` on ledgerRoot's store. Best-effort: undefined on any failure, because a ledger failure must not break context or recall. */
15
+ export function withLedgerDb(hippoRoot, fn, opts) {
16
+ let root = null;
17
+ try {
18
+ root = ledgerRoot(hippoRoot, opts);
19
+ }
20
+ catch {
21
+ // An unreadable store root means no ledger write; the ledger must never break context or recall.
22
+ return undefined;
23
+ }
24
+ if (root === null)
25
+ return undefined;
26
+ let db;
27
+ try {
28
+ db = openHippoDb(root);
29
+ return fn(db);
30
+ }
31
+ catch (error) {
32
+ // Best effort, but a busy store is the one failure an operator can act on, so it warns once.
33
+ if (isSqliteBusy(error))
34
+ noteStoreBusy('token ledger row skipped');
35
+ return undefined;
36
+ }
37
+ finally {
38
+ if (db)
39
+ closeHippoDb(db);
40
+ }
41
+ }
42
+ //# sourceMappingURL=ledger-db.js.map
@@ -1,8 +1,8 @@
1
1
  import { type ToolCall } from './protocol.js';
2
2
  export declare function runPredictBaserateTool({ args, ctx, hippoRoot, tenantId }: ToolCall): string;
3
3
  export declare function runStatusTool({ hippoRoot, config, tenantId }: ToolCall): string;
4
- export declare function runConflictsTool({ hippoRoot, tenantId }: ToolCall): string;
4
+ export declare function runConflictsTool(call: ToolCall): string;
5
5
  export declare function runResolveTool({ args, ctx, hippoRoot, tenantId }: ToolCall): string;
6
- export declare function runShareTool({ args, hippoRoot, tenantId }: ToolCall): string;
6
+ export declare function runShareTool({ args, ctx, hippoRoot, tenantId }: ToolCall): string;
7
7
  export declare function runPeersTool({ tenantId }: ToolCall): string;
8
8
  //# sourceMappingURL=admin-tools.d.ts.map
@@ -2,10 +2,28 @@
2
2
  import { calculateStrength } from '../memory.js';
3
3
  import { evalNow } from '../ablation.js';
4
4
  import { loadStrengthRows } from '../store/candidates.js';
5
- import { listMemoryConflicts, resolveConflict } from '../store/conflicts.js';
5
+ import { listMemoryConflicts, listTouchableConflicts, resolveConflict } from '../store/conflicts.js';
6
6
  import { shareMemory, listPeers } from '../shared.js';
7
7
  import { computePredictionBaserate } from '../predictions/store.js';
8
+ import { closeHippoDb, openHippoDb } from '../db.js';
9
+ import { NotFoundError } from '../api-errors.js';
10
+ import { classifyOriginProject } from '../project-identity.js';
11
+ import { canTouchScope, passesScopeFilterForRecall, personalScopeOf } from '../recall-scope.js';
12
+ import { selectEntriesByIds } from '../store/entry-reads.js';
13
+ import { selectMemoryReach } from '../store/tenant-lookup.js';
14
+ import { mcpActor } from './protocol.js';
8
15
  import { isJsonString } from '../json.js';
16
+ const NOT_RESOLVED = 'Could not resolve. Check the conflict ID and --keep value.';
17
+ /** The scope of memory `id`, null when it has none or does not exist. */
18
+ function memoryScope(hippoRoot, id) {
19
+ const db = openHippoDb(hippoRoot);
20
+ try {
21
+ return selectMemoryReach(db, id)?.scope ?? null;
22
+ }
23
+ finally {
24
+ closeHippoDb(db);
25
+ }
26
+ }
9
27
  export function runPredictBaserateTool({ args, ctx, hippoRoot, tenantId }) {
10
28
  // Text-only reply, matching the other MCP tools; the helper opens its own db
11
29
  // and emits the audit, so call sites cannot drift.
@@ -55,8 +73,27 @@ export function runStatusTool({ hippoRoot, config, tenantId }) {
55
73
  `Half-life default: ${config.defaultHalfLifeDays}d`,
56
74
  ].join('\n');
57
75
  }
58
- export function runConflictsTool({ hippoRoot, tenantId }) {
59
- const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId);
76
+ /** Pairs whose two rows the caller could recall: its repo or user-global, and no scope it was not asked for. */
77
+ function recallablePairs(call, conflicts, project) {
78
+ const own = personalScopeOf(mcpActor(call.ctx));
79
+ const db = openHippoDb(call.hippoRoot);
80
+ try {
81
+ const rows = selectEntriesByIds(db, conflicts.flatMap((c) => [c.memory_a_id, c.memory_b_id]), call.tenantId);
82
+ const shown = (id) => {
83
+ const row = rows.get(id);
84
+ return row !== undefined && classifyOriginProject(row.origin_project, project) !== 'cross-project'
85
+ && passesScopeFilterForRecall(row.scope ?? null, undefined, own);
86
+ };
87
+ return conflicts.filter((c) => shown(c.memory_a_id) && shown(c.memory_b_id));
88
+ }
89
+ finally {
90
+ closeHippoDb(db);
91
+ }
92
+ }
93
+ export function runConflictsTool(call) {
94
+ const { ctx, hippoRoot, tenantId } = call;
95
+ const touchable = listTouchableConflicts(hippoRoot, 'open', tenantId, mcpActor(ctx));
96
+ const conflicts = ctx?.project ? recallablePairs(call, touchable, ctx.project) : touchable;
60
97
  if (conflicts.length === 0)
61
98
  return 'No open conflicts.';
62
99
  return conflicts.map((c) => `conflict_${c.id}: ${c.memory_a_id} <-> ${c.memory_b_id} (score=${c.score.toFixed(2)}) — ${c.reason}`).join('\n');
@@ -70,6 +107,8 @@ export function runResolveTool({ args, ctx, hippoRoot, tenantId }) {
70
107
  const reason = isJsonString(args.reason) ? args.reason : undefined;
71
108
  if (isNaN(conflictId) || !keepId)
72
109
  return 'Required: conflict_id and keep.';
110
+ if (!listTouchableConflicts(hippoRoot, 'open', tenantId, mcpActor(ctx)).some((c) => c.id === conflictId))
111
+ return NOT_RESOLVED;
73
112
  const result = resolveConflict(hippoRoot, conflictId, keepId, forget, tenantId, {
74
113
  rejectLoserValue: rejectLoser,
75
114
  reason,
@@ -78,15 +117,18 @@ export function runResolveTool({ args, ctx, hippoRoot, tenantId }) {
78
117
  rejectedBy: ctx?.actor ?? 'mcp',
79
118
  });
80
119
  if (!result)
81
- return 'Could not resolve. Check the conflict ID and --keep value.';
120
+ return NOT_RESOLVED;
82
121
  const action = rejectLoser ? 'rejected (tombstoned) and removed' : forget ? 'deleted' : 'weakened';
83
122
  return `Resolved conflict ${conflictId}: kept ${keepId}, ${action} ${result.loserId}`;
84
123
  }
85
- export function runShareTool({ args, hippoRoot, tenantId }) {
124
+ export function runShareTool({ args, ctx, hippoRoot, tenantId }) {
86
125
  const shareId = String(args.id || '');
87
126
  if (!shareId)
88
127
  return 'Required: id (memory ID to share).';
89
128
  const force = Boolean(args.force);
129
+ // Checked before shareMemory, whose personal-row refusal would tell another person the id exists.
130
+ if (!canTouchScope(mcpActor(ctx), memoryScope(hippoRoot, shareId)))
131
+ throw new NotFoundError(`Memory not found: ${shareId}`);
90
132
  // Pass tenantId so shareMemory's readEntry filters by tenant. Without
91
133
  // this, a Bearer for tenant A could call hippo_share with tenant B's
92
134
  // id and copy the row to the global store. The 'Memory not found'
@@ -25,15 +25,19 @@ export function runRememberTool({ args, ctx, hippoRoot, config, tenantId }) {
25
25
  hippoRoot,
26
26
  tenantId,
27
27
  actor: mcpActor(ctx),
28
+ store: ctx?.store,
28
29
  };
29
30
  const result = apiRemember(apiCtx, {
30
31
  content: text,
31
32
  tags,
33
+ personal: args.personal === true,
34
+ project: ctx?.project,
32
35
  });
33
36
  const entry = readEntry(hippoRoot, result.id, tenantId);
34
37
  // Auto-sleep: one run per store at a time, triggered by what arrived since the last one.
35
38
  // Consolidation is host-wide, so only the host tenant's writes may start it.
36
- if (config.autoSleep.enabled &&
39
+ if (ctx?.autoSleep !== false &&
40
+ config.autoSleep.enabled &&
37
41
  tenantId === resolveTenantId({}) &&
38
42
  !autoSleepInFlight.has(hippoRoot) &&
39
43
  countCreatedSinceLastSleep(hippoRoot, tenantId) >= config.autoSleep.threshold) {
@@ -64,13 +68,14 @@ export function runOutcomeTool({ args, ctx, hippoRoot, tenantId }) {
64
68
  hippoRoot,
65
69
  tenantId,
66
70
  actor: mcpActor(ctx),
71
+ store: ctx?.store,
67
72
  };
68
73
  const { applied } = apiOutcome(apiCtx, ids, good);
69
74
  return `Applied ${good ? 'positive' : 'negative'} outcome to ${applied} memories`;
70
75
  }
71
76
  export function runLearnTool({ args, ctx, hippoRoot, tenantId }) {
72
77
  const days = Number(args.days) || 7;
73
- const result = apiLearn({ hippoRoot, tenantId, actor: mcpActor(ctx) }, { repoPath: process.cwd(), days, profile: MCP_LEARN });
78
+ const result = apiLearn({ hippoRoot, tenantId, actor: mcpActor(ctx), store: ctx?.store }, { repoPath: process.cwd(), days, profile: MCP_LEARN });
74
79
  if (result.status === 'not-a-repo')
75
80
  return 'No git history found.';
76
81
  if (result.status === 'no-commits')
@@ -2,6 +2,8 @@ import { loadConfig } from '../config.js';
2
2
  import type { Actor as ApiActor } from '../api.js';
3
3
  import { type ResolveProjectIdentityOpts } from '../project-identity.js';
4
4
  import type { JsonValue } from '../json.js';
5
+ import type { CallerProject } from '../prompt-hook.js';
6
+ import type { HippoStore } from '../store-port.js';
5
7
  /** Same bounded walk as the CLI (ends at home, so HIPPO_HOME wins over ~/.hippo); cwd/opts are the test seam. */
6
8
  export declare function findHippoRoot(cwd?: string, opts?: ResolveProjectIdentityOpts): string | null;
7
9
  interface McpRequest {
@@ -50,6 +52,10 @@ export interface McpContext {
50
52
  viaAuthResolver?: true;
51
53
  /** Set by the HTTP transport for the host's operator; a context without a role is in-process and implies it. */
52
54
  hostAdmin?: true;
55
+ owner?: string;
56
+ project?: CallerProject;
57
+ store?: HippoStore;
58
+ autoSleep?: false;
53
59
  /**
54
60
  * Per-client key for state isolation under HTTP-MCP. For stdio: 'stdio-${pid}'
55
61
  * (one process = one client). For HTTP-SSE / HTTP MCP: hash(bearer + remoteAddr)
@@ -5,7 +5,7 @@ import { INTERNAL_ERROR_MESSAGE, mapApiError } from '../http-util.js';
5
5
  import { log } from '../log.js';
6
6
  import { getGlobalRoot } from '../shared.js';
7
7
  import { findHippoStoreDir } from '../project-identity.js';
8
- import { isSqliteBusy, STORE_BUSY_MESSAGE } from '../db.js';
8
+ import { isStoreBusy, STORE_BUSY_MESSAGE } from '../db.js';
9
9
  // ── Find hippo root ──
10
10
  /** Same bounded walk as the CLI (ends at home, so HIPPO_HOME wins over ~/.hippo); cwd/opts are the test seam. */
11
11
  export function findHippoRoot(cwd = process.cwd(), opts) {
@@ -18,7 +18,7 @@ export function findHippoRoot(cwd = process.cwd(), opts) {
18
18
  }
19
19
  /** JSON-RPC reply for a request that threw: typed API errors keep their text; anything else is logged and answered generically. */
20
20
  export function mcpErrorResponse(id, err, requestId = randomUUID()) {
21
- if (isSqliteBusy(err))
21
+ if (isStoreBusy(err))
22
22
  return { jsonrpc: '2.0', id, error: { code: -32603, message: STORE_BUSY_MESSAGE } };
23
23
  const { status, message } = mapApiError(err);
24
24
  if (status !== 500)
@@ -41,6 +41,8 @@ export function mcpActor(ctx) {
41
41
  actor.viaAuthResolver = true;
42
42
  if (ctx?.role === undefined || ctx.hostAdmin)
43
43
  actor.hostAdmin = true;
44
+ if (ctx?.owner !== undefined)
45
+ actor.owner = ctx.owner;
44
46
  return actor;
45
47
  }
46
48
  // ── JSON-ish domain type for untrusted MCP tool-call arguments ──
@@ -5,6 +5,7 @@ import { dropHeldCopies, duplicateKey, storedTextKeys } from '../same-text.js';
5
5
  import { retrieve as apiRetrieve, drillDown as apiDrillDown, assemble as apiAssemble, getContext as apiGetContext, buildSuppressionSummary } from '../api.js';
6
6
  import { autoDetectContext } from '../context-auto.js';
7
7
  import { resolveProjectIdentity } from '../project-identity.js';
8
+ import { isSharedStore } from '../config.js';
8
9
  import { appendAuditEvent, auditQueryFields } from '../audit.js';
9
10
  import { detectAnchoring, hashQueryText, biasHintEnabled, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from '../recall-history.js';
10
11
  import { detectAvailabilityBias } from '../availability.js';
@@ -201,6 +202,7 @@ export async function runRecallTool(call) {
201
202
  hippoRoot,
202
203
  tenantId,
203
204
  actor: mcpActor(ctx),
205
+ store: ctx?.store,
204
206
  };
205
207
  const anchorRing = biasHintEnabled('anchoring') && sessionId
206
208
  ? getOrCreateRing(sessionRecallHistoryMcp, buildSessionKey(tenantId, sessionId))
@@ -217,6 +219,7 @@ export async function runRecallTool(call) {
217
219
  // The hint is computed below over the list MCP shows; the window band's copy would emit its audit row twice.
218
220
  suppressAvailabilityHint: true,
219
221
  keepHeldCopies: true,
222
+ project: ctx?.project,
220
223
  ...recallExtra,
221
224
  showRanked: recallPresenter(budget, includeContinuity, anchorRing, queryHash, out),
222
225
  });
@@ -237,6 +240,7 @@ export function runAssembleTool({ args, ctx, hippoRoot, tenantId }) {
237
240
  hippoRoot,
238
241
  tenantId,
239
242
  actor: mcpActor(ctx),
243
+ store: ctx?.store,
240
244
  };
241
245
  const explicitScope = isJsonString(args.scope) && args.scope.length > 0
242
246
  ? args.scope
@@ -250,6 +254,7 @@ export function runAssembleTool({ args, ctx, hippoRoot, tenantId }) {
250
254
  assembleExtra.scope = explicitScope;
251
255
  const r = apiAssemble(apiCtx, sessionId, {
252
256
  summarizeOlder,
257
+ project: ctx?.project,
253
258
  ...assembleExtra,
254
259
  cost: assembleCost(sessionId),
255
260
  });
@@ -267,6 +272,7 @@ export function runDrillTool({ args, ctx, hippoRoot, tenantId }) {
267
272
  hippoRoot,
268
273
  tenantId,
269
274
  actor: mcpActor(ctx),
275
+ store: ctx?.store,
270
276
  };
271
277
  const drillExtra = {};
272
278
  if (Number.isFinite(limit) && limit > 0)
@@ -275,7 +281,7 @@ export function runDrillTool({ args, ctx, hippoRoot, tenantId }) {
275
281
  drillExtra.budget = budget;
276
282
  if (depth !== undefined)
277
283
  drillExtra.depth = depth;
278
- const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra, cost: drillCost });
284
+ const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra, project: ctx?.project, cost: drillCost });
279
285
  if ('failure' in r) {
280
286
  // Only not_drillable is caller-actionable. not_found merges cross-tenant, scope-blocked and
281
287
  // missing, because telling scope_blocked apart would leak private-row existence.
@@ -286,7 +292,15 @@ export function runDrillTool({ args, ctx, hippoRoot, tenantId }) {
286
292
  }
287
293
  return drillText(r);
288
294
  }
295
+ // The served store names the project (an HTTP daemon runs from anywhere); the global root names none, so stdio falls back to its launch cwd.
296
+ function localProject(hippoRoot) {
297
+ const storeProject = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
298
+ return storeProject.name !== '' ? storeProject : resolveProjectIdentity(process.cwd());
299
+ }
289
300
  export async function runContextTool({ args, ctx, hippoRoot, config, tenantId }) {
301
+ // Stdio names no project, and the store's folder or the launch cwd is no caller's; HTTP sends X-Hippo-Project.
302
+ if (isSharedStore(hippoRoot) && ctx?.project === undefined)
303
+ return 'hippo_context needs a project on a shared store; use hippo_recall';
290
304
  const budget = args.budget === undefined
291
305
  ? config.defaultContextBudget
292
306
  : Number(args.budget);
@@ -299,13 +313,12 @@ export async function runContextTool({ args, ctx, hippoRoot, config, tenantId })
299
313
  const exactScope = isJsonString(args.scope) && args.scope.length > 0
300
314
  ? args.scope
301
315
  : undefined;
302
- // The served store names the project (an HTTP daemon runs from anywhere); the global root names none, so stdio falls back to its launch cwd.
303
- const storeProject = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
304
- const result = await apiGetContext({ hippoRoot, tenantId, actor: mcpActor(ctx) }, {
305
- q: autoDetectContext(),
316
+ const result = await apiGetContext({ hippoRoot, tenantId, actor: mcpActor(ctx), store: ctx?.store }, {
317
+ // The server's git state is no caller's, so a shared-store caller gets its project's rows without a query.
318
+ q: ctx?.project ? undefined : autoDetectContext(),
306
319
  budget,
307
320
  exactScope,
308
- currentProject: storeProject.name !== '' ? storeProject : resolveProjectIdentity(process.cwd()),
321
+ currentProject: ctx?.project ?? localProject(hippoRoot),
309
322
  cost: contextCost,
310
323
  });
311
324
  lastRecalledIds.set(resolveClientKey(ctx), result.entries.map((r) => r.entry.id));
@@ -12,6 +12,7 @@ import { TOOLS, TOOLS_BY_NAME, ARGS_CHECKED_BY_API } from './tools.js';
12
12
  import { runRecallTool, runAssembleTool, runDrillTool, runContextTool } from './recall-tools.js';
13
13
  import { runRememberTool, runOutcomeTool, runLearnTool } from './memory-tools.js';
14
14
  import { runPredictBaserateTool, runStatusTool, runConflictsTool, runResolveTool, runShareTool, runPeersTool } from './admin-tools.js';
15
+ import { sharedStoreRefusal } from './shared-gate.js';
15
16
  import { isJsonString } from '../json.js';
16
17
  /**
17
18
  * Zero-install first run (`npx -y hippo-memory mcp` with no store anywhere):
@@ -126,6 +127,9 @@ export async function handleMcpRequest(req, ctx) {
126
127
  if (!tool) {
127
128
  return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${toolName.slice(0, 128)}` } };
128
129
  }
130
+ const refusal = sharedStoreRefusal(toolName, ctx);
131
+ if (refusal !== undefined)
132
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: refusal }], isError: true } };
129
133
  const argumentsValue = params?.arguments;
130
134
  if (argumentsValue !== undefined && argumentsValue !== null && !isJsonObjectRecord(argumentsValue)) {
131
135
  return { jsonrpc: '2.0', id, error: { code: -32602, message: `${toolName}: arguments must be an object` } };
@@ -2,10 +2,14 @@ import type { RingBuffer } from '../recall-history.js';
2
2
  export declare const sessionRecallHistoryMcp: Map<string, RingBuffer>;
3
3
  /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
4
4
  export declare function __resetSessionRecallHistoryMcp(): void;
5
+ export declare const MAX_RECALL_CLIENTS = 4096;
5
6
  export declare const lastRecalledIds: Map<string, string[]>;
6
7
  export declare const autoSleepInFlight: Set<string>;
7
8
  export declare function resolveClientKey(ctx: {
8
9
  clientKey?: string;
9
10
  tenantId: string;
11
+ project?: {
12
+ name: string;
13
+ };
10
14
  } | undefined): string;
11
15
  //# sourceMappingURL=session-state.d.ts.map
@@ -15,13 +15,22 @@ export function __resetSessionRecallHistoryMcp() {
15
15
  // back to `'stdio-${pid}'` (one process = one client) or
16
16
  // `${tenantId}:default` if a McpContext is constructed in tests without a
17
17
  // pid-bound transport.
18
- export const lastRecalledIds = new Map();
18
+ export const MAX_RECALL_CLIENTS = 4096;
19
+ // The caller names the project half of each key, so a set past the cap drops the client that recalled longest ago.
20
+ class RecentRecalls extends Map {
21
+ set(key, ids) {
22
+ this.delete(key);
23
+ super.set(key, ids);
24
+ const oldest = this.keys().next().value;
25
+ if (this.size > MAX_RECALL_CLIENTS && oldest !== undefined)
26
+ this.delete(oldest);
27
+ return this;
28
+ }
29
+ }
30
+ export const lastRecalledIds = new RecentRecalls();
19
31
  export const autoSleepInFlight = new Set();
20
32
  export function resolveClientKey(ctx) {
21
- if (ctx?.clientKey)
22
- return ctx.clientKey;
23
- if (ctx?.tenantId)
24
- return `stdio-${process.pid}:${ctx.tenantId}`;
25
- return `stdio-${process.pid}:default`;
33
+ const base = ctx?.clientKey ? ctx.clientKey : `stdio-${process.pid}:${ctx?.tenantId || 'default'}`;
34
+ return ctx?.project ? `${base}:${ctx.project.name}` : base;
26
35
  }
27
36
  //# sourceMappingURL=session-state.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpContext } from './protocol.js';
2
+ /** The refusal text for this call, or undefined when it may run; stdio passes no context and is never gated. */
3
+ export declare function sharedStoreRefusal(name: string, ctx: McpContext | undefined): string | undefined;
4
+ //# sourceMappingURL=shared-gate.d.ts.map
@@ -0,0 +1,22 @@
1
+ // Which MCP tools run on a shared store, and which need the caller's project first.
2
+ import { isSharedStore } from '../config.js';
3
+ // A shared store holds many repos' rows, so a memory tool needs the caller's project and four tools never run.
4
+ const OFF_ON_SHARED = new Map([
5
+ ['hippo_learn', "reads the server's git history"],
6
+ ['hippo_share', "copies into the server's global store"],
7
+ ['hippo_resolve', 'tombstones across every project; the admin resolves with the CLI'],
8
+ ['hippo_peers', "lists the server's own global store, not the repos that share this one"],
9
+ ]);
10
+ const OPEN_WITHOUT_PROJECT = new Set(['hippo_predict_baserate']);
11
+ /** The refusal text for this call, or undefined when it may run; stdio passes no context and is never gated. */
12
+ export function sharedStoreRefusal(name, ctx) {
13
+ if (ctx === undefined || !isSharedStore(ctx.hippoRoot))
14
+ return undefined;
15
+ const off = OFF_ON_SHARED.get(name);
16
+ if (off !== undefined)
17
+ return `${name} is off on a shared store: it ${off}`;
18
+ if (OPEN_WITHOUT_PROJECT.has(name) || ctx.project !== undefined)
19
+ return undefined;
20
+ return `${name} needs the caller's project on a shared store; the client sends it in the X-Hippo-Project header`;
21
+ }
22
+ //# sourceMappingURL=shared-gate.js.map
package/dist/mcp/tools.js CHANGED
@@ -131,6 +131,7 @@ export const TOOLS = [
131
131
  error: { type: 'boolean', description: 'Mark as error memory (doubles half-life)' },
132
132
  pin: { type: 'boolean', description: 'Pin memory (never decays)' },
133
133
  tag: { type: 'string', description: 'Optional tag for categorization' },
134
+ personal: { type: 'boolean', description: 'Store it as your own private memory: only you can recall it, in every project. Needs a key you minted or a sign-in.' },
134
135
  },
135
136
  required: ['text'],
136
137
  },