@hasna/mementos 0.14.68 → 0.14.70

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 (57) hide show
  1. package/README.md +155 -73
  2. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +2 -0
  3. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +1 -0
  4. package/dist/cli/commands/io-clean.d.ts.map +1 -1
  5. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  6. package/dist/cli/commands/storage.d.ts.map +1 -1
  7. package/dist/cli/commands/system-mcp.d.ts.map +1 -1
  8. package/dist/cli/commands/system-profile.d.ts.map +1 -1
  9. package/dist/cli/global-options.d.ts +27 -0
  10. package/dist/cli/global-options.d.ts.map +1 -0
  11. package/dist/cli/index.js +699 -301
  12. package/dist/cli/register-all.d.ts +17 -0
  13. package/dist/cli/register-all.d.ts.map +1 -0
  14. package/dist/cli/startup-side-effects.d.ts +9 -0
  15. package/dist/cli/startup-side-effects.d.ts.map +1 -0
  16. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +2 -0
  17. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +1 -0
  18. package/dist/db/agents.d.ts.map +1 -1
  19. package/dist/db/api-mode.d.ts +70 -2
  20. package/dist/db/api-mode.d.ts.map +1 -1
  21. package/dist/db/database.d.ts +16 -0
  22. package/dist/db/database.d.ts.map +1 -1
  23. package/dist/db/locks.d.ts.map +1 -1
  24. package/dist/db/memories.d.ts +24 -6
  25. package/dist/db/memories.d.ts.map +1 -1
  26. package/dist/db/migrations.d.ts +1 -0
  27. package/dist/db/migrations.d.ts.map +1 -1
  28. package/dist/db/pg-migrations.d.ts.map +1 -1
  29. package/dist/db/session-jobs.d.ts.map +1 -1
  30. package/dist/db/store-backend.d.ts +33 -0
  31. package/dist/db/store-backend.d.ts.map +1 -0
  32. package/dist/index.js +196 -62
  33. package/dist/lib/auto-memory-queue.d.ts +2 -0
  34. package/dist/lib/auto-memory-queue.d.ts.map +1 -1
  35. package/dist/lib/auto-memory.d.ts +1 -0
  36. package/dist/lib/auto-memory.d.ts.map +1 -1
  37. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  38. package/dist/lib/enum-validation.d.ts +20 -0
  39. package/dist/lib/enum-validation.d.ts.map +1 -0
  40. package/dist/lib/gdpr.d.ts.map +1 -1
  41. package/dist/mcp/index.d.ts.map +1 -1
  42. package/dist/mcp/index.js +301 -78
  43. package/dist/mcp/tools/system-tools-memory-admin.d.ts.map +1 -1
  44. package/dist/server/helpers.d.ts +11 -0
  45. package/dist/server/helpers.d.ts.map +1 -1
  46. package/dist/server/index.d.ts.map +1 -1
  47. package/dist/server/index.js +304 -72
  48. package/dist/storage.d.ts +11 -2
  49. package/dist/storage.d.ts.map +1 -1
  50. package/dist/storage.js +42 -11
  51. package/dist/test-support/preload-local-store.d.ts +2 -0
  52. package/dist/test-support/preload-local-store.d.ts.map +1 -0
  53. package/dist/test-support/store-isolation.d.ts +83 -0
  54. package/dist/test-support/store-isolation.d.ts.map +1 -0
  55. package/dist/types/index.d.ts +8 -4
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/package.json +1 -1
@@ -65,6 +65,27 @@ var __export = (target, all) => {
65
65
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
66
66
  var __require = import.meta.require;
67
67
 
68
+ // src/generated/storage-kit/mode.ts
69
+ function normalizeStorageMode(value) {
70
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
71
+ if (normalized === "local")
72
+ return { mode: "local", deprecatedAlias: null };
73
+ if (normalized === "cloud")
74
+ return { mode: "cloud", deprecatedAlias: null };
75
+ if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
76
+ return { mode: "cloud", deprecatedAlias: normalized };
77
+ }
78
+ throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
79
+ }
80
+ var DEPRECATED_STORAGE_MODE_ALIASES;
81
+ var init_mode = __esm(() => {
82
+ DEPRECATED_STORAGE_MODE_ALIASES = [
83
+ "remote",
84
+ "hybrid",
85
+ "self_hosted"
86
+ ];
87
+ });
88
+
68
89
  // src/storage.ts
69
90
  import { Database } from "bun:sqlite";
70
91
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -281,18 +302,20 @@ function warnDeprecatedStorageMode(alias) {
281
302
  warnedDeprecatedModes.add(alias);
282
303
  process.emitWarning(`${MEMENTOS_STORAGE_ENV.mode}="${alias}" is deprecated; use "cloud". ` + `"${alias}" now maps to pure-remote cloud storage. The local<->remote ` + `sync path is deprecated and is not the fleet cutover path.`, { type: "DeprecationWarning", code: "MEMENTOS_STORAGE_MODE_ALIAS" });
283
304
  }
