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
@@ -23,7 +23,7 @@ export function retireHeldTexts(run) {
23
23
  const rejectedIn = (tenantId) => (text) => run.tombstones.find(tenantId, rejectionDigest(text)) !== null;
24
24
  for (let i = survivors.length - 1; i >= 0; i--) {
25
25
  const row = survivors[i];
26
- const successor = run.retirable(row) ? successorAfterRetirement(row, byId, rejectedIn(row.tenantId)) : undefined;
26
+ const successor = run.retirable(row) ? successorAfterRetirement(row, byId, rejectedIn(row.tenant_id)) : undefined;
27
27
  if (successor === undefined)
28
28
  continue;
29
29
  run.result.details.push(` ✂️ ${row.id} held a retired text${successor ? `, ${successor.id} holds the rest` : ''}`);
@@ -51,7 +51,7 @@ function partitionMergeCandidates(survivors) {
51
51
  // Map keeps insertion order, so a single-tenant store gets one partition in the original order.
52
52
  const mergeCandidatesByTenant = new Map();
53
53
  for (const entry of mergeCandidates) {
54
- const key = derivationPartitionKey(entry.tenantId, entry.scope, entry.origin_project);
54
+ const key = derivationPartitionKey(entry.tenant_id, entry.scope, entry.origin_project);
55
55
  const bucket = mergeCandidatesByTenant.get(key);
56
56
  if (bucket)
57
57
  bucket.push(entry);
@@ -70,7 +70,7 @@ export function mergePass(run) {
70
70
  let mergesSkippedRejected = 0;
71
71
  for (const [, tenantCandidates] of mergeCandidatesByTenant) {
72
72
  const partition = {
73
- tenantId: tenantCandidates[0].tenantId,
73
+ tenantId: tenantCandidates[0].tenant_id,
74
74
  scope: derivationScope(tenantCandidates[0].scope),
75
75
  origin: tenantCandidates[0].origin_project,
76
76
  };
@@ -156,8 +156,8 @@ function mergeRejected(run, semantic, cluster, related, used) {
156
156
  const { tombstones } = run;
157
157
  const newDigest = rejectionDigest(semantic.content);
158
158
  const oldDigest = rejectionDigest(legacyMergeContents(related)); // tombstones from older releases hold this format's digest
159
- const newHit = tombstones.find(semantic.tenantId, newDigest);
160
- const tombstone = newHit ?? tombstones.find(semantic.tenantId, oldDigest);
159
+ const newHit = tombstones.find(semantic.tenant_id, newDigest);
160
+ const tombstone = newHit ?? tombstones.find(semantic.tenant_id, oldDigest);
161
161
  const mergeDigest = newHit ? newDigest : oldDigest;
162
162
  if (!tombstone)
163
163
  return false;
@@ -167,7 +167,7 @@ function mergeRejected(run, semantic, cluster, related, used) {
167
167
  used.add(e.id);
168
168
  try {
169
169
  tombstones.audit({
170
- tenantId: semantic.tenantId,
170
+ tenantId: semantic.tenant_id,
171
171
  actor: 'sleep',
172
172
  op: 'reject_refusal',
173
173
  metadata: {
@@ -156,7 +156,7 @@ function auditRescues(run, { rescuedEntries, rankById }) {
156
156
  const rescueEvent = (entry) => {
157
157
  const rank = rankById.get(entry.id);
158
158
  return {
159
- tenantId: entry.tenantId,
159
+ tenantId: entry.tenant_id,
160
160
  actor: 'sleep',
161
161
  op: 'mv_rescue',
162
162
  targetId: entry.id,
@@ -103,12 +103,12 @@ function sessionEnding(events) {
103
103
  // sleep; check the tombstone under the entry's own stamped tenantId, never a guessed one.
104
104
  function traceRejected(run, trace, sessionId) {
105
105
  const traceDigest = rejectionDigest(trace.content);
106
- const tombstone = run.tombstones.find(trace.tenantId, traceDigest);
106
+ const tombstone = run.tombstones.find(trace.tenant_id, traceDigest);
107
107
  if (!tombstone)
108
108
  return false;
109
109
  try {
110
110
  run.tombstones.audit({
111
- tenantId: trace.tenantId,
111
+ tenantId: trace.tenant_id,
112
112
  actor: 'sleep',
113
113
  op: 'reject_refusal',
114
114
  metadata: {
@@ -252,17 +252,23 @@ function fileStamp(file) {
252
252
  return null;
253
253
  }
254
254
  }
255
- function parseConfigFile(configPath) {
255
+ /** A config.json that exists must load: running on defaults would silently drop the user's settings, such as `sharedStore`. */
256
+ function readConfigFile(configPath) {
257
+ let raw;
256
258
  try {
257
- const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
258
- return configOverDefaults(raw);
259
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
259
260
  }
260
261
  catch (err) {
261
- if (fs.existsSync(configPath)) {
262
- log.warn(`failed to parse ${configPath}: ${errorMessage(err)}`);
263
- }
264
- return { ...DEFAULT_CONFIG };
262
+ throw new Error(`${configPath} could not be loaded: ${errorMessage(err)}. Fix the file or delete it to use the defaults.`, { cause: err });
265
263
  }
264
+ if (!isJsonObject(raw)) {
265
+ throw new Error(`${configPath} must hold a JSON object. Fix the file or delete it to use the defaults.`);
266
+ }
267
+ // SAFETY: an object is all the shape check proves; configOverDefaults validates each key it reads.
268
+ return raw;
269
+ }
270
+ function parseConfigFile(configPath) {
271
+ return configOverDefaults(readConfigFile(configPath));
266
272
  }
267
273
  function configOverDefaults(raw) {
268
274
  const basis = raw.decayBasis;
@@ -334,15 +340,8 @@ export function isSharedStore(hippoRoot) {
334
340
  const stamp = fileStamp(configPath);
335
341
  if (stamp === null || notSharedStamps.get(configPath) === stamp)
336
342
  return false;
337
- let raw;
338
- try {
339
- raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
340
- }
341
- catch (err) {
342
- log.warn(`failed to read ${configPath}: ${errorMessage(err)} - sharedStore read as false.`);
343
- return false;
344
- }
345
- if (raw?.sharedStore !== true) {
343
+ const raw = readConfigFile(configPath);
344
+ if (raw.sharedStore !== true) {
346
345
  if (notSharedStamps.size >= PARSED_CONFIGS_MAX)
347
346
  notSharedStamps.clear();
348
347
  notSharedStamps.set(configPath, stamp);
@@ -42,7 +42,7 @@ export function serializeEntry(entry) {
42
42
  const frontmatter = entryFrontmatter(entry);
43
43
  // Emit tenant_id only when not 'default' to keep diffs clean for the dominant
44
44
  // single-tenant case (mirrors the plan's task 7 guidance).
45
- const tenantId = entry.tenantId ?? DEFAULT_TENANT_ID;
45
+ const tenantId = entry.tenant_id ?? DEFAULT_TENANT_ID;
46
46
  if (tenantId !== DEFAULT_TENANT_ID) {
47
47
  frontmatter['tenant_id'] = tenantId;
48
48
  }
@@ -98,7 +98,7 @@ export function deserializeEntry(raw) {
98
98
  scope: data['scope'] === null || data['scope'] === undefined ? null : String(data['scope']),
99
99
  owner: data['owner'] === null || data['owner'] === undefined ? null : String(data['owner']),
100
100
  artifact_ref: data['artifact_ref'] === null || data['artifact_ref'] === undefined ? null : String(data['artifact_ref']),
101
- tenantId: data['tenant_id'] === null || data['tenant_id'] === undefined ? DEFAULT_TENANT_ID : String(data['tenant_id']),
101
+ tenant_id: data['tenant_id'] === null || data['tenant_id'] === undefined ? DEFAULT_TENANT_ID : String(data['tenant_id']),
102
102
  origin_project: !('origin_project' in data) ? undefined : data['origin_project'] === null ? null : String(data['origin_project']),
103
103
  };
104
104
  }
@@ -44,8 +44,8 @@ export interface MemoryEntry {
44
44
  descendant_count?: number;
45
45
  earliest_at?: string | null;
46
46
  latest_at?: string | null;
47
- /** 1 when a child was invalidated, superseded, forgotten or archived since the last rebuild; sleep clears it. Always 0 below level 2. */
48
- summary_dirty?: 0 | 1;
47
+ /** True when a child was invalidated, superseded, forgotten or archived since the last rebuild; sleep clears it. Always false below level 2. */
48
+ summary_dirty?: boolean;
49
49
  /** ISO 8601 time of the last successful rebuild, or null if never rebuilt. */
50
50
  last_rebuilt_at?: string | null;
51
51
  /** Count of successful rebuilds: 0 at the first buildDag write, bumped by each rebuild. */
@@ -56,7 +56,7 @@ export interface MemoryEntry {
56
56
  scope: string | null;
57
57
  owner: string | null;
58
58
  artifact_ref: string | null;
59
- tenantId: string;
59
+ tenant_id: string;
60
60
  /** Owning project, lowercased; '' = user-global (injectable everywhere), null = none known (legacy or unnamed write), denied by ambient context.
61
61
  * Stamped at write time by stampOriginProject (src/store/entry-row.ts); undefined only before the first write. */
62
62
  origin_project?: string | null;
@@ -114,7 +114,7 @@ export interface ConfidenceFacets {
114
114
  tier: ConfidenceLevel;
115
115
  agedOut: boolean;
116
116
  }
117
- export declare function confidenceFacets(entry: MemoryEntry, now?: Date): ConfidenceFacets;
117
+ export declare function confidenceFacets(entry: ConfidenceInputs, now?: Date): ConfidenceFacets;
118
118
  export declare function facetsOf(entry: ConfidenceInputs, now: Date): ConfidenceFacets;
119
119
  export declare function confidenceLabel(entry: MemoryEntry, now?: Date): {
120
120
  text: string;
@@ -261,7 +261,7 @@ export function createMemory(content, options = {}) {
261
261
  scope: options.scope ?? null,
262
262
  owner: options.owner ?? null,
263
263
  artifact_ref: options.artifact_ref ?? null,
264
- tenantId: options.tenantId ?? DEFAULT_TENANT_ID,
264
+ tenant_id: options.tenantId ?? DEFAULT_TENANT_ID,
265
265
  };
266
266
  entry.strength = calculateStrength(entry);
267
267
  return entry;
@@ -310,8 +310,14 @@ export function schemaFitSourceOf(entries) {
310
310
  }
311
311
  return { rows: entries.length, tagCounts, contents: entries.map((e) => e.content) };
312
312
  }
313
+ // Tokens of this length or shorter are filler for schema fit.
314
+ const FILLER_TOKEN_MAX_LENGTH = 3;
315
+ // Matching half the tags at average weight scores about 0.5, so the raw overlap ratio is doubled.
316
+ const TAG_OVERLAP_SCALE = 2;
317
+ // Floor on the matches that saturate the content score, so a tiny store cannot saturate it on one hit.
318
+ const CONTENT_ENOUGH_MIN_MATCHES = 5;
313
319
  function significantTokens(text) {
314
- return new Set(text.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter((t) => t.length > 3));
320
+ return new Set(text.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter((t) => t.length > FILLER_TOKEN_MAX_LENGTH));
315
321
  }
316
322
  /** computeSchemaFit against `source`, for a caller that holds counts and texts but no rows. */
317
323
  export function schemaFitFrom(content, tags, source) {
@@ -331,13 +337,12 @@ export function schemaFitFrom(content, tags, source) {
331
337
  weightedOverlap += Math.log(N / freq) + 1;
332
338
  totalWeight += maxIdf;
333
339
  }
334
- // Scaled so that matching half the tags at average weight gives about 0.5.
335
- const tagScore = totalWeight > 0 ? Math.min(1, (weightedOverlap / totalWeight) * 2) : 0;
340
+ const tagScore = totalWeight > 0 ? Math.min(1, (weightedOverlap / totalWeight) * TAG_OVERLAP_SCALE) : 0;
336
341
  const newTokens = significantTokens(content);
337
342
  if (newTokens.size === 0)
338
343
  return Math.min(1, Math.max(0, tagScore));
339
344
  // The content score is capped at 1, which this many matching memories reach.
340
- const enough = Math.max(5, N * CONTENT_ENOUGH_FRACTION);
345
+ const enough = Math.max(CONTENT_ENOUGH_MIN_MATCHES, N * CONTENT_ENOUGH_FRACTION);
341
346
  let contentMatches = 0;
342
347
  for (const text of source.contents) {
343
348
  const entryTokens = significantTokens(text);
@@ -1,7 +1,7 @@
1
1
  /** Memory-as-Physics engine: pure math (forces, Velocity Verlet integration, physics-based scoring, cluster amplification), no I/O.
2
2
  * Memories are particles on the unit hypersphere in embedding space (384-dim); query gravity, attraction, conflict repulsion and drag act on them,
3
3
  * and nearby high-scoring memories amplify each other. */
4
- import { type EmotionalValence } from './memory.js';
4
+ import { type EmotionalValence, type MemoryEntry } from './memory.js';
5
5
  import type { PhysicsConfig } from './physics-config.js';
6
6
  export interface PhysicsParticle {
7
7
  memoryId: string;
@@ -71,4 +71,6 @@ export declare function physicsScore(particles: PhysicsParticle[], queryEmbeddin
71
71
  tieKeyOf?: (memoryId: string) => string): ScoredPhysicsResult[];
72
72
  /** Nudges a particle toward (good outcome) or away from (bad outcome) the query embedding; new memories respond more (temperature). Mutates in place. */
73
73
  export declare function applyOutcomeFeedback(particle: PhysicsParticle, queryEmbedding: number[], good: boolean, feedbackAlpha: number): void;
74
+ /** Initialize physics state for a memory from its embedding vector; returns the new particle without persisting (the caller must save). */
75
+ export declare function initializeParticle(entry: MemoryEntry, embedding: number[], now?: Date): PhysicsParticle;
74
76
  //# sourceMappingURL=physics.d.ts.map
@@ -2,8 +2,9 @@
2
2
  * Memories are particles on the unit hypersphere in embedding space (384-dim); query gravity, attraction, conflict repulsion and drag act on them,
3
3
  * and nearby high-scoring memories amplify each other. */
4
4
  import { cosineOf } from './cosine.js';
5
- import { isRecallBoostAblated } from './ablation.js';
6
- import { FALLBACK_HALF_LIFE_DAYS } from './memory.js';
5
+ import { evalNow, isRecallBoostAblated, strengthAblation } from './ablation.js';
6
+ import { calculateStrength, FALLBACK_HALF_LIFE_DAYS } from './memory.js';
7
+ import { DAY_MS } from '../util/time.js';
7
8
  import { comparePhysicsResultsBy } from './compare.js';
8
9
  export function vecDot(a, b) {
9
10
  let sum = 0;
@@ -301,4 +302,18 @@ export function applyOutcomeFeedback(particle, queryEmbedding, good, feedbackAlp
301
302
  const nudge = vecScale(direction, sign * feedbackAlpha * particle.temperature);
302
303
  particle.position = vecNormalize(vecAdd(particle.position, nudge));
303
304
  }
305
+ /** Initialize physics state for a memory from its embedding vector; returns the new particle without persisting (the caller must save). */
306
+ export function initializeParticle(entry, embedding, now = evalNow()) {
307
+ const strength = calculateStrength(entry, now, { ablation: strengthAblation() });
308
+ const ageDays = (now.getTime() - new Date(entry.created).getTime()) / DAY_MS;
309
+ return {
310
+ memoryId: entry.id,
311
+ position: [...embedding], // copy: position will diverge from original embedding
312
+ velocity: vecZero(embedding.length),
313
+ mass: computeMass(strength, entry.retrieval_count),
314
+ charge: computeCharge(entry.emotional_valence),
315
+ temperature: computeTemperature(ageDays, 1.0),
316
+ lastSimulation: now.toISOString(),
317
+ };
318
+ }
304
319
  //# sourceMappingURL=physics.js.map
@@ -8,6 +8,7 @@ import { MAX_ID_LEN } from '../util/limits.js';
8
8
  import { errorMessage, log } from '../util/log.js';
9
9
  import { originRemoteId, projectFileId } from './project-remote.js';
10
10
  import { realpathOrResolve } from '../util/real-path.js';
11
+ import { placeholders } from '../util/sql.js';
11
12
  const MAX_WALK_DEPTH = 64;
12
13
  const identityCache = new Map();
13
14
  let remoteSwitch = null;
@@ -196,7 +197,7 @@ export function assertCallerProject(project) {
196
197
  }
197
198
  /** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
198
199
  export function originInSql(names, column = 'origin_project') {
199
- return names.length === 0 ? '0' : `${column} IN (${names.map(() => '?').join(', ')})`;
200
+ return names.length === 0 ? '0' : `${column} IN (${placeholders(names.length)})`;
200
201
  }
201
202
  /** Classifies a memory's origin_project against the active project. An empty current id (home or markerless cwd) puts everything in scope;
202
203
  * NULL/undefined origin (legacy row, or a shared-store write naming none) counts as cross-project: deny by default for a security partition. */
@@ -63,7 +63,7 @@ export interface ScoreBreakdown {
63
63
  scopeBoost: number;
64
64
  /** Extra multiplier applied post-hybrid (e.g. 1.2x for local hits in a local+global merge); 1.0 otherwise. */
65
65
  sourceBump: number;
66
- /** Immediate outcome nudge, 1 + 0.15*tanh(pos - neg) clipped to [0.85, 1.15]; separate from the slow strength path. */
66
+ /** Immediate outcome nudge, 1 + 0.15*tanh((pos - neg) / 2) clipped to [0.85, 1.15]; separate from the slow strength path. */
67
67
  outcomeBoost: number;
68
68
  /** CHURN_STALE_RANK_MULTIPLIER if tagged 'churn-stale', else 1.0. */
69
69
  churnStaleMultiplier: number;
@@ -12,9 +12,9 @@ export interface ActionResult {
12
12
  /** Sets `pinned`; the audit row says `remember` with actor `dashboard`, the same op the old star toggle wrote. */
13
13
  export declare function pinMemory(hippoRoot: string, tenantId: string, rawId: string, body: ActionBody): ActionResult;
14
14
  /** Marks a memory wrong, the same signal as `hippo outcome --bad`. */
15
- export declare function markWrong(hippoRoot: string, tenantId: string, rawId: string): ActionResult;
15
+ export declare function markWrong(hippoRoot: string, tenantId: string, rawId: string): Promise<ActionResult>;
16
16
  /** Deletes a memory like `hippo forget`; a raw receipt is append-only and answers 409 with the archive hint. */
17
- export declare function forgetMemory(hippoRoot: string, tenantId: string, rawId: string): ActionResult;
17
+ export declare function forgetMemory(hippoRoot: string, tenantId: string, rawId: string): Promise<ActionResult>;
18
18
  /** Keeps one side of an open conflict and weakens the other; every refusal maps to its own status. */
19
19
  export declare function resolveOpenConflict(hippoRoot: string, tenantId: string, conflictId: number, body: ActionBody): ActionResult;
20
20
  //# sourceMappingURL=dashboard-actions.d.ts.map
@@ -29,25 +29,25 @@ export function pinMemory(hippoRoot, tenantId, rawId, body) {
29
29
  return { status: 200, body: null, changed: true, entry: next };
30
30
  }
31
31
  /** Marks a memory wrong, the same signal as `hippo outcome --bad`. */
32
- export function markWrong(hippoRoot, tenantId, rawId) {
32
+ export async function markWrong(hippoRoot, tenantId, rawId) {
33
33
  const id = parseMemoryId(rawId);
34
34
  if (liveEntry(hippoRoot, tenantId, id) === null)
35
35
  return notFound();
36
36
  const ctx = { hippoRoot, tenantId, actor: api.adminActor(ACTOR) };
37
37
  // outcome() answers `applied: 0` for a missing or other-tenant id instead of throwing.
38
- if (api.outcome(ctx, [id], false).applied === 0)
38
+ if ((await api.outcome(ctx, [id], false)).applied === 0)
39
39
  return notFound();
40
40
  const entry = readEntry(hippoRoot, id, tenantId);
41
41
  return entry === null ? notFound() : { status: 200, body: null, changed: true, entry };
42
42
  }
43
43
  /** Deletes a memory like `hippo forget`; a raw receipt is append-only and answers 409 with the archive hint. */
44
- export function forgetMemory(hippoRoot, tenantId, rawId) {
44
+ export async function forgetMemory(hippoRoot, tenantId, rawId) {
45
45
  const id = parseMemoryId(rawId);
46
46
  if (liveEntry(hippoRoot, tenantId, id) === null)
47
47
  return notFound();
48
48
  const ctx = { hippoRoot, tenantId, actor: api.adminActor(ACTOR) };
49
49
  try {
50
- const done = api.forget(ctx, id);
50
+ const done = await api.forget(ctx, id);
51
51
  return { status: 200, body: done, changed: true };
52
52
  }
53
53
  catch (err) {
@@ -79,7 +79,7 @@ export function resolveOpenConflict(hippoRoot, tenantId, conflictId, body) {
79
79
  if (liveEntry(hippoRoot, tenantId, conflict.memory_a_id) === null || liveEntry(hippoRoot, tenantId, conflict.memory_b_id) === null) {
80
80
  return notFound();
81
81
  }
82
- const resolved = resolveConflict(hippoRoot, conflictId, keep, false, tenantId, { rejectedBy: ACTOR });
82
+ const resolved = resolveConflict(hippoRoot, { conflictId, keepId: keep, tenantId }, { rejectedBy: ACTOR });
83
83
  // Both guards passed above, so null here is a race with another resolver.
84
84
  if (resolved === null)
85
85
  return already();
@@ -2,6 +2,7 @@
2
2
  import { calculateStrength } from '../core/memory.js';
3
3
  import { loadOpenConflictsOf } from '../store/conflicts.js';
4
4
  import { BANDS, DAY_MS, LAYERS, isLiveMemory, memoryFacts, projectIdentity, } from './dashboard-snapshot.js';
5
+ import { pct, round1 } from '../util/format.js';
5
6
  const SERIES_DAYS = 90;
6
7
  /** The server words each delta for this range; the client re-slices the series for 7d and 90d. */
7
8
  const DELTA_DAYS = 30;
@@ -9,9 +10,6 @@ const POINTS_MAX = 4_000;
9
10
  const GRID_COLS = 64;
10
11
  const GRID_ROWS = 32;
11
12
  const NAME_MATCHES = 3;
12
- function round1(value) {
13
- return Math.round(value * 10) / 10;
14
- }
15
13
  function round3(value) {
16
14
  return Math.round(value * 1000) / 1000;
17
15
  }
@@ -32,9 +30,6 @@ function cumulativeSeries(times, nowMs) {
32
30
  function addedWithin(series) {
33
31
  return series[SERIES_DAYS] - series[SERIES_DAYS - DELTA_DAYS];
34
32
  }
35
- function pct(share) {
36
- return `${(share * 100).toFixed(1)}%`;
37
- }
38
33
  function plural(n, one, many) {
39
34
  return `${n} ${n === 1 ? one : many}`;
40
35
  }
@@ -342,7 +337,7 @@ export function buildMemoryDetail(hippoRoot, tenantId, entry, options) {
342
337
  projectKey: project.key,
343
338
  projectName: project.name,
344
339
  scope: entry.scope,
345
- tenant: entry.tenantId,
340
+ tenant: entry.tenant_id,
346
341
  kind: entry.kind,
347
342
  embedded,
348
343
  conflicts: openConflictsOf(hippoRoot, tenantId, entry, nowMs),
@@ -5,6 +5,7 @@ import { isQuarantineScope } from '../trust/quarantine.js';
5
5
  import { listMemoryConflicts } from '../store/conflicts.js';
6
6
  import { DashboardConnection, loadDashboardRows } from '../store/dashboard-reads.js';
7
7
  import { DAY_MS } from '../util/time.js';
8
+ import { round1 } from '../util/format.js';
8
9
  export { DAY_MS };
9
10
  export const LAYERS = ['buffer', 'episodic', 'semantic', 'trace'];
10
11
  // Wire order of `Band`, and the order the mockup sorts bands in.
@@ -99,9 +100,6 @@ function cutHead(content) {
99
100
  end -= 1;
100
101
  return { head: content.slice(0, end), truncated: true };
101
102
  }
102
- function round1(value) {
103
- return Math.round(value * 10) / 10;
104
- }
105
103
  function newDraft(identity) {
106
104
  return {
107
105
  summary: {
@@ -179,7 +179,7 @@ async function handlePost(ctx, segments) {
179
179
  }
180
180
  const body = await readActionBody(req);
181
181
  const tenantId = resolveTenantId({});
182
- const result = action(hippoRoot, tenantId, body);
182
+ const result = await action(hippoRoot, tenantId, body);
183
183
  if (result.changed)
184
184
  snapshots.invalidate();
185
185
  if (result.entry) {
@@ -300,7 +300,7 @@ function answerDashboardFailure(req, res, err) {
300
300
  const clientFault = err instanceof ParamError || err instanceof URIError || err instanceof BodyTimeoutError;
301
301
  // A cut-short response is logged even for a client fault; a bare 400 is not.
302
302
  if (res.headersSent || !clientFault) {
303
- log.error('dashboard request failed', { ...errorFields(err), error: errorMessage(err), path: req.url });
303
+ log.error('dashboard request failed', { ...errorFields(err), error: errorMessage(err), path: (req.url ?? '/').split('?')[0] });
304
304
  }
305
305
  if (res.headersSent) {
306
306
  res.end();
@@ -318,9 +318,20 @@ function answerDashboardFailure(req, res, err) {
318
318
  return jsonResponse(res, { error: 'Malformed URL path' }, 400);
319
319
  jsonResponse(res, { error: 'Internal error' }, 500);
320
320
  }
321
- function printDashboardBanner(boundPort, token, distUiDir) {
321
+ const DASHBOARD_URL_FILE = 'dashboard.url';
322
+ function printDashboardBanner(hippoRoot, boundPort, token, distUiDir) {
322
323
  // The banner is the `hippo dashboard` command's printed result, so it stays on stdout.
323
- console.log(`Hippo Dashboard running at http://localhost:${boundPort}/?token=${token}`);
324
+ const url = `http://localhost:${boundPort}/?token=${token}`;
325
+ if (process.stdout.isTTY) {
326
+ console.log(`Hippo Dashboard running at ${url}`);
327
+ }
328
+ else {
329
+ // Captured stdout lands in logs, so the token goes to an owner-only file, created fresh so its mode holds.
330
+ const file = path.join(hippoRoot, DASHBOARD_URL_FILE);
331
+ fs.rmSync(file, { force: true });
332
+ fs.writeFileSync(file, `${url}\n`, { flag: 'wx', mode: 0o600 });
333
+ console.log(`Hippo Dashboard running at http://localhost:${boundPort}/ (open the URL with its token from ${file})`);
334
+ }
324
335
  if (distUiDir !== null)
325
336
  console.log(`Serving React UI from ${distUiDir}`);
326
337
  console.log('Press Ctrl+C to stop.');
@@ -406,7 +417,7 @@ export function serveDashboard(hippoRoot, port = 3333, token = randomBytes(32).t
406
417
  server.listen(port, '127.0.0.1', () => {
407
418
  // SAFETY: listen() was given a TCP port, so address() is AddressInfo, never a pipe name.
408
419
  const boundPort = server.address().port;
409
- printDashboardBanner(boundPort, token, hasDistUi ? distUiDir : null);
420
+ printDashboardBanner(hippoRoot, boundPort, token, hasDistUi ? distUiDir : null);
410
421
  });
411
422
  return server;
412
423
  }
@@ -8,6 +8,7 @@ export interface OpenedDb {
8
8
  readonly db: DatabaseSyncLike;
9
9
  readonly facts: OpenFacts | null;
10
10
  }
11
- /** A new connection with the store's pragmas and migrations applied and the mirror cleanup run when it is due; the caller owns and closes it. */
11
+ /** A new connection with the store's pragmas and migrations applied and the mirror cleanup run when it is due; the caller owns and closes it.
12
+ * Outside any request scope (CLI start-up, timers) this is the only handle there is; openHippoDb reuses the scope's when one exists. */
12
13
  export declare function connectWithFacts(hippoRoot: string, busyWaitMs?: number): OpenedDb;
13
14
  //# sourceMappingURL=connect.d.ts.map
@@ -51,7 +51,8 @@ function archiveTop(db) {
51
51
  const row = db.prepare('SELECT MAX(id) AS top FROM raw_archive').get();
52
52
  return Number(row?.top ?? 0);
53
53
  }
54
- /** A new connection with the store's pragmas and migrations applied and the mirror cleanup run when it is due; the caller owns and closes it. */
54
+ /** A new connection with the store's pragmas and migrations applied and the mirror cleanup run when it is due; the caller owns and closes it.
55
+ * Outside any request scope (CLI start-up, timers) this is the only handle there is; openHippoDb reuses the scope's when one exists. */
55
56
  export function connectWithFacts(hippoRoot, busyWaitMs = DEFAULT_BUSY_WAIT_MS) {
56
57
  createStoreFilesOwnerOnly(hippoRoot);
57
58
  const db = new DatabaseSync(getHippoDbPath(hippoRoot));
@@ -1,11 +1,12 @@
1
1
  import { PACKAGE_VERSION, compareSemver } from '../util/version.js';
2
- import { errorMessage, log } from '../util/log.js';
2
+ import { errorFields, errorMessage, log } from '../util/log.js';
3
3
  import { importLegacyEmbeddingIndex } from './vector-store.js';
4
4
  import { execWithBusyRetry, withWriteScope } from './busy.js';
5
5
  import { tableExists } from './tables.js';
6
6
  import { ensureMetaTable, getMeta, getSchemaVersion, setMeta, setSchemaVersion } from './meta.js';
7
7
  import { MEMORIES_FTS_DDL, REQUIRED_SCHEMA_OBJECTS, ensureContinuityIndexes, ensureContinuityTables } from './continuity.js';
8
8
  import { CURRENT_SCHEMA_VERSION, MIGRATIONS } from './migrations/index.js';
9
+ import { placeholders } from '../util/sql.js';
9
10
  export function getCurrentSchemaVersion() {
10
11
  return CURRENT_SCHEMA_VERSION;
11
12
  }
@@ -130,7 +131,7 @@ function ensureMetaDefaults(db) {
130
131
  const keys = META_DEFAULTS.map(([key]) => key);
131
132
  // SAFETY: the statement selects only the `key` column of `meta`, which is TEXT NOT NULL, so each row is { key: string }.
132
133
  const present = new Set(db
133
- .prepare(`SELECT key FROM meta WHERE key IN (${keys.map(() => '?').join(',')})`)
134
+ .prepare(`SELECT key FROM meta WHERE key IN (${placeholders(keys.length)})`)
134
135
  .all(...keys)
135
136
  .map((r) => r.key));
136
137
  if (present.size === META_DEFAULTS.length)
@@ -142,18 +143,24 @@ function ensureMetaDefaults(db) {
142
143
  }
143
144
  }
144
145
  function ensureOptionalFts(db) {
145
- let available = false;
146
146
  try {
147
147
  db.exec(MEMORIES_FTS_DDL);
148
148
  repairFtsDrift(db);
149
- available = true;
150
149
  }
151
150
  catch (err) {
151
+ // A busy or locked open says nothing about fts5, so only a missing module may flip the flag.
152
+ if (!/no such module/i.test(errorMessage(err)) || !/fts5/i.test(errorMessage(err))) {
153
+ log.warn(`full-text index not set up this open (${errorMessage(err)}); the next open retries`, errorFields(err));
154
+ return;
155
+ }
152
156
  log.warn(`full-text index unavailable (${errorMessage(err)}); search falls back to slower LIKE matching`);
157
+ writeFtsFlag(db, '0');
158
+ return;
153
159
  }
154
- // Read-first: only write when the flag actually changed, so a healthy
155
- // store's open doesn't take a write lock for a same-value upsert.
156
- const flag = available ? '1' : '0';
160
+ writeFtsFlag(db, '1');
161
+ }
162
+ // Read-first: only write when the flag actually changed, so a healthy store's open takes no write lock.
163
+ function writeFtsFlag(db, flag) {
157
164
  if (getMeta(db, 'fts5_available') !== flag)
158
165
  setMeta(db, 'fts5_available', flag);
159
166
  }
@@ -9,6 +9,7 @@ export const v11 = {
9
9
  if (!tableHasColumn(db, 'memories', 'superseded_by')) {
10
10
  db.exec(`ALTER TABLE memories ADD COLUMN superseded_by TEXT`);
11
11
  }
12
+ // SHORTCUT: overlaps idx_memories_layer_created (v01); dropping it is held until a schema migration is approved.
12
13
  db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_current ON memories(layer, created) WHERE superseded_by IS NULL`);
13
14
  },
14
15
  };
@@ -48,6 +48,7 @@ export const v14 = {
48
48
  if (!tableHasColumn(db, 'memories', 'owner')) {
49
49
  db.exec(`ALTER TABLE memories ADD COLUMN owner TEXT`);
50
50
  }
51
+ // SHORTCUT: artifact_ref has no index, so connector deletes and vault lookups scan the tenant; the index is held until a schema migration is approved.
51
52
  if (!tableHasColumn(db, 'memories', 'artifact_ref')) {
52
53
  db.exec(`ALTER TABLE memories ADD COLUMN artifact_ref TEXT`);
53
54
  }
@@ -2,8 +2,6 @@
2
2
  import type { DatabaseSyncLike } from './index.js';
3
3
  import type { MemoryEntry } from '../core/memory.js';
4
4
  import type { PhysicsParticle } from '../core/physics.js';
5
- export declare function float32ToBuffer(arr: number[]): Buffer;
6
- export declare function bufferToFloat32(buf: Buffer | Uint8Array): number[];
7
5
  /** Create the memory_physics table (migration v8). */
8
6
  export declare function createPhysicsTable(db: DatabaseSyncLike): void;
9
7
  /**
@@ -14,8 +12,6 @@ export declare function loadPhysicsState(db: DatabaseSyncLike, memoryIds?: strin
14
12
  * Save physics state for multiple particles (batch upsert).
15
13
  */
16
14
  export declare function savePhysicsState(db: DatabaseSyncLike, particles: PhysicsParticle[]): void;
17
- /** Initialize physics state for a memory from its embedding vector; returns the new particle without persisting (the caller must save). */
18
- export declare function initializeParticle(entry: MemoryEntry, embedding: number[], now?: Date): PhysicsParticle;
19
15
  /** Drops every particle, then seeds one per entry that has an embedding; `entries` may be read lazily, as each batch is written before the next is read. */
20
16
  export declare function resetAllPhysicsState(db: DatabaseSyncLike, entries: Iterable<MemoryEntry>, embeddingIndex: Record<string, number[]>, now?: Date): number;
21
17
  /** Refresh mass, charge and temperature for existing particles from current memory attributes (called during consolidation). */
@@ -1,23 +1,11 @@
1
1
  /** Physics state persistence: particle state (position, velocity, mass, charge, temperature) in SQLite, with BLOB columns for 384-dim vectors. */
2
2
  import { evalNow, strengthAblation } from '../core/ablation.js';
3
3
  import { withWriteScope } from './busy.js';
4
- import { computeMass, computeCharge, computeTemperature, vecZero } from '../core/physics.js';
4
+ import { computeMass, computeCharge, computeTemperature, initializeParticle } from '../core/physics.js';
5
5
  import { calculateStrength } from '../core/memory.js';
6
6
  import { DAY_MS } from '../util/time.js';
7
- // Float32Array <-> Buffer serialization
8
- export function float32ToBuffer(arr) {
9
- const f32 = new Float32Array(arr);
10
- return Buffer.from(f32.buffer);
11
- }
12
- export function bufferToFloat32(buf) {
13
- if (buf.byteLength === 0 || buf.byteLength % 4 !== 0)
14
- return [];
15
- // Ensure we have a properly aligned copy (SQLite may return Uint8Array, not Buffer)
16
- const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
17
- const aligned = new ArrayBuffer(bytes.length);
18
- new Uint8Array(aligned).set(bytes);
19
- return Array.from(new Float32Array(aligned));
20
- }
7
+ import { placeholders } from '../util/sql.js';
8
+ import { bufferToFloat32, float32ToBuffer } from '../util/vector-codec.js';
21
9
  // Schema migration
22
10
  /** Create the memory_physics table (migration v8). */
23
11
  export function createPhysicsTable(db) {
@@ -48,10 +36,10 @@ export function loadPhysicsState(db, memoryIds) {
48
36
  // Chunked so a whole-store candidate list stays under SQLite's bound-parameter limit.
49
37
  for (let i = 0; i < memoryIds.length; i += PHYSICS_ID_CHUNK) {
50
38
  const chunk = memoryIds.slice(i, i + PHYSICS_ID_CHUNK);
51
- const placeholders = chunk.map(() => '?').join(',');
39
+ const marks = placeholders(chunk.length);
52
40
  // SAFETY: the SELECT projects exactly the PhysicsRow columns, in order.
53
41
  rows = rows.concat(db.prepare(`SELECT memory_id, position_blob, velocity_blob, mass, charge, temperature, last_simulation
54
- FROM memory_physics WHERE memory_id IN (${placeholders})`).all(...chunk));
42
+ FROM memory_physics WHERE memory_id IN (${marks})`).all(...chunk));
55
43
  }
56
44
  }
57
45
  else {
@@ -97,20 +85,6 @@ export function savePhysicsState(db, particles) {
97
85
  }
98
86
  });
99
87
  }
100
- /** Initialize physics state for a memory from its embedding vector; returns the new particle without persisting (the caller must save). */
101
- export function initializeParticle(entry, embedding, now = evalNow()) {
102
- const strength = calculateStrength(entry, now, { ablation: strengthAblation() });
103
- const ageDays = (now.getTime() - new Date(entry.created).getTime()) / DAY_MS;
104
- return {
105
- memoryId: entry.id,
106
- position: [...embedding], // copy — position will diverge from original embedding
107
- velocity: vecZero(embedding.length),
108
- mass: computeMass(strength, entry.retrieval_count),
109
- charge: computeCharge(entry.emotional_valence),
110
- temperature: computeTemperature(ageDays, 1.0),
111
- lastSimulation: now.toISOString(),
112
- };
113
- }
114
88
  const RESET_BATCH = 64;
115
89
  /** Drops every particle, then seeds one per entry that has an embedding; `entries` may be read lazily, as each batch is written before the next is read. */
116
90
  export function resetAllPhysicsState(db, entries, embeddingIndex, now = evalNow()) {
@@ -15,6 +15,7 @@ export declare class RequestStores implements RequestScope {
15
15
  readonly busyWaitMs: number | undefined;
16
16
  readonly requestId: string | undefined;
17
17
  readonly deadline: import("../util/request-scope.js").RequestDeadline | undefined;
18
+ readonly signal: AbortSignal | undefined;
18
19
  readonly setupDoneFor: string | undefined;
19
20
  constructor(opts?: RequestStoresOptions);
20
21
  get closed(): boolean;