284
- function normalizeStorageMode(value) {
285
- if (!value)
305
+ function normalizeStorageMode2(value, source) {
306
+ if (!value || !value.trim())
286
307
  return null;
287
- const normalized = value.trim().toLowerCase();
288
- if (normalized === "local" || normalized === "cloud") {
289
- return normalized;
308
+ let normalized;
309
+ try {
310
+ normalized = normalizeStorageMode(value);
311
+ } catch (error) {
312
+ const detail = error instanceof Error ? error.message : String(error);
313
+ throw new Error(`mementos: ${source}=${value} is not a valid mode. ${detail}`);
290
314
  }
291
- if (normalized === "remote" || normalized === "hybrid") {
292
- warnDeprecatedStorageMode(normalized);
293
- return "cloud";
315
+ if (normalized.deprecatedAlias) {
316
+ warnDeprecatedStorageMode(normalized.deprecatedAlias);
294
317
  }
295
- return null;
318
+ return normalized.mode;
296
319
  }
297
320
  function readConfigFile() {
298
321
  if (!existsSync2(STORAGE_CONFIG_PATH)) {
@@ -317,7 +340,7 @@ function getStorageDatabaseUrl() {
317
340
  }
318
341
  function getStorageModeOverride() {
319
342
  for (const env of MODE_ENV_NAMES) {
320
- const value = normalizeStorageMode(readEnv(env.name) ?? undefined);
343
+ const value = normalizeStorageMode2(readEnv(env.name) ?? undefined, env.name);
321
344
  if (value)
322
345
  return value;
323
346
  }
@@ -327,7 +350,7 @@ function getStorageConfig() {
327
350
  const fileConfig = readConfigFile();
328
351
  const modeOverride = getStorageModeOverride();
329
352
  const envConnectionString = getConfiguredConnectionString();
330
- const fileMode = normalizeStorageMode(fileConfig.mode);
353
+ const fileMode = normalizeStorageMode2(fileConfig.mode, `${STORAGE_CONFIG_PATH} "mode"`);
331
354
  const merged = {
332
355
  ...DEFAULT_STORAGE_CONFIG,
333
356
  ...fileConfig,
@@ -449,6 +472,7 @@ function getStorageConnectionString(dbName = "mementos") {
449
472
  }
450
473
  var _serverContext = false, PgSyncPool, MEMENTOS_STORAGE_ENV, MEMENTOS_STORAGE_FALLBACK_ENV, LOCAL_DATA_DIR, DEFAULT_STORAGE_CONFIG, STORAGE_CONFIG_DIR, STORAGE_CONFIG_PATH, DATABASE_ENV_NAMES, MODE_ENV_NAMES, warnedDeprecatedModes, SECRET_QUERY_PARAMS;
451
474
  var init_storage = __esm(() => {
475
+ init_mode();
452
476
  PgSyncPool = class PgSyncPool {
453
477
  worker;
454
478
  status;
@@ -568,7 +592,7 @@ import { tmpdir } from "os";
568
592
  import { join as join3 } from "path";
569
593
  import { writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
570
594
  import { randomUUID } from "crypto";
571
- function firstEnv(...keys) {
595
+ function firstEnv(keys) {
572
596
  for (const k of keys) {
573
597
  const v = process.env[k]?.trim();
574
598
  if (v)
@@ -576,8 +600,38 @@ function firstEnv(...keys) {
576
600
  }
577
601
  return;
578
602
  }
603
+ function firstEnvKey(keys) {
604
+ for (const k of keys) {
605
+ if (process.env[k]?.trim())
606
+ return k;
607
+ }
608
+ return null;
609
+ }
579
610
  function hasDatabaseUrl() {
580
- return Boolean(firstEnv("HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"));
611
+ return Boolean(firstEnv(DATABASE_URL_ENV_KEYS));
612
+ }
613
+ function isLoopbackHost(rawHost) {
614
+ const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
615
+ return host === "localhost" || host === "::1" || /^127\./.test(host);
616
+ }
617
+ function assertRequestAllowedUnderTest(baseUrl) {
618
+ if (process.env["NODE_ENV"] !== "test")
619
+ return;
620
+ if (process.env[ALLOW_REMOTE_API_IN_TESTS_ENV]?.trim())
621
+ return;
622
+ let host;
623
+ try {
624
+ host = new URL(baseUrl).hostname;
625
+ } catch {
626
+ host = "";
627
+ }
628
+ if (host && isLoopbackHost(host))
629
+ return;
630
+ throw new Error("api-mode: REFUSING to make a cloud request from a test process \u2014 this would write to or read " + "from the SHARED PRODUCTION memory store, where test fixtures are indistinguishable from real " + `memories.
631
+ ` + ` host : ${host || "(unparseable base URL)"}
632
+ ` + ` how this happens: a selector set at module scope (after the bun test preload ran), or \`bun test\` ` + `invoked from a directory with no bunfig.toml so the preload never loaded.
633
+ ` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
634
+ ` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
581
635
  }
582
636
  function normalizeBase(raw) {
583
637
  let base = raw.trim().replace(/\/+$/, "");
@@ -585,9 +639,24 @@ function normalizeBase(raw) {
585
639
  return base;
586
640
  return `${base}/v1`;
587
641
  }
642
+ function assertUnambiguousStoreEnv() {
643
+ if (firstEnvKey(DB_PATH_ENV_KEYS))
644
+ return;
645
+ if (hasDatabaseUrl())
646
+ return;
647
+ const urlKey = firstEnvKey(API_URL_ENV_KEYS);
648
+ const keyKey = firstEnvKey(API_KEY_ENV_KEYS);
649
+ if (urlKey && !keyKey) {
650
+ throw new MementosStoreConfigError(`${urlKey} points at the cloud memory store but ${API_KEY_ENV_KEYS[0]} is not set. ` + `Refusing to serve the on-box SQLite store in its place, because it holds a different ` + `dataset. Set ${API_KEY_ENV_KEYS[0]} to reach the cloud store. If you meant to use the ` + `on-box SQLite store, unset ${urlKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
651
+ }
652
+ if (keyKey && !urlKey) {
653
+ throw new MementosStoreConfigError(`${keyKey} is set but ${API_URL_ENV_KEYS[0]} is not, so the cloud memory store cannot be ` + `reached. Refusing to serve the on-box SQLite store in its place, because it holds a ` + `different dataset. Set ${API_URL_ENV_KEYS[0]} to reach the cloud store. If you meant to ` + `use the on-box SQLite store, unset ${keyKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
654
+ }
655
+ }
588
656
  function getApiConfig() {
589
- const rawBase = firstEnv("HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL");
590
- const apiKey = firstEnv("HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY");
657
+ assertUnambiguousStoreEnv();
658
+ const rawBase = firstEnv(API_URL_ENV_KEYS);
659
+ const apiKey = firstEnv(API_KEY_ENV_KEYS);
591
660
  if (!rawBase || !apiKey)
592
661
  return null;
593
662
  return { baseUrl: normalizeBase(rawBase), apiKey };
@@ -601,6 +670,7 @@ function apiRequestRaw(method, path, body) {
601
670
  const cfg = getApiConfig();
602
671
  if (!cfg)
603
672
  throw new Error("api-mode: not configured (HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY)");
673
+ assertRequestAllowedUnderTest(cfg.baseUrl);
604
674
  const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
605
675
  const hasBody = body !== undefined && body !== null;
606
676
  const timeout = process.env["HASNA_MEMENTOS_API_TIMEOUT"] || DEFAULT_TIMEOUT_S;
@@ -667,13 +737,13 @@ x-api-key: ${cfg.apiKey}
667
737
  }
668
738
  return { status, body: respBody };
669
739
  }
670
- function apiJson(method, path, body) {
740
+ function apiJson(method, path, body, options) {
671
741
  const raw = apiRequestRaw(method, path, body);
672
742
  if (raw.status >= 200 && raw.status < 300) {
673
743
  const data = raw.body.trim() ? JSON.parse(raw.body) : undefined;
674
744
  return { status: raw.status, data };
675
745
  }
676
- if (raw.status === 404) {
746
+ if (raw.status === 404 && options?.allow404) {
677
747
  return { status: 404, data: undefined };
678
748
  }
679
749
  let msg = `mementos cloud ${method} ${path} \u2192 ${raw.status}`;
@@ -705,8 +775,19 @@ function toQuery(params) {
705
775
  const s = sp.toString();
706
776
  return s ? `?${s}` : "";
707
777
  }
708
- var ApiRequestError, DEFAULT_TIMEOUT_S = "45";
778
+ var API_URL_ENV_KEYS, API_KEY_ENV_KEYS, DATABASE_URL_ENV_KEYS, ALLOW_REMOTE_API_IN_TESTS_ENV = "MEMENTOS_ALLOW_REMOTE_API_IN_TESTS", DB_PATH_ENV_KEYS, MementosStoreConfigError, ApiRequestError, DEFAULT_TIMEOUT_S = "45";
709
779
  var init_api_mode = __esm(() => {
780
+ API_URL_ENV_KEYS = ["HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL"];
781
+ API_KEY_ENV_KEYS = ["HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"];
782
+ DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
783
+ DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
784
+ MementosStoreConfigError = class MementosStoreConfigError extends Error {
785
+ code = "MEMENTOS_STORE_CONFIG";
786
+ constructor(message) {
787
+ super(message);
788
+ this.name = "MementosStoreConfigError";
789
+ }
790
+ };
710
791
  ApiRequestError = class ApiRequestError extends Error {
711
792
  status;
712
793
  body;
@@ -720,7 +801,24 @@ var init_api_mode = __esm(() => {
720
801
  });
721
802
 
722
803
  // src/db/migrations.ts
723
- var MIGRATIONS;
804
+ var MEMORY_VERSION_SNAPSHOT_TRIGGER = `
805
+ CREATE TRIGGER IF NOT EXISTS memories_version_snapshot
806
+ BEFORE UPDATE ON memories
807
+ WHEN NEW.version > OLD.version
808
+ BEGIN
809
+ INSERT OR IGNORE INTO memory_versions (
810
+ id, memory_id, version, value, importance, scope, category, tags,
811
+ summary, pinned, status, when_to_use, created_at
812
+ ) VALUES (
813
+ lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' ||
814
+ lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' ||
815
+ lower(hex(randomblob(6))),
816
+ OLD.id, OLD.version, OLD.value, OLD.importance, OLD.scope, OLD.category,
817
+ OLD.tags, OLD.summary, OLD.pinned, OLD.status, OLD.when_to_use,
818
+ OLD.updated_at
819
+ );
820
+ END;
821
+ `, MIGRATIONS;
724
822
  var init_migrations = __esm(() => {
725
823
  MIGRATIONS = [
726
824
  `
@@ -1618,6 +1716,10 @@ CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_memory ON memory_reflec
1618
1716
  CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_kind ON memory_reflection_lessons(kind);
1619
1717
 
1620
1718
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
1719
+ `,
1720
+ `
1721
+ ${MEMORY_VERSION_SNAPSHOT_TRIGGER}
1722
+ INSERT OR IGNORE INTO _migrations (id) VALUES (36);
1621
1723
  `
1622
1724
  ];
1623
1725
  });
@@ -1632,6 +1734,7 @@ __export(exports_database, {
1632
1734
  now: () => now,
1633
1735
  getDbPath: () => getDbPath2,
1634
1736
  getDatabase: () => getDatabase,
1737
+ escapeLikePrefix: () => escapeLikePrefix,
1635
1738
  closeDatabase: () => closeDatabase
1636
1739
  });
1637
1740
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, cpSync as cpSync2 } from "fs";
@@ -1806,15 +1909,20 @@ function uuid() {
1806
1909
  function shortUuid() {
1807
1910
  return crypto.randomUUID().slice(0, 8);
1808
1911
  }
1912
+ function escapeLikePrefix(s) {
1913
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
1914
+ }
1809
1915
  function resolvePartialId(db, table, partialId) {
1810
1916
  if (!ALLOWED_TABLES.has(table)) {
1811
1917
  throw new Error(`Invalid table name: ${table}`);
1812
1918
  }
1919
+ if (partialId === "")
1920
+ return null;
1813
1921
  if (partialId.length >= 36) {
1814
1922
  const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
1815
1923
  return row?.id ?? null;
1816
1924
  }
1817
- const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
1925
+ const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ? ESCAPE '\\'`).all(`${escapeLikePrefix(partialId)}%`);
1818
1926
  if (rows.length === 1) {
1819
1927
  return rows[0].id;
1820
1928
  }
@@ -1926,8 +2034,19 @@ var init_hooks = __esm(() => {
1926
2034
  });
1927
2035
 
1928
2036
  // src/types/index.ts
1929
- var AgentConflictError, EntityNotFoundError, MemoryNotFoundError, DuplicateMemoryError, VersionConflictError, MemoryConflictError;
2037
+ var MEMORY_SCOPES, MEMORY_CATEGORIES, MEMORY_SOURCES, MEMORY_STATUSES, AgentConflictError, EntityNotFoundError, MemoryNotFoundError, DuplicateMemoryError, VersionConflictError, MemoryConflictError;
1930
2038
  var init_types = __esm(() => {
2039
+ MEMORY_SCOPES = ["global", "shared", "private", "working"];
2040
+ MEMORY_CATEGORIES = [
2041
+ "preference",
2042
+ "fact",
2043
+ "knowledge",
2044
+ "history",
2045
+ "procedural",
2046
+ "resource"
2047
+ ];
2048
+ MEMORY_SOURCES = ["user", "agent", "system", "auto", "imported"];
2049
+ MEMORY_STATUSES = ["active", "archived", "expired"];
1931
2050
  AgentConflictError = class AgentConflictError extends Error {
1932
2051
  conflict = true;
1933
2052
  existing_id;
@@ -2090,6 +2209,41 @@ var init_redact = __esm(() => {
2090
2209
  ];
2091
2210
  });
2092
2211
 
2212
+ // src/lib/enum-validation.ts
2213
+ function formatEnumViolation(v) {
2214
+ return `Invalid ${v.field}: "${v.value}". Allowed values: ${v.allowed.join(", ")}.`;
2215
+ }
2216
+ function validateEnumField(field, value) {
2217
+ const allowed = ENUM_FIELDS[field];
2218
+ if (!allowed)
2219
+ return null;
2220
+ if (value === undefined || value === null || value === "")
2221
+ return null;
2222
+ if (typeof value === "string" && allowed.includes(value))
2223
+ return null;
2224
+ return { field, value: String(value), allowed };
2225
+ }
2226
+ function validateMemoryEnums(input) {
2227
+ for (const field of Object.keys(ENUM_FIELDS)) {
2228
+ if (!(field in input))
2229
+ continue;
2230
+ const violation = validateEnumField(field, input[field]);
2231
+ if (violation)
2232
+ return violation;
2233
+ }
2234
+ return null;
2235
+ }
2236
+ var ENUM_FIELDS;
2237
+ var init_enum_validation = __esm(() => {
2238
+ init_types();
2239
+ ENUM_FIELDS = {
2240
+ category: MEMORY_CATEGORIES,
2241
+ scope: MEMORY_SCOPES,
2242
+ source: MEMORY_SOURCES,
2243
+ status: MEMORY_STATUSES
2244
+ };
2245
+ });
2246
+
2093
2247
  // src/lib/poisoning.ts
2094
2248
  function computeTrustScore(value, key, existingMemories, claimedImportance) {
2095
2249
  let score = 1;
@@ -2320,7 +2474,10 @@ function parseMemoryRow(row) {
2320
2474
  }
2321
2475
  function createMemory(input, dedupeMode = "merge", db) {
2322
2476
  if (!db && isApiMode()) {
2323
- const { data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
2477
+ const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
2478
+ if (!data || !data.id) {
2479
+ throw new ApiRequestError(`mementos cloud POST /memories \u2192 ${status} but no memory was returned; the write did not persist (key: ${input.key})`, status, "");
2480
+ }
2324
2481
  return data;
2325
2482
  }
2326
2483
  const d = db || getDatabase();
@@ -2459,16 +2616,25 @@ function bulkUpsertMemories(memories, db) {
2459
2616
  const d = db || getDatabase();
2460
2617
  let inserted = 0;
2461
2618
  let skipped = 0;
2619
+ let rejected = 0;
2462
2620
  const errors = [];
2463
- const insert = d.prepare(`INSERT OR IGNORE INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, namespace, created_by_agent, when_to_use, sequence_group, sequence_order, metadata, access_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at)
2464
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
2621
+ const insert = d.prepare(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, namespace, created_by_agent, when_to_use, sequence_group, sequence_order, metadata, access_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at)
2622
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2623
+ ON CONFLICT DO NOTHING`);
2465
2624
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
2466
2625
  for (const mem of memories) {
2467
2626
  const key = mem["key"];
2468
2627
  const id = mem["id"] || uuid();
2469
2628
  try {
2470
2629
  if (!key) {
2471
- errors.push(`skipped row without key (id=${id})`);
2630
+ rejected++;
2631
+ errors.push(`rejected row without key (id=${id})`);
2632
+ continue;
2633
+ }
2634
+ const violation = validateMemoryEnums(mem);
2635
+ if (violation) {
2636
+ rejected++;
2637
+ errors.push(`Rejected "${key}": ${formatEnumViolation(violation)}`);
2472
2638
  continue;
2473
2639
  }
2474
2640
  const timestamp = now();
@@ -2517,10 +2683,11 @@ function bulkUpsertMemories(memories, db) {
2517
2683
  skipped++;
2518
2684
  }
2519
2685
  } catch (e) {
2686
+ rejected++;
2520
2687
  errors.push(`Failed "${String(key)}": ${e instanceof Error ? e.message : String(e)}`);
2521
2688
  }
2522
2689
  }
2523
- return { inserted, skipped, errors, total: memories.length };
2690
+ return { inserted, skipped, rejected, errors, total: memories.length };
2524
2691
  }
2525
2692
  function ensureMemoryReferences(d, input) {
2526
2693
  const t = now();
@@ -2548,7 +2715,7 @@ function listMemoriesByKey(key, db) {
2548
2715
  }
2549
2716
  function getMemory(id, db) {
2550
2717
  if (!db && isApiMode()) {
2551
- const { status, data } = apiJson("GET", `/memories/${encodeURIComponent(id)}`);
2718
+ const { status, data } = apiJson("GET", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
2552
2719
  return status === 404 ? null : data ?? null;
2553
2720
  }
2554
2721
  const d = db || getDatabase();
@@ -2881,36 +3048,22 @@ function getMemoryEmbeddings(ids, db) {
2881
3048
  }
2882
3049
  function updateMemory(id, input, db) {
2883
3050
  if (!db && isApiMode()) {
2884
- const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input);
3051
+ const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input, { allow404: true });
2885
3052
  if (status === 404)
2886
3053
  throw new MemoryNotFoundError(id);
3054
+ if (data && typeof input.version === "number" && typeof data.version === "number" && data.version <= input.version) {
3055
+ throw new Error(`Update did not persist for memory ${id}: the server returned success but the record is unchanged ` + `(version still ${data.version}). Your data was NOT written. ` + `The server is likely running a build predating the partial-id fix \u2014 pass the full 36-character id as a workaround.`);
3056
+ }
2887
3057
  return data;
2888
3058
  }
2889
3059
  const d = db || getDatabase();
2890
3060
  const existing = getMemory(id, d);
2891
3061
  if (!existing)
2892
3062
  throw new MemoryNotFoundError(id);
3063
+ const memoryId = existing.id;
2893
3064
  if (existing.version !== input.version) {
2894
3065
  throw new VersionConflictError(id, input.version, existing.version);
2895
3066
  }
2896
- try {
2897
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, when_to_use, created_at)
2898
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2899
- uuid(),
2900
- existing.id,
2901
- existing.version,
2902
- existing.value,
2903
- existing.importance,
2904
- existing.scope,
2905
- existing.category,
2906
- JSON.stringify(existing.tags),
2907
- existing.summary,
2908
- existing.pinned ? 1 : 0,
2909
- existing.status,
2910
- existing.when_to_use || null,
2911
- existing.updated_at
2912
- ]);
2913
- } catch {}
2914
3067
  const sets = ["version = version + 1", "updated_at = ?"];
2915
3068
  const params = [now()];
2916
3069
  if (input.value !== undefined) {
@@ -2960,15 +3113,18 @@ function updateMemory(id, input, db) {
2960
3113
  if (input.tags !== undefined) {
2961
3114
  sets.push("tags = ?");
2962
3115
  params.push(JSON.stringify(input.tags));
2963
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
3116
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
2964
3117
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
2965
3118
  for (const tag of input.tags) {
2966
- insertTag.run(id, tag);
3119
+ insertTag.run(memoryId, tag);
2967
3120
  }
2968
3121
  }
2969
- params.push(id);
2970
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
2971
- const updated = getMemory(id, d);
3122
+ params.push(memoryId);
3123
+ const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
3124
+ if (result.changes === 0) {
3125
+ throw new Error(`Update affected no rows for memory ${memoryId}: the record was read but not written. ` + `This is a bug in @hasna/mementos, not a bad argument \u2014 please report it.`);
3126
+ }
3127
+ const updated = getMemory(memoryId, d);
2972
3128
  if (input.value !== undefined) {
2973
3129
  try {
2974
3130
  const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
@@ -2989,14 +3145,15 @@ function updateMemory(id, input, db) {
2989
3145
  }
2990
3146
  function deleteMemory(id, db) {
2991
3147
  if (!db && isApiMode()) {
2992
- const { status } = apiJson("DELETE", `/memories/${encodeURIComponent(id)}`);
3148
+ const { status } = apiJson("DELETE", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
2993
3149
  return status !== 404;
2994
3150
  }
2995
3151
  const d = db || getDatabase();
2996
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
3152
+ const memoryId = resolvePartialId(d, "memories", id) ?? id;
3153
+ const result = d.run("DELETE FROM memories WHERE id = ?", [memoryId]);
2997
3154
  if (result.changes > 0) {
2998
3155
  hookRegistry.runHooks("PostMemoryDelete", {
2999
- memoryId: id,
3156
+ memoryId,
3000
3157
  timestamp: Date.now()
3001
3158
  });
3002
3159
  }
@@ -3010,11 +3167,12 @@ function bulkDeleteMemories(ids, db) {
3010
3167
  return data?.deleted ?? 0;
3011
3168
  }
3012
3169
  const d = db || getDatabase();
3013
- const placeholders = ids.map(() => "?").join(",");
3014
- const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
3170
+ const resolvedIds = ids.map((id) => resolvePartialId(d, "memories", id) ?? id);
3171
+ const placeholders = resolvedIds.map(() => "?").join(",");
3172
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...resolvedIds);
3015
3173
  const count = countRow.c;
3016
3174
  if (count > 0) {
3017
- d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
3175
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, resolvedIds);
3018
3176
  }
3019
3177
  return count;
3020
3178
  }
@@ -3141,6 +3299,7 @@ var init_memories = __esm(() => {
3141
3299
  init_types();
3142
3300
  init_database();
3143
3301
  init_redact();
3302
+ init_enum_validation();
3144
3303
  init_hooks();
3145
3304
  init_poisoning();
3146
3305
  init_entity_memories();
@@ -3209,7 +3368,7 @@ function createEntity(input, db) {
3209
3368
  }
3210
3369
  function getEntity(id, db) {
3211
3370
  if (!db && isApiMode()) {
3212
- const { status, data } = apiJson("GET", `/entities/${encodeURIComponent(id)}`);
3371
+ const { status, data } = apiJson("GET", `/entities/${encodeURIComponent(id)}`, undefined, { allow404: true });
3213
3372
  if (status === 404 || !data)
3214
3373
  throw new EntityNotFoundError(id);
3215
3374
  return data;
@@ -3292,7 +3451,7 @@ function listEntities(filter = {}, db) {
3292
3451
  }
3293
3452
  function updateEntity(id, input, db) {
3294
3453
  if (!db && isApiMode()) {
3295
- const { status, data } = apiJson("PATCH", `/entities/${encodeURIComponent(id)}`, input);
3454
+ const { status, data } = apiJson("PATCH", `/entities/${encodeURIComponent(id)}`, input, { allow404: true });
3296
3455
  if (status === 404 || !data)
3297
3456
  throw new EntityNotFoundError(id);
3298
3457
  return data;
@@ -3325,7 +3484,7 @@ function updateEntity(id, input, db) {
3325
3484
  }
3326
3485
  function deleteEntity(id, db) {
3327
3486
  if (!db && isApiMode()) {
3328
- const { status } = apiJson("DELETE", `/entities/${encodeURIComponent(id)}`);
3487
+ const { status } = apiJson("DELETE", `/entities/${encodeURIComponent(id)}`, undefined, { allow404: true });
3329
3488
  if (status === 404)
3330
3489
  throw new EntityNotFoundError(id);
3331
3490
  return;
@@ -4253,7 +4412,7 @@ function createRelation(input, db) {
4253
4412
  }
4254
4413
  function getRelation(id, db) {
4255
4414
  if (!db && isApiMode()) {
4256
- const { status, data } = apiJson("GET", `/relations/${encodeURIComponent(id)}`);
4415
+ const { status, data } = apiJson("GET", `/relations/${encodeURIComponent(id)}`, undefined, { allow404: true });
4257
4416
  if (status === 404 || !data)
4258
4417
  throw new Error(`Relation not found: ${id}`);
4259
4418
  return data;
@@ -4298,7 +4457,7 @@ function listRelations(filter, db) {
4298
4457
  }
4299
4458
  function deleteRelation(id, db) {
4300
4459
  if (!db && isApiMode()) {
4301
- const { status } = apiJson("DELETE", `/relations/${encodeURIComponent(id)}`);
4460
+ const { status } = apiJson("DELETE", `/relations/${encodeURIComponent(id)}`, undefined, { allow404: true });
4302
4461
  if (status === 404)
4303
4462
  throw new Error(`Relation not found: ${id}`);
4304
4463
  return;
@@ -4947,6 +5106,32 @@ class AutoMemoryQueue {
4947
5106
  getStats() {
4948
5107
  return { ...this.stats, pending: this.queue.length };
4949
5108
  }
5109
+ async waitForIdleForTests(timeoutMs = 3000) {
5110
+ const start = Date.now();
5111
+ while (Date.now() - start < timeoutMs) {
5112
+ if (this.queue.length === 0 && this.activeCount === 0)
5113
+ return;
5114
+ await new Promise((r) => setTimeout(r, 20));
5115
+ }
5116
+ throw new Error("autoMemoryQueue did not become idle before test reset");
5117
+ }
5118
+ resetForTests(handler) {
5119
+ if (this.activeCount !== 0) {
5120
+ throw new Error("Cannot reset autoMemoryQueue while jobs are processing");
5121
+ }
5122
+ this.queue = [];
5123
+ this.running = false;
5124
+ this.stats = {
5125
+ pending: 0,
5126
+ processing: 0,
5127
+ processed: 0,
5128
+ failed: 0,
5129
+ dropped: 0
5130
+ };
5131
+ if (handler !== undefined) {
5132
+ this.handler = handler;
5133
+ }
5134
+ }
4950
5135
  startLoop() {
4951
5136
  this.running = true;
4952
5137
  this.loop();
@@ -4992,6 +5177,7 @@ var init_auto_memory_queue = __esm(() => {
4992
5177
  // src/lib/auto-memory.ts
4993
5178
  var exports_auto_memory = {};
4994
5179
  __export(exports_auto_memory, {
5180
+ resetAutoMemoryForTests: () => resetAutoMemoryForTests,
4995
5181
  processConversationTurn: () => processConversationTurn,
4996
5182
  getAutoMemoryStats: () => getAutoMemoryStats,
4997
5183
  configureAutoMemory: () => configureAutoMemory
@@ -5152,6 +5338,12 @@ function getAutoMemoryStats() {
5152
5338
  function configureAutoMemory(config) {
5153
5339
  providerRegistry.configure(config);
5154
5340
  }
5341
+ async function resetAutoMemoryForTests() {
5342
+ if (autoMemoryQueue.getStats().processing > 0) {
5343
+ await autoMemoryQueue.waitForIdleForTests();
5344
+ }
5345
+ autoMemoryQueue.resetForTests(processJob);
5346
+ }
5155
5347
  var DEDUP_SIMILARITY_THRESHOLD = 0.85;
5156
5348
  var init_auto_memory = __esm(() => {
5157
5349
  init_memories();
@@ -52822,7 +53014,7 @@ function createWebhookHook(input, db) {
52822
53014
  }
52823
53015
  function getWebhookHook(id, db) {
52824
53016
  if (!db && isApiMode()) {
52825
- const { status, data } = apiJson("GET", `/webhooks/${encodeURIComponent(id)}`);
53017
+ const { status, data } = apiJson("GET", `/webhooks/${encodeURIComponent(id)}`, undefined, { allow404: true });
52826
53018
  if (status === 404 || !data)
52827
53019
  return null;
52828
53020
  return data;
@@ -52858,7 +53050,7 @@ function updateWebhookHook(id, updates, db) {
52858
53050
  enabled: updates.enabled,
52859
53051
  priority: updates.priority,
52860
53052
  description: updates.description
52861
- });
53053
+ }, { allow404: true });
52862
53054
  if (status === 404 || !data)
52863
53055
  return null;
52864
53056
  return data;
@@ -52889,7 +53081,7 @@ function updateWebhookHook(id, updates, db) {
52889
53081
  }
52890
53082
  function deleteWebhookHook(id, db) {
52891
53083
  if (!db && isApiMode()) {
52892
- const { status } = apiJson("DELETE", `/webhooks/${encodeURIComponent(id)}`);
53084
+ const { status } = apiJson("DELETE", `/webhooks/${encodeURIComponent(id)}`, undefined, { allow404: true });
52893
53085
  return status === 204 || status === 200;
52894
53086
  }
52895
53087
  const d = db || getDatabase();
@@ -52924,6 +53116,8 @@ hookRegistry.register({
52924
53116
  priority: 100,
52925
53117
  description: "Trigger async LLM entity extraction when a memory is saved",
52926
53118
  handler: async (ctx) => {
53119
+ if (process.env["NODE_ENV"] === "test")
53120
+ return;
52927
53121
  if (ctx.wasUpdated)
52928
53122
  return;
52929
53123
  const processConversationTurn2 = await getAutoMemory();
@@ -53161,7 +53355,7 @@ function createSessionJob(input, db) {
53161
53355
  }
53162
53356
  function getSessionJob(id, db) {
53163
53357
  if (!db && isApiMode()) {
53164
- const { status, data } = apiJson("GET", `/sessions/jobs/${encodeURIComponent(id)}`);
53358
+ const { status, data } = apiJson("GET", `/sessions/jobs/${encodeURIComponent(id)}`, undefined, { allow404: true });
53165
53359
  if (status === 404 || !data)
53166
53360
  return null;
53167
53361
  return data;
@@ -54296,6 +54490,16 @@ function errorResponse(message, status, details) {
54296
54490
  body["details"] = details;
54297
54491
  return json(body, status);
54298
54492
  }
54493
+ function describeConstraintViolation(e) {
54494
+ if (!e || typeof e !== "object")
54495
+ return null;
54496
+ const code = String(e.code ?? "");
54497
+ const message = String(e.message ?? "");
54498
+ const isConstraint = code.startsWith("SQLITE_CONSTRAINT") || /^23\d{3}$/.test(code) || /constraint failed/i.test(message);
54499
+ if (!isConstraint)
54500
+ return null;
54501
+ return `Request rejected by a database constraint: ${message || code}. Check that enum fields (category, scope, source, status) and referenced ids are valid.`;
54502
+ }
54299
54503
  var MAX_BODY_BYTES = 1 * 1024 * 1024;
54300
54504
  async function readJson(req) {
54301
54505
  try {
@@ -54496,6 +54700,7 @@ var FORMAT_UNITS = [
54496
54700
 
54497
54701
  // src/server/routes/memories-crud.ts
54498
54702
  init_router();
54703
+ init_enum_validation();
54499
54704
  init_types();
54500
54705
  addRoute("GET", "/api/memories", (_req, url) => {
54501
54706
  const q = getSearchParams(url);
@@ -54544,6 +54749,14 @@ addRoute("POST", "/api/memories", async (req) => {
54544
54749
  if (!body["key"] || !body["value"]) {
54545
54750
  return errorResponse("Missing required fields: key, value", 400);
54546
54751
  }
54752
+ const violation = validateMemoryEnums(body);
54753
+ if (violation) {
54754
+ return errorResponse(formatEnumViolation(violation), 400, {
54755
+ field: violation.field,
54756
+ value: violation.value,
54757
+ allowed: violation.allowed
54758
+ });
54759
+ }
54547
54760
  try {
54548
54761
  if (body["ttl_ms"] !== undefined && typeof body["ttl_ms"] === "string") {
54549
54762
  body["ttl_ms"] = parseDuration(body["ttl_ms"]);
@@ -54573,6 +54786,14 @@ addRoute("PATCH", "/api/memories/:id", async (req, _url, params) => {
54573
54786
  if (!body) {
54574
54787
  return errorResponse("Invalid JSON body", 400);
54575
54788
  }
54789
+ const patchViolation = validateMemoryEnums(body);
54790
+ if (patchViolation) {
54791
+ return errorResponse(formatEnumViolation(patchViolation), 400, {
54792
+ field: patchViolation.field,
54793
+ value: patchViolation.value,
54794
+ allowed: patchViolation.allowed
54795
+ });
54796
+ }
54576
54797
  const updateBody = { ...body };
54577
54798
  if (updateBody["version"] === undefined) {
54578
54799
  const existing = getMemory(params["id"]);
@@ -55602,6 +55823,12 @@ addRoute("POST", "/api/memories/bulk-upsert", async (req) => {
55602
55823
  }
55603
55824
  const memoriesArr = body["memories"];
55604
55825
  const result = bulkUpsertMemories(memoriesArr);
55826
+ if (result.rejected > 0) {
55827
+ return json({
55828
+ ...result,
55829
+ error: `${result.rejected} of ${result.total} memories were rejected and did not persist. See errors.`
55830
+ }, 400);
55831
+ }
55605
55832
  return json(result, 201);
55606
55833
  });
55607
55834
 
@@ -55985,7 +56212,7 @@ function registerAgent(name, sessionId, description, role, projectId, db) {
55985
56212
  }
55986
56213
  function getAgent(idOrName, db) {
55987
56214
  if (!db && isApiMode()) {
55988
- const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`);
56215
+ const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`, undefined, { allow404: true });
55989
56216
  if (status === 404 || !data)
55990
56217
  return null;
55991
56218
  return data;
@@ -55997,7 +56224,7 @@ function getAgent(idOrName, db) {
55997
56224
  row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
55998
56225
  if (row)
55999
56226
  return parseAgentRow(row);
56000
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
56227
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ? ESCAPE '\\'").all(`${escapeLikePrefix(idOrName)}%`);
56001
56228
  if (rows.length === 1)
56002
56229
  return parseAgentRow(rows[0]);
56003
56230
  return null;
@@ -56024,7 +56251,7 @@ function listAgentsByProject(projectId, db) {
56024
56251
  }
56025
56252
  function updateAgent(id, updates, db) {
56026
56253
  if (!db && isApiMode()) {
56027
- const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates);
56254
+ const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates, { allow404: true });
56028
56255
  if (status === 404 || !data)
56029
56256
  return null;
56030
56257
  return data;
@@ -56132,7 +56359,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
56132
56359
  }
56133
56360
  function releaseLock(lockId, agentId, db) {
56134
56361
  if (!db && isApiMode()) {
56135
- const { status } = apiJson("DELETE", `/locks/${encodeURIComponent(lockId)}`, { agent_id: agentId });
56362
+ const { status } = apiJson("DELETE", `/locks/${encodeURIComponent(lockId)}`, { agent_id: agentId }, { allow404: true });
56136
56363
  return status !== 404;
56137
56364
  }
56138
56365
  const d = db || getDatabase();
@@ -56315,7 +56542,7 @@ function registerProject(name, path, description, memoryPrefix, db) {
56315
56542
  }
56316
56543
  function getProject(idOrPath, db) {
56317
56544
  if (!db && isApiMode()) {
56318
- const { status, data } = apiJson("GET", `/projects/${encodeURIComponent(idOrPath)}`);
56545
+ const { status, data } = apiJson("GET", `/projects/${encodeURIComponent(idOrPath)}`, undefined, { allow404: true });
56319
56546
  if (status === 404 || !data)
56320
56547
  return null;
56321
56548
  return data;
@@ -59043,7 +59270,7 @@ function createSubscription(input, db) {
59043
59270
  }
59044
59271
  function deleteSubscription(id, db) {
59045
59272
  if (!db && isApiMode()) {
59046
- const { status } = apiJson("DELETE", `/subscriptions/${encodeURIComponent(id)}`);
59273
+ const { status } = apiJson("DELETE", `/subscriptions/${encodeURIComponent(id)}`, undefined, { allow404: true });
59047
59274
  return status !== 404;
59048
59275
  }
59049
59276
  const d = db || getDatabase();
@@ -59337,6 +59564,11 @@ function startServer(port) {
59337
59564
  try {
59338
59565
  return await matched.handler(req, url2, matched.params);
59339
59566
  } catch (e) {
59567
+ const constraint = describeConstraintViolation(e);
59568
+ if (constraint) {
59569
+ console.warn(`[mementos-serve] ${req.method} ${pathname}: rejected \u2014 ${constraint}`);
59570
+ return errorResponse(constraint, 400);
59571
+ }
59340
59572
  console.error(`[mementos-serve] ${req.method} ${pathname}:`, e);
59341
59573
  return errorResponse("Internal server error", 500);
59342
59574
  